diff --git a/packages/provider-core/src/server/managedProvider.test.ts b/packages/provider-core/src/server/managedProvider.test.ts index 5295232d3b14..8547d3a47bd6 100644 --- a/packages/provider-core/src/server/managedProvider.test.ts +++ b/packages/provider-core/src/server/managedProvider.test.ts @@ -861,7 +861,7 @@ describe("makeManagedServerProvider", () => { windows: [{ ...weekly }], }); assert.strictEqual(yield* provider.getSnapshot, partial); - assert.deepStrictEqual((yield* provider.refresh).usageLimits, failedLimits); + assert.deepStrictEqual((yield* provider.refresh).usageLimits, partial.usageLimits); const completeLimits = { checkedAt: "2026-04-10T00:00:04.000Z", diff --git a/packages/provider-core/src/server/usageLimits.test.ts b/packages/provider-core/src/server/usageLimits.test.ts index 4b9c1b39c862..7d2a132dea3b 100644 --- a/packages/provider-core/src/server/usageLimits.test.ts +++ b/packages/provider-core/src/server/usageLimits.test.ts @@ -118,5 +118,14 @@ describe("resolveUsageLimitsAfterProbe", () => { expect(resolveUsageLimitsAfterProbe({ published, probed: failed })).toBe(published); expect(resolveUsageLimitsAfterProbe({ published, probed: unsupported })).toBe(unsupported); expect(resolveUsageLimitsAfterProbe({ published: undefined, probed: failed })).toBe(failed); + const streamed = { + checkedAt, + windows: [weekly], + unavailable: { reason: "probeFailed" as const }, + }; + expect(resolveUsageLimitsAfterProbe({ published: streamed, probed: failed })).toBe(streamed); + expect( + resolveUsageLimitsAfterProbe({ published: { ...streamed, windows: [] }, probed: failed }), + ).toBe(failed); }); }); diff --git a/packages/provider-core/src/server/usageLimits.ts b/packages/provider-core/src/server/usageLimits.ts index 1e4bcd009085..bffc6391aafe 100644 --- a/packages/provider-core/src/server/usageLimits.ts +++ b/packages/provider-core/src/server/usageLimits.ts @@ -104,7 +104,12 @@ export function resolveUsageLimitsAfterProbe(input: { readonly probed: ServerProviderUsageLimits | undefined; }): ServerProviderUsageLimits | undefined { const { published, probed } = input; - if (probed?.unavailable?.reason === "probeFailed" && published && !published.unavailable) { + // Probes can fail permanently (org-disabled nonessential traffic); keep streamed windows. + if ( + probed?.unavailable?.reason === "probeFailed" && + published && + (!published.unavailable || published.windows.length > 0) + ) { return published; } if ( diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index e9d5f6e604f3..665f49b5f747 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -23,6 +23,7 @@ import { paceRemainingPercent, usesChatGptSharing, resolveNewThreadModelSelection, + limitsNotice, } from "./usageLimits.ts"; const now = Date.parse("2026-09-03T12:00:00.000Z"); @@ -188,41 +189,38 @@ describe("pools", () => { auth: { status: "authenticated", email: "Same@example.com" }, usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 40 }] }, }); - const input = new Map([ - [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [native] } }], - [ - EnvironmentId.make("env-b"), - { - entry: { target: { label: "Desktop" } }, - serverConfig: { - providers: [ - { - ...native, - usageLimits: { - checkedAt: "2026-09-03T11:30:00.000Z", - windows: [{ ...window, usedPercent: 55 }], + const newer: NonNullable = { + checkedAt: "2026-09-03T11:30:00.000Z", + windows: [{ ...window, usedPercent: 55 }], + }; + const inputWith = (newerLimits: NonNullable) => + new Map([ + [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [native] } }], + [ + EnvironmentId.make("env-b"), + { + entry: { target: { label: "Desktop" } }, + serverConfig: { + providers: [{ ...native, usageLimits: newerLimits }], + usageLimitSources: [ + { + ...source, + accounts: [ + { + id: "claude-same@example.com.json", + driver: claude, + email: "same@example.com", + plan: "Claude Subscription", + usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 10 }] }, + }, + ], }, - }, - ], - usageLimitSources: [ - { - ...source, - accounts: [ - { - id: "claude-same@example.com.json", - driver: claude, - email: "same@example.com", - plan: "Claude Subscription", - usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 10 }] }, - }, - ], - }, - ], + ], + }, }, - }, - ], - ]); - const accounts = collectLimitAccounts(input); + ], + ]); + const accounts = collectLimitAccounts(inputWith(newer)); expect(accounts).toHaveLength(1); expect(accounts[0]).toMatchObject({ key: "env-a:claude", @@ -234,6 +232,11 @@ describe("pools", () => { ], }); expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(55); + const [clean] = collectLimitAccounts( + inputWith({ ...newer, unavailable: { reason: "probeFailed" } }), + ); + expect(clean?.limits).toMatchObject({ windows: [{ usedPercent: 40 }] }); + expect(clean?.limits.unavailable).toBeUndefined(); }); it("uses the freshest reported plan even when the newest quota has no plan", () => { @@ -1385,6 +1388,24 @@ describe("/usage-limits", () => { ), ).toEqual(["hub:duplicate", "hub:oss"]); const failed = provider({ usageLimits: { ...limits, unavailable: { reason: "probeFailed" } } }); + expect(limitsNotice(failed.usageLimits!)).toBeNull(); + expect(limitsNotice({ ...failed.usageLimits!, windows: [] })).toBe("Could not read limits."); + const newerEmpty = provider({ + usageLimits: { + checkedAt: "2026-09-03T11:59:00.000Z", + windows: [], + unavailable: { reason: "probeFailed" }, + }, + }); + for (const duplicates of [ + [failed, newerEmpty], + [newerEmpty, failed], + ]) { + expect( + collectProviderUsageLimits(selected.instanceId, duplicates, [], now)?.accounts[0]?.limits + .windows, + ).toEqual(limits.windows); + } expect( collectProviderUsageLimits(selected.instanceId, [failed], sources, now)?.accounts.map( (account) => account.id, diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index 24403fe8a6c5..57e40455c4c9 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -299,7 +299,8 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl !previous.environments.some((seen) => seen.environmentId === candidate.environmentId), ), ]; - const winner = fresher ? next : previous; + const byRank = readRank(next.limits) - readRank(previous.limits); + const winner = byRank > 0 || (byRank === 0 && fresher) ? next : previous; const creditSource = creditSources.get(key); // A hub e-mail names the account better than the driver's default instance name. const hubEmail = [previous, next].some( @@ -619,12 +620,17 @@ export function limitsNotice(limits: ServerProviderUsageLimits): string | null { if (limits.unavailable?.reason === "unsupported") { return limits.unavailable.message ?? "This account has no subscription limits."; } - if (limits.unavailable?.reason === "probeFailed") { + if (limits.unavailable?.reason === "probeFailed" && limits.windows.length === 0) { return limits.unavailable.message ?? "Could not read limits."; } return limits.windows.length === 0 ? "No limits reported." : null; } +function readRank(limits: ServerProviderUsageLimits): number { + if (limitsNotice(limits) !== null) return 0; + return limits.unavailable ? 1 : 2; +} + export function remainingPercent(window: ServerProviderUsageWindow): number { return Math.round(100 - Math.max(0, Math.min(100, window.usedPercent))); } @@ -830,12 +836,11 @@ export function collectProviderUsageLimits( const snapshot = { provider, limits: provider.usageLimits }; const key = resolveKey(nativeReport(provider)) ?? `instance:${provider.instanceId}`; const previous = nativeByAccount.get(key); - const previousUsable = previous ? limitsNotice(previous.usage.limits) === null : false; - const usable = limitsNotice(snapshot.limits) === null; + const rank = readRank(snapshot.limits); const providerWins = !previous || - (usable && !previousUsable) || - (usable === previousUsable && + rank > readRank(previous.usage.limits) || + (rank === readRank(previous.usage.limits) && Date.parse(snapshot.limits.checkedAt) > Date.parse(previous.usage.limits.checkedAt)); const previousCredit = previous?.credits; const credits =