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;