| author | |
| committer | |
| log | 6abdf37a50ce6c1c9fabc499f4d0e159a6182d90 |
| tree | 0d30e4ceb0550ccd97b00ba3f0b34612ef495218 |
| parent | f729e8fd837e4d6de86781351c3aee5a4ea19ab3 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>2 files changed, 117 insertions(+), 8 deletions(-)
packages/shared/src/usageLimits.test.ts+89| ... | ... | @@ -433,6 +433,55 @@ describe("pools", () => { |
| 433 | 433 | expect(account?.environments).toEqual([{ environmentId: "env-a", label: "Laptop" }]); |
| 434 | 434 | }); |
| 435 | 435 | |
| 436 | it("redeems through the hub when it holds a credit, even with a fresher native read", () => { | |
| 437 | const native = provider({ | |
| 438 | driver: claude, | |
| 439 | instanceId: ProviderInstanceId.make("claude"), | |
| 440 | auth: { status: "authenticated", email: "same@example.com" }, | |
| 441 | usageLimits: { | |
| 442 | checkedAt: "2026-09-03T11:30:00.000Z", | |
| 443 | windows: [{ ...window, usedPercent: 40 }], | |
| 444 | resetCredits: { availableCount: 3, nextCreditId: "native-credit" }, | |
| 445 | }, | |
| 446 | }); | |
| 447 | const input = new Map([ | |
| 448 | [ | |
| 449 | EnvironmentId.make("env-a"), | |
| 450 | { | |
| 451 | ...laptop, | |
| 452 | serverConfig: { | |
| 453 | providers: [native], | |
| 454 | usageLimitSources: [ | |
| 455 | { | |
| 456 | ...source, | |
| 457 | accounts: [ | |
| 458 | { | |
| 459 | id: "claude-same@example.com.json", | |
| 460 | driver: claude, | |
| 461 | email: "same@example.com", | |
| 462 | usageLimits: { | |
| 463 | checkedAt, | |
| 464 | windows: [{ ...window, usedPercent: 55 }], | |
| 465 | resetCredits: { availableCount: 2, nextCreditId: "hub-credit" }, | |
| 466 | }, | |
| 467 | }, | |
| 468 | ], | |
| 469 | }, | |
| 470 | ], | |
| 471 | }, | |
| 472 | }, | |
| 473 | ], | |
| 474 | ]); | |
| 475 | const [account] = collectLimitAccounts(input); | |
| 476 | // Only the hub path clears the routing cooldown it holds for this account. | |
| 477 | expect(account?.redeem).toEqual({ | |
| 478 | environmentId: "env-a", | |
| 479 | input: { sourceId: "hub", accountId: "claude-same@example.com.json", creditId: "hub-credit" }, | |
| 480 | }); | |
| 481 | // The fresher native balance is still the one shown. | |
| 482 | expect(account?.limits.resetCredits?.availableCount).toBe(3); | |
| 483 | }); | |
| 484 | ||
| 436 | 485 | it("redeems on the environment whose snapshot supplied the credits on show", () => { |
| 437 | 486 | const stale = provider({ |
| 438 | 487 | auth: { status: "authenticated", email: "same@example.com" }, |
| ... | ... | @@ -839,6 +888,46 @@ describe("/usage-limits", () => { |
| 839 | 888 | }); |
| 840 | 889 | }); |
| 841 | 890 | |
| 891 | it("redeems a native duplicate through the hub even when the native snapshot is fresher", () => { | |
| 892 | const fresher = provider({ | |
| 893 | usageLimits: { | |
| 894 | checkedAt: "2026-09-03T11:30:00.000Z", | |
| 895 | windows: [window], | |
| 896 | resetCredits: { availableCount: 3, nextCreditId: "native-credit" }, | |
| 897 | }, | |
| 898 | auth: { status: "authenticated", email: "same@example.com" }, | |
| 899 | }); | |
| 900 | const stale = [ | |
| 901 | { | |
| 902 | id: UsageLimitSourceId.make("hub"), | |
| 903 | kind: "cliproxy" as const, | |
| 904 | label: "Accounts", | |
| 905 | checkedAt: limits.checkedAt, | |
| 906 | accounts: [ | |
| 907 | { | |
| 908 | id: "duplicate", | |
| 909 | driver: fresher.driver, | |
| 910 | email: "SAME@example.com", | |
| 911 | usageLimits: { | |
| 912 | ...limits, | |
| 913 | resetCredits: { availableCount: 2, nextCreditId: "hub-credit" }, | |
| 914 | }, | |
| 915 | }, | |
| 916 | ], | |
| 917 | }, | |
| 918 | ]; | |
| 919 | const report = collectProviderUsageLimits(fresher.instanceId, [fresher], stale, now); | |
| 920 | // Only redeeming through the hub clears the routing cooldown it holds for | |
| 921 | // this account, so the hub wins the path even with a staler balance. | |
| 922 | expect(report?.accounts[0]?.resetCreditInput).toEqual({ | |
| 923 | sourceId: "hub", | |
| 924 | accountId: "duplicate", | |
| 925 | creditId: "hub-credit", | |
| 926 | }); | |
| 927 | // The fresher native balance is still the one shown. | |
| 928 | expect(report?.accounts[0]?.limits.resetCredits?.availableCount).toBe(3); | |
| 929 | }); | |
| 930 | ||
| 842 | 931 | it("keeps accounts and custom instances separate, filtering by driver", () => { |
| 843 | 932 | const report = collectProviderUsageLimits( |
| 844 | 933 | selected.instanceId, |
packages/shared/src/usageLimits.ts+28-8| ... | ... | @@ -189,7 +189,21 @@ export function collectLimitAccounts( |
| 189 | 189 | ): readonly LimitAccount[] { |
| 190 | 190 | const accounts = new Map<string, LimitAccount>(); |
| 191 | 191 | const creditSources = new Map<string, LimitAccount>(); |
| 192 | const hubRedeems = new Map<string, LimitAccount>(); | |
| 192 | 193 | const merge = (key: string, next: LimitAccount) => { |
| 194 | // Redeeming through a hub also clears the routing cooldown that hub holds | |
| 195 | // for the account. Redeeming natively against the same subscription resets | |
| 196 | // it upstream but leaves the hub refusing to route to the account until | |
| 197 | // its own cooldown expires, so a hub target wins the redemption outright | |
| 198 | // while the displayed balance still follows the freshest read. | |
| 199 | const previousHub = hubRedeems.get(key); | |
| 200 | if ( | |
| 201 | next.redeem && | |
| 202 | "sourceId" in next.redeem.input && | |
| 203 | (!previousHub || Date.parse(next.limits.checkedAt) > Date.parse(previousHub.limits.checkedAt)) | |
| 204 | ) { | |
| 205 | hubRedeems.set(key, next); | |
| 206 | } | |
| 193 | 207 | const previousCredit = creditSources.get(key); |
| 194 | 208 | if ( |
| 195 | 209 | next.limits.resetCredits && |
| ... | ... | @@ -224,9 +238,9 @@ export function collectLimitAccounts( |
| 224 | 238 | environments, |
| 225 | 239 | // A hub only names the account when no environment has it natively. |
| 226 | 240 | sourceLabel: environments.length > 0 ? null : (previous.sourceLabel ?? next.sourceLabel), |
| 227 | redeem: creditSource | |
| 228 | ? creditSource.redeem | |
| 229 | : (winner.redeem ?? previous.redeem ?? next.redeem), | |
| 241 | redeem: | |
| 242 | hubRedeems.get(key)?.redeem ?? | |
| 243 | (creditSource ? creditSource.redeem : (winner.redeem ?? previous.redeem ?? next.redeem)), | |
| 230 | 244 | limits: { |
| 231 | 245 | ...winner.limits, |
| 232 | 246 | ...(creditSource?.limits.resetCredits |
| ... | ... | @@ -645,14 +659,20 @@ export function collectProviderUsageLimits( |
| 645 | 659 | (a, b) => |
| 646 | 660 | Date.parse(b.account.usageLimits.checkedAt) - Date.parse(a.account.usageLimits.checkedAt), |
| 647 | 661 | )[0]; |
| 648 | const useHubCredits = | |
| 662 | // Two independent decisions. Which balance to *display* follows whichever | |
| 663 | // snapshot is fresher. Which path to *redeem through* always prefers the | |
| 664 | // hub, because only the hub path clears the routing cooldown it holds for | |
| 665 | // that account; redeeming natively against the same account resets the | |
| 666 | // subscription upstream but leaves the hub refusing to route to it until | |
| 667 | // its own cooldown expires. A hub credit id that a fresher native redeem | |
| 668 | // already spent comes back as `alreadyRedeemed`, which still clears the | |
| 669 | // cooldown, so preferring it is safe even when the hub snapshot is stale. | |
| 670 | const hubCreditId = hubCredits?.account.usageLimits.resetCredits?.nextCreditId; | |
| 671 | const showHubCredits = | |
| 649 | 672 | hubCredits && |
| 650 | 673 | (!provider.usageLimits.resetCredits || |
| 651 | 674 | Date.parse(hubCredits.account.usageLimits.checkedAt) > |
| 652 | 675 | Date.parse(provider.usageLimits.checkedAt)); |
| 653 | const hubCreditId = useHubCredits | |
| 654 | ? hubCredits.account.usageLimits.resetCredits?.nextCreditId | |
| 655 | : undefined; | |
| 656 | 676 | accounts.push({ |
| 657 | 677 | id: provider.instanceId, |
| 658 | 678 | driver: provider.driver, |
| ... | ... | @@ -670,7 +690,7 @@ export function collectProviderUsageLimits( |
| 670 | 690 | ...(provider.displayName ? { displayName: provider.displayName } : {}), |
| 671 | 691 | ...(provider.accentColor ? { accentColor: provider.accentColor } : {}), |
| 672 | 692 | ...(provider.auth.email ? { email: provider.auth.email } : {}), |
| 673 | limits: useHubCredits | |
| 693 | limits: showHubCredits | |
| 674 | 694 | ? { ...provider.usageLimits, resetCredits: hubCredits.account.usageLimits.resetCredits } |
| 675 | 695 | : provider.usageLimits, |
| 676 | 696 | }); |