From 618d78144edcc6a8d6827cc6323acaa21299ce6e Mon Sep 17 00:00:00 2001 From: Peter Lindsey Date: Wed, 10 Jun 2026 14:44:38 +0800 Subject: [PATCH] fix(usage): don't resolve subscription windows for an absent auth signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A missing per-turn authMode was mapped to "oauth", so an OpenAI api-key turn that arrived without an explicit auth mechanism could resolve and display ChatGPT subscription windows that aren't its own — served straight from the 60s limits cache, which the "re-checked at fetch time" guard does not cover. Treat a genuinely absent signal as non-eligible (same as api-key): no usage provider resolves and the footer omits limit windows. Present mechanisms are unchanged — oauth/auth-profile/token stay eligible, and only OpenAI is gated on the credential type so other providers are unaffected. A real oauth/profile turn always carries its mechanism; one arriving blank is an upstream tagging bug to fix at the source. Inverts the now-incorrect "absent => oauth-eligible" test into regression coverage for the absent/api-key case. Co-Authored-By: Claude Opus 4.8 --- src/infra/provider-usage.limits.test.ts | 14 +++++++------- src/infra/provider-usage.limits.ts | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/infra/provider-usage.limits.test.ts b/src/infra/provider-usage.limits.test.ts index 957fdd174832..ed3c7ea2ecfd 100644 --- a/src/infra/provider-usage.limits.test.ts +++ b/src/infra/provider-usage.limits.test.ts @@ -33,13 +33,13 @@ describe("getProviderUsageLimits credential awareness", () => { expect(loadMock).toHaveBeenCalledTimes(1); }); - it("resolves OpenAI limits when the credential type is absent (oauth-eligible)", async () => { - loadMock.mockResolvedValue({ - updatedAt: 0, - providers: [{ provider: "openai", displayName: "OpenAI", windows: [] }], - }); - await getProviderUsageLimits("openai"); - expect(loadMock).toHaveBeenCalledTimes(1); + it("returns undefined for an OpenAI turn with an absent credential type and never fetches", async () => { + // A missing authMode is not evidence of an oauth turn, so we must NOT resolve + // (and cache-borrow) ChatGPT windows for it — same outcome as an explicit + // api-key turn. A real oauth/profile turn carries its mechanism explicitly. + const out = await getProviderUsageLimits("openai"); + expect(out).toBeUndefined(); + expect(loadMock).not.toHaveBeenCalled(); }); it("resolves OpenAI limits for an oauth turn", async () => { diff --git a/src/infra/provider-usage.limits.ts b/src/infra/provider-usage.limits.ts index 8c174bd63cb8..04e8d49586a5 100644 --- a/src/infra/provider-usage.limits.ts +++ b/src/infra/provider-usage.limits.ts @@ -21,10 +21,19 @@ const limitsCache = new Map(); // Map a per-turn auth signal — which may be the auth *mechanism* (e.g. // "auth-profile"), not a credential *type* — to the usage-credential vocabulary // resolveUsageProviderId expects. api-key/aws-sdk resolve NO usage provider, so an -// api-key turn never resolves "openai" and cannot borrow cached oauth windows; -// oauth/token/auth-profile and a missing signal are usage-eligible, and the actual -// credential is re-checked at fetch time before any usage request is made. +// api-key turn never resolves "openai" and cannot borrow cached oauth windows. +// A genuinely *absent* signal is treated the same way (non-eligible): a missing +// authMode is not evidence of an oauth/subscription turn, and resolving +// optimistically would let an untagged OpenAI api-key turn borrow cached +// oauth/ChatGPT windows (only OpenAI is gated on oauth/token here; other +// providers ignore the credential type, so they are unaffected). A real +// oauth/profile turn always carries its mechanism ("oauth"/"auth-profile"/ +// "token"); one arriving blank is an upstream tagging bug to fix at the source, +// not a reason to show another turn's windows. function toUsageCredentialType(raw: string | null | undefined): string { + if (raw == null) { + return "api-key"; + } if (raw === "api-key" || raw === "aws-sdk") { return raw; }