fix(usage): map auth mechanism to usage-credential type for provider limits

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 <noreply@anthropic.com>
This commit is contained in:
Peter Lindsey
2026-06-03 21:47:02 +08:00
committed by Ayaan Zaidi
parent 0dbfa1f6be
commit 99db98a7ce
2 changed files with 31 additions and 14 deletions

View File

@@ -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 () => {

View File

@@ -18,6 +18,19 @@ type LimitsCacheEntry = {
};
const limitsCache = new Map<string, LimitsCacheEntry>();
// 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<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;
@@ -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;