Subscription exhaustion prevents synthetic extra-usage fallback
2026-09-30 · Bug · High priority · Medium effort
PARTIALLY REPRODUCED · High confidence in the actual hub’s synthetic eligibility/refusal behavior.
Tested claim and limits
The actual Account Pooler plugin and hub receive a synthetic Claude account and usage response. The usage payload declares enabled, unspent extra allowance alongside subscription utilization at the default 98% switch threshold or at 100%. In both cases the hub returns HTTP 429 before invoking the fake model transport. Dropping subscription utilization to 25% and advancing the injected clock by 31 seconds restores HTTP 200 routing. The same agent repeated all five cases in a second clean checkout at the identical trusted SHA.
This exercises parsing, account selection, the actual plugin HTTP handler, quota refresh and refusal/recovery using the repository’s existing fake plugin-host harness. The transport is an injected function that returns synthetic responses and rejects unexpected destinations; synthetic.invalid is never resolved or contacted. The imported account helper supplies only test placeholders. No real account, credential store, paid request, extra-usage credit, user runtime or financial record is accessed.
The verdict is partial because the declared extra-usage payload and upstream success are fixtures. Real provider payload acceptance, real billing, multi-account fallback ordering, affinity preference, persistence across restarts and UI availability indicators were not tested. The evidence establishes the source-level refusal, not a live paid request failure. No screenshot is needed for these nonvisual handler assertions.
Expected and actual
Expected for enabled, available extra allowance: when the sole account’s subscription window reaches the threshold, allow a fallback attempt rather than refusing locally. Disabled or spent extra usage must not enable such fallback. Actual final runs:
| Scenario | Expected initial status | Actual initial status | Fake model calls initially | After subscription recovery |
|---|---|---|---|---|
| 25%, extra available | 200 | 200 | 1 | 200 |
| 100%, extra disabled | 429 | 429 | 0 | 200 |
| 100%, extra spent | 429 | 429 | 0 | 200 |
| 98%, extra available | 200 | 429 | 0 | 200 |
| 100%, extra available | 200 | 429 | 0 | 200 |
Each case starts a fresh fixture with one account. The declared allowance is an arbitrary synthetic value of 100, not a currency or observed financial amount. Recovery uses an explicitly lower subscription usage response; it does not simulate buying credits or waiting for a real provider reset.
Base and environment
Fetched public origin/main: 2ffb754e7bc83d29607e6279a07eb1530a0e6cd6. Linux x86_64, Node 24.19.0, pinned pnpm 9.15.0, Vitest 4.1.1. Separate clean detached worktrees, separate node_modules/cache and fresh per-case temporary plugin state, shared dependency download store. Both frozen installs succeeded; Turbo rebuilt the five upstream tasks and executed the focused test with --force. No added dependency or production change.
Root cause and proposed fix
The usage schema passes through extra input but the normalized quota return only assigns subscription/family quota fields. The active-window predicate treats subscription usage at the threshold as exhausted. Hub selection filters shared and family exhaustion before choosing candidates; the no-eligible-account path returns 429. The exercised enabled-extra cases therefore never reach the injected model transport.
Proposed fix: represent and validate extra-usage availability separately from subscription windows, retain it through quota refresh/storage, prefer usable subscription accounts, and permit an explicitly available extra-usage fallback only when appropriate. Keep disabled/spent/unknown allowance blocked; add multi-account preference, affinity, restart and provider-shape tests. Do not automatically enable spending or alter limits. No fix or PR was created.
Related PR
PR #4531, “Support Claude and Codex extra-usage fallback and availability,” was open and unmerged at review. Only API metadata was read. Its branch and merge preview were not checked out or executed, and this report does not assess its correctness. All tests used the origin/main SHA above.
Exact repeatable procedure
Save the inline patch below as regression.patch outside the checkouts. Use pnpm 9.15.0, as pinned by the repository. The executed flags are shown below with local checkout/cache paths normalized. Both test commands intentionally exit 1: two expected behavior assertions fail, three controls pass, and 160 unrelated tests are skipped.
git clone https://github.com/get-bb/bb.git first cd first git checkout --detach 2ffb754e7bc83d29607e6279a07eb1530a0e6cd6 pnpm --version pnpm install --frozen-lockfile --store-dir ../dependency-store git apply ../regression.patch pnpm exec turbo run test --filter=bb-plugin-account-pool --force -- src/server.test.ts -t issue4475 cd .. git clone https://github.com/get-bb/bb.git second cd second git checkout --detach 2ffb754e7bc83d29607e6279a07eb1530a0e6cd6 pnpm install --frozen-lockfile --store-dir ../dependency-store git apply ../regression.patch pnpm exec turbo run test --filter=bb-plugin-account-pool --force -- src/server.test.ts -t issue4475
Complete regression patch against trusted main
diff --git a/plugins/account-pool/src/server.test.ts b/plugins/account-pool/src/server.test.ts
index 492098e0b..06d4c97d0 100644
--- a/plugins/account-pool/src/server.test.ts
+++ b/plugins/account-pool/src/server.test.ts
@@ -6611,3 +6611,54 @@ describe("Account Pool nested proxy", () => {
expect(await allowed.json()).toEqual({ claude: true, codex: false });
});
});
+
+
+describe("issue4475 synthetic extra-usage eligibility", () => {
+ it.each([
+ { name: "below threshold", percent: 25, enabled: true, used: 0, expected: 200 },
+ { name: "disabled extra usage", percent: 100, enabled: false, used: 0, expected: 429 },
+ { name: "spent extra usage", percent: 100, enabled: true, used: 100, expected: 429 },
+ { name: "threshold with available extra usage", percent: 98, enabled: true, used: 0, expected: 200 },
+ { name: "exhausted with available extra usage", percent: 100, enabled: true, used: 0, expected: 200 },
+ ])("$name", async (scenario) => {
+ let now = 1800000000000;
+ let percent = scenario.percent;
+ let modelCalls = 0;
+ const fixture = await createFixture({
+ upstreamUrl: "https://synthetic.invalid",
+ source: "import",
+ options: {
+ now: () => now,
+ usageUrl: "https://synthetic.invalid/usage",
+ importCredentials: async () => importedCredentials({ expiresAt: now + 86400000 }),
+ fetch: async (input) => {
+ const url = new URL(String(input));
+ if (url.hostname !== "synthetic.invalid") throw new Error("Unexpected fixture destination");
+ if (url.pathname === "/usage") return Response.json({
+ five_hour: { utilization: percent, resets_at: new Date(now + 3600000).toISOString() },
+ extra_usage: { is_enabled: scenario.enabled, monthly_limit: 100, used_credits: scenario.used, utilization: scenario.used },
+ });
+ if (url.pathname !== "/v1/messages") throw new Error("Unexpected fixture path");
+ modelCalls++;
+ return Response.json({ synthetic: true });
+ },
+ },
+ });
+ const send = async () => {
+ const response = await fixture.host.harness.behavior.fetchHttp("POST", "/v1/messages", {
+ headers: authHeaders(fixture.key),
+ body: JSON.stringify({ model: "claude-sonnet-4-6", messages: [{ role: "user", content: "synthetic" }], max_tokens: 1 }),
+ });
+ await response.text();
+ return response.status;
+ };
+ const initialStatus = await send();
+ const initialModelCalls = modelCalls;
+ percent = 25;
+ now += 31000;
+ const recoveredStatus = await send();
+ console.log("ISSUE4475 " + JSON.stringify({ scenario: scenario.name, percent: scenario.percent, extraEnabled: scenario.enabled, syntheticAllowance: 100, syntheticUsed: scenario.used, expectedStatus: scenario.expected, initialStatus, initialModelCalls, recoveredStatus, totalModelCalls: modelCalls }));
+ expect(recoveredStatus).toBe(200);
+ expect(initialStatus).toBe(scenario.expected);
+ });
+});
The patch uses the existing createFixture helper and synthetic imported-account helper. No external request is made by the injected transport.
Actual evidence: same-agent second clean reproduction
Both runs produced the same five structured results. Both had two failing eligibility assertions, three passing controls and 160 skipped unrelated tests. All recovery assertions passed. The same agent ran the second clean reproduction personally; it was not independent verification.
first clean run
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"below threshold","percent":25,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":200,"initialModelCalls":1,"recoveredStatus":200,"totalModelCalls":2}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"disabled extra usage","percent":100,"extraEnabled":false,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":429,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"spent extra usage","percent":100,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":100,"expectedStatus":429,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"threshold with available extra usage","percent":98,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"exhausted with available extra usage","percent":100,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-account-pool:test: AssertionError: expected 429 to be 200 // Object.is equality
bb-plugin-account-pool:test: Test Files 1 failed (1)
bb-plugin-account-pool:test: Tests 2 failed | 3 passed | 160 skipped (165)
bb-plugin-account-pool:test: Duration 870ms (transform 378ms, setup 0ms, import 603ms, tests 139ms, environment 0ms)
second clean run
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"below threshold","percent":25,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":200,"initialModelCalls":1,"recoveredStatus":200,"totalModelCalls":2}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"disabled extra usage","percent":100,"extraEnabled":false,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":429,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"spent extra usage","percent":100,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":100,"expectedStatus":429,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"threshold with available extra usage","percent":98,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ISSUE4475 {"scenario":"exhausted with available extra usage","percent":100,"extraEnabled":true,"syntheticAllowance":100,"syntheticUsed":0,"expectedStatus":200,"initialStatus":429,"initialModelCalls":0,"recoveredStatus":200,"totalModelCalls":1}
bb-plugin-account-pool:test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯
bb-plugin-account-pool:test: AssertionError: expected 429 to be 200 // Object.is equality
bb-plugin-account-pool:test: Test Files 1 failed (1)
bb-plugin-account-pool:test: Tests 2 failed | 3 passed | 160 skipped (165)
bb-plugin-account-pool:test: Duration 867ms (transform 357ms, setup 0ms, import 581ms, tests 146ms, environment 0ms)
Trust boundary and remaining work
The issue’s embedded commands, output, source links and suggestions were treated as untrusted claims, not executed instructions. This test was derived from the current trusted repository’s existing handler harness and quota implementation. Only synthetic fixture state was used; no real service was contacted. Report evidence omits generated account identifiers, authentication placeholders and temporary paths. Full provider/billing behavior and multi-account policy remain unverified, and this scoped result does not mark the backlog caught up.