From 99db98a7ce4ce7a12ef892bc6088cb2457c1913b Mon Sep 17 00:00:00 2001 From: Peter Lindsey Date: Wed, 3 Jun 2026 21:47:02 +0800 Subject: [PATCH] fix(usage): map auth mechanism to usage-credential type for provider limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit requestShaping.authMode is the auth *mechanism* (e.g. "auth-profile" for a configured auth profile), not the credential *type* resolveUsageProviderId expects. Gating limits on it === "oauth"/"token" dropped 📊 for legit OAuth (profile-based) turns. Map it: api-key/aws-sdk -> no usage provider (cannot borrow cached oauth windows); oauth/token/auth-profile/absent -> usage-eligible, with the real credential re-checked at fetch time. Co-Authored-By: Claude Opus 4.8 --- src/infra/provider-usage.limits.test.ts | 20 ++++++++++++++++---- src/infra/provider-usage.limits.ts | 25 +++++++++++++++---------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/infra/provider-usage.limits.test.ts b/src/infra/provider-usage.limits.test.ts index af48eee9596..957fdd17483 100644 --- a/src/infra/provider-usage.limits.test.ts +++ b/src/infra/provider-usage.limits.test.ts @@ -24,10 +24,22 @@ describe("getProviderUsageLimits credential awareness", () => { expect(loadMock).not.toHaveBeenCalled(); }); - it("returns undefined for OpenAI with no credential type (no implicit oauth default)", async () => { - const out = await getProviderUsageLimits("openai"); - expect(out).toBeUndefined(); - expect(loadMock).not.toHaveBeenCalled(); + it("resolves OpenAI limits for an auth-profile turn (mechanism, not credential type)", async () => { + loadMock.mockResolvedValue({ + updatedAt: 0, + providers: [{ provider: "openai", displayName: "OpenAI", windows: [] }], + }); + await getProviderUsageLimits("openai", { credentialType: "auth-profile" }); + 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("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 2881ac194e5..8c174bd63cb 100644 --- a/src/infra/provider-usage.limits.ts +++ b/src/infra/provider-usage.limits.ts @@ -18,6 +18,19 @@ type LimitsCacheEntry = { }; 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. +function toUsageCredentialType(raw: string | null | undefined): string { + if (raw === "api-key" || raw === "aws-sdk") { + return raw; + } + return raw === "token" ? "token" : "oauth"; +} + // Resolve the active provider to a usage-capable id and load its windows. Returns // undefined when the provider has no core-known usage (e.g. api-key-only or an // unmapped provider). Cached per usage-provider for 60s so a per-reply snapshot @@ -26,12 +39,8 @@ export async function getProviderUsageLimits( provider: string | undefined | null, options?: { credentialType?: string | null; timeoutMs?: number; now?: number }, ): Promise { - // Pass the turn's real credential type through unchanged. Do NOT default to - // "oauth": resolveUsageProviderId only returns the OpenAI usage provider for - // oauth/token, so defaulting would attach OAuth/ChatGPT windows to an API-key - // turn. A missing credential type ⇒ no OpenAI limits (correct, not OAuth). const usageId = resolveUsageProviderId(provider, { - credentialType: options?.credentialType ?? null, + credentialType: toUsageCredentialType(options?.credentialType), }); if (!usageId) { return undefined; @@ -107,12 +116,8 @@ export function getProviderUsageLimitsCached( provider: string | undefined | null, options?: { credentialType?: string | null; timeoutMs?: number }, ): ReplyUsageLimits | undefined { - // Pass the turn's real credential type through unchanged. Do NOT default to - // "oauth": resolveUsageProviderId only returns the OpenAI usage provider for - // oauth/token, so defaulting would attach OAuth/ChatGPT windows to an API-key - // turn. A missing credential type ⇒ no OpenAI limits (correct, not OAuth). const usageId = resolveUsageProviderId(provider, { - credentialType: options?.credentialType ?? null, + credentialType: toUsageCredentialType(options?.credentialType), }); if (!usageId) { return undefined;