diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index b2bef55cb108..add8fc3c680c 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -950,7 +950,7 @@ describe("/usage-limits", () => { expect(report?.accounts[0]?.limits.resetCredits?.availableCount).toBe(3); }); - it("keeps accounts and custom instances separate, filtering by driver", () => { + it("keeps unidentified custom instances separate, filtering by driver", () => { const report = collectProviderUsageLimits( selected.instanceId, [ @@ -991,6 +991,45 @@ describe("/usage-limits", () => { expect(report?.notices).toEqual([]); }); + it("keeps the freshest usable duplicate and its latest native credits", () => { + const failed = provider({ + auth: selected.auth, + usageLimits: { ...limits, unavailable: { reason: "probeFailed" } }, + }); + const credited = provider({ + instanceId: ProviderInstanceId.make("codex-credit"), + auth: selected.auth, + usageLimits: { + checkedAt: "2026-09-03T11:20:00.000Z", + windows: [{ ...window, usedPercent: 50 }], + resetCredits: { availableCount: 2 }, + }, + }); + const freshest = provider({ + instanceId: ProviderInstanceId.make("codex-fresh"), + auth: selected.auth, + usageLimits: { + checkedAt: "2026-09-03T11:30:00.000Z", + windows: [{ ...window, usedPercent: 55 }], + }, + }); + const report = collectProviderUsageLimits( + failed.instanceId, + [failed, credited, freshest], + sources, + now, + ); + expect(report?.accounts.map((account) => account.id)).toEqual(["codex-fresh", "hub:oss"]); + expect(report?.accounts[0]).toMatchObject({ + limits: { + checkedAt: "2026-09-03T11:30:00.000Z", + windows: [{ usedPercent: 55 }], + resetCredits: { availableCount: 2 }, + }, + resetCreditInput: { instanceId: "codex-credit" }, + }); + }); + it("supports a source-only provider and keeps duplicates when the native probe failed", () => { expect( collectProviderUsageLimits(selected.instanceId, [provider({})], sources, now)?.accounts.map( diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index b1211bffd54d..7d24281d611d 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -630,11 +630,40 @@ export function collectProviderUsageLimits( ): UsageLimitsReport | null { const selected = providers.find((provider) => provider.instanceId === instanceId); if (!selected || !hasProviderUsageLimits(selected.driver, providers, sources)) return null; - const native = providersWithLimits(providers).filter( - (provider) => provider.driver === selected.driver, - ); - const nativeAccounts = new Set( - native.flatMap((provider) => { + const nativeByAccount = new Map< + string, + { provider: ServerProvider; creditProvider: ServerProvider | null } + >(); + for (const provider of providersWithLimits(providers)) { + if (provider.driver !== selected.driver || !provider.usageLimits) continue; + const key = + accountKey(provider.driver, provider.auth.email, provider.usageLimits) ?? + `instance:${provider.instanceId}`; + const previous = nativeByAccount.get(key); + const previousUsable = previous ? limitsNotice(previous.provider.usageLimits!) === null : false; + const usable = limitsNotice(provider.usageLimits) === null; + const providerWins = + !previous || + (usable && !previousUsable) || + (usable === previousUsable && + Date.parse(provider.usageLimits.checkedAt) > + Date.parse(previous.provider.usageLimits!.checkedAt)); + const previousCredit = previous?.creditProvider; + const creditProvider = + provider.usageLimits.resetCredits && + (!previousCredit || + Date.parse(provider.usageLimits.checkedAt) > + Date.parse(previousCredit.usageLimits!.checkedAt)) + ? provider + : (previousCredit ?? null); + nativeByAccount.set(key, { + provider: providerWins ? provider : previous.provider, + creditProvider, + }); + } + const native = [...nativeByAccount.values()]; + const nativeAccountKeys = new Set( + native.flatMap(({ provider }) => { const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits); return key && provider.usageLimits?.windows.length && !provider.usageLimits.unavailable ? [key] @@ -643,7 +672,7 @@ export function collectProviderUsageLimits( ); const accounts: Array = []; const notices: string[] = []; - for (const provider of native) { + for (const { provider, creditProvider } of native) { if (!provider.usageLimits) continue; const key = accountKey(provider.driver, provider.auth.email, provider.usageLimits); const hubCredits = sources @@ -670,9 +699,12 @@ export function collectProviderUsageLimits( const hubCreditId = hubCredits?.account.usageLimits.resetCredits?.nextCreditId; const showHubCredits = hubCredits && - (!provider.usageLimits.resetCredits || + (!creditProvider || Date.parse(hubCredits.account.usageLimits.checkedAt) > - Date.parse(provider.usageLimits.checkedAt)); + Date.parse(creditProvider.usageLimits!.checkedAt)); + const shownCredits = showHubCredits + ? hubCredits.account.usageLimits.resetCredits + : creditProvider?.usageLimits?.resetCredits; accounts.push({ id: provider.instanceId, driver: provider.driver, @@ -686,12 +718,12 @@ export function collectProviderUsageLimits( accountId: hubCredits.account.id, creditId: hubCreditId, } - : { instanceId: provider.instanceId }, + : { instanceId: creditProvider?.instanceId ?? provider.instanceId }, ...(provider.displayName ? { displayName: provider.displayName } : {}), ...(provider.accentColor ? { accentColor: provider.accentColor } : {}), ...(provider.auth.email ? { email: provider.auth.email } : {}), - limits: showHubCredits - ? { ...provider.usageLimits, resetCredits: hubCredits.account.usageLimits.resetCredits } + limits: shownCredits + ? { ...provider.usageLimits, resetCredits: shownCredits } : provider.usageLimits, }); } @@ -699,7 +731,7 @@ export function collectProviderUsageLimits( const matching = source.accounts.filter((account) => account.driver === selected.driver); for (const account of matching) { const key = accountKey(account.driver, account.email, account.usageLimits); - if (key && nativeAccounts.has(key)) continue; + if (key && nativeAccountKeys.has(key)) continue; accounts.push({ id: `${source.id}:${account.id}`, driver: account.driver,