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; }