Add reasoning-based model and effort routing (#7607)

* feat(routing): add reasoning-based model and effort routing

* refactor(routing): modularize reasoning and auto-routing pipeline

* fix(routing): remove redundant DB re-export and prevent SQL scan false positives

* fix(routing): resolve reasoning routing review blockers

* fix(i18n): keep release ranking fallbacks outside reasoning

* fix(db): renumber reasoning-routing migration past release tip (124→125)

124_generic_session_affinity_ttl.sql (#7274) has since landed on
release/v3.8.49 at version 124, colliding with this PR's own
124_reasoning_routing_rules.sql. Renumbers to 125 (the next free slot
past the current release tip) and updates the one filename reference
in docs/routing/REASONING_ROUTING.md.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(db): renumber reasoning-routing migration 125→126 (slot taken by #7360)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(api): compact temp-path decls in exportAll GET (complexity-ratchet lines budget)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* refactor(api): single-statement auth guard in exportAll GET (function under 80-line cap)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Jan Leon
2026-07-19 03:09:47 +02:00
committed by GitHub
parent 9fce7d0fbf
commit 74c006e245
92 changed files with 7887 additions and 871 deletions

View File

@@ -253,6 +253,122 @@ async function isComboAllowedForKey(
return { allowed, comboName };
}
function quotaPolicyResponse(message: string, code: string): Response {
const body = buildErrorBody(HTTP_STATUS.FORBIDDEN, message);
body.error.code = code;
return new Response(JSON.stringify(body), {
status: HTTP_STATUS.FORBIDDEN,
headers: { "Content-Type": "application/json" },
});
}
async function validateQuotaRoutingTarget(
modelStr: string,
allowedQuotas: string[]
): Promise<Response | null> {
if (isQuotaModelName(modelStr) && allowedQuotas.length === 0) {
return quotaPolicyResponse(
`Model "${modelStr}" requires a quota-pool allocation; this API key is not allocated to any quota pool`,
"QUOTA_NOT_ALLOCATED"
);
}
if (allowedQuotas.length === 0) return null;
try {
const scope = await resolveQuotaKeyScope(allowedQuotas);
const parsed = isQuotaModelName(modelStr) ? parseQuotaModelName(modelStr) : null;
const allowed =
parsed !== null &&
scope.poolSlugs.includes(parsed.groupSlug) &&
scope.providers.includes(parsed.provider);
if (allowed) return null;
return quotaPolicyResponse(
isQuotaModelName(modelStr)
? `Model "${modelStr}" is not in this key's quota pools`
: "This quota-exclusive API key may only use quotaShared-* models",
"QUOTA_ONLY"
);
} catch (error) {
log.error("API_POLICY", "Routing target quota check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "API key quota policy unavailable");
}
}
async function validateStandardRoutingTarget(
request: Request,
apiKey: string,
apiKeyInfo: ApiKeyMetadata,
modelStr: string
): Promise<Response | null> {
let requestedComboName: string | null = null;
if (apiKeyInfo.allowedCombos && apiKeyInfo.allowedCombos.length > 0) {
try {
const comboAccess = await isComboAllowedForKey(apiKeyInfo.allowedCombos, modelStr);
requestedComboName = comboAccess.comboName;
if (!comboAccess.allowed) {
return errorResponse(
HTTP_STATUS.FORBIDDEN,
`Combo "${comboAccess.comboName || modelStr}" is not allowed for this API key`
);
}
} catch (error) {
log.error("API_POLICY", "Routing target combo check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "API key combo policy unavailable");
}
}
const hasModelRestrictions =
(apiKeyInfo.allowedModels && apiKeyInfo.allowedModels.length > 0) ||
apiKeyInfo.disableNonPublicModels === true;
if (!requestedComboName && hasModelRestrictions && modelStr.startsWith("auto/")) {
requestedComboName = modelStr;
}
if (!requestedComboName && hasModelRestrictions) {
try {
requestedComboName = await resolveRequestedComboName(modelStr);
} catch {
requestedComboName = null;
}
}
if (
!requestedComboName &&
hasModelRestrictions &&
!(await isModelAllowedForKey(apiKey, modelStr))
) {
return policyErrorResponse(
request,
HTTP_STATUS.FORBIDDEN,
`Model "${modelStr}" is not allowed for this API key`,
`Model "${modelStr}" is not enabled or quota is insufficient. Choose another allowed model.`,
"invalid_request_error",
HTTP_STATUS.BAD_REQUEST
);
}
return null;
}
/**
* Validate only the model/combo authorization of a routing target.
*
* The full request policy has already run before routing. Calling it again for a
* policy-generated target would charge request limits twice and apply throttling
* twice. This narrower check proves that routing did not widen the key's access
* without consuming any budget, token-limit, or rate-limit state.
*/
export async function validateApiKeyRoutingTarget(
request: Request,
apiKey: string | null,
apiKeyInfo: ApiKeyMetadata | null,
modelStr: string | null
): Promise<Response | null> {
if (!apiKey || !apiKeyInfo || !modelStr) return null;
const allowedQuotas = Array.isArray(apiKeyInfo.allowedQuotas) ? apiKeyInfo.allowedQuotas : [];
const quotaRejection = await validateQuotaRoutingTarget(modelStr, allowedQuotas);
if (quotaRejection || allowedQuotas.length > 0) return quotaRejection;
return validateStandardRoutingTarget(request, apiKey, apiKeyInfo, modelStr);
}
export interface ApiKeyPolicyResult {
/** API key string (null if no key provided) */
apiKey: string | null;
@@ -306,6 +422,222 @@ export async function resolvePlaygroundTestKey(request: Request): Promise<string
}
}
type PolicyContext = {
request: Request;
apiKey: string;
apiKeyInfo: ApiKeyMetadata;
modelStr: string | null;
};
function validateKeyStatus(context: PolicyContext): Response | null {
const { apiKeyInfo } = context;
if (apiKeyInfo.isActive === false) {
return errorResponse(HTTP_STATUS.FORBIDDEN, "This API key is disabled");
}
if (apiKeyInfo.isBanned === true) {
return errorResponse(HTTP_STATUS.FORBIDDEN, "This API key is banned due to policy violations");
}
if (apiKeyInfo.expiresAt && Date.now() > new Date(apiKeyInfo.expiresAt).getTime()) {
return errorResponse(HTTP_STATUS.FORBIDDEN, "This API key has expired");
}
return null;
}
async function validateKeyScheduleAndUsage(context: PolicyContext): Promise<Response | null> {
const { request, apiKey, apiKeyInfo } = context;
if (apiKeyInfo.accessSchedule?.enabled && !isWithinSchedule(apiKeyInfo.accessSchedule)) {
const { from, until, tz } = apiKeyInfo.accessSchedule;
return errorResponse(
HTTP_STATUS.FORBIDDEN,
`Access denied outside allowed hours (${from}${until} ${tz})`
);
}
if (apiKeyInfo.usageLimitEnabled !== true) return null;
try {
const rejection = await buildApiKeyUsageLimitPolicyRejection(request, {
id: apiKeyInfo.id,
usageLimitEnabled: apiKeyInfo.usageLimitEnabled,
dailyUsageLimitUsd: apiKeyInfo.dailyUsageLimitUsd,
weeklyUsageLimitUsd: apiKeyInfo.weeklyUsageLimitUsd,
});
return rejection;
} catch (error) {
log.error("API_POLICY", "API key USD usage limit check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "API key usage limit unavailable");
}
}
function validateEndpointAccess(context: PolicyContext): Response | null {
const { request, apiKeyInfo } = context;
if (!apiKeyInfo.allowedEndpoints?.length) return null;
try {
const category = resolveEndpointCategory(new URL(request.url).pathname);
if (category && !apiKeyInfo.allowedEndpoints.includes(category)) {
return errorResponse(
HTTP_STATUS.FORBIDDEN,
`Endpoint category "${category}" is not allowed for this API key`
);
}
} catch {
// URL parse failure — fail open, let other checks decide.
}
return null;
}
async function validateQuotaAccess(context: PolicyContext): Promise<Response | null> {
const { apiKey, apiKeyInfo, modelStr } = context;
if (!modelStr) return null;
const allowedQuotas = Array.isArray(apiKeyInfo.allowedQuotas) ? apiKeyInfo.allowedQuotas : [];
if (isQuotaModelName(modelStr) && allowedQuotas.length === 0) {
return quotaPolicyResponse(
`Model "${modelStr}" requires a quota-pool allocation; this API key is not allocated to any quota pool`,
"QUOTA_NOT_ALLOCATED"
);
}
if (!allowedQuotas.length) return null;
try {
const scope = await resolveQuotaKeyScope(allowedQuotas);
const parsed = isQuotaModelName(modelStr) ? parseQuotaModelName(modelStr) : null;
const allowed =
parsed !== null &&
scope.poolSlugs.length > 0 &&
scope.poolSlugs.includes(parsed.groupSlug) &&
scope.providers.includes(parsed.provider);
if (allowed) return null;
const message = isQuotaModelName(modelStr)
? `Model "${modelStr}" is not in this key's quota pools`
: "This quota-exclusive API key may only use quotaShared-* models";
return quotaPolicyResponse(message, "QUOTA_ONLY");
} catch (error) {
log.error("API_POLICY", "Quota scope check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "API key quota policy unavailable");
}
}
async function validateModelAccess(context: PolicyContext): Promise<Response | null> {
const { request, apiKey, apiKeyInfo, modelStr } = context;
if (!modelStr || apiKeyInfo.allowedQuotas?.length) return null;
const comboAccess = await validateComboAccess(apiKeyInfo.allowedCombos, modelStr);
if (comboAccess.rejection) return comboAccess.rejection;
let requestedComboName = comboAccess.comboName;
const hasModelRestrictions =
Boolean(apiKeyInfo.allowedModels?.length) || apiKeyInfo.disableNonPublicModels === true;
if (!requestedComboName && hasModelRestrictions) {
if (modelStr.startsWith("auto/") || modelStr.startsWith("qtSd/")) {
requestedComboName = modelStr;
} else {
try {
requestedComboName = await resolveRequestedComboName(modelStr);
} catch {
requestedComboName = null;
}
}
}
if (requestedComboName || !hasModelRestrictions) return null;
if (await isModelAllowedForKey(apiKey, modelStr)) return null;
return policyErrorResponse(
request,
HTTP_STATUS.FORBIDDEN,
`Model "${modelStr}" is not allowed for this API key`,
`Model "${modelStr}" is not enabled or quota is insufficient. Choose another allowed model.`,
"invalid_request_error",
HTTP_STATUS.BAD_REQUEST
);
}
async function validateComboAccess(
allowedCombos: string[] | undefined,
modelStr: string
): Promise<{ comboName: string | null; rejection: Response | null }> {
if (!allowedCombos?.length) return { comboName: null, rejection: null };
try {
const comboAccess = await isComboAllowedForKey(allowedCombos, modelStr);
if (comboAccess.allowed) return { comboName: comboAccess.comboName, rejection: null };
return {
comboName: comboAccess.comboName,
rejection: errorResponse(
HTTP_STATUS.FORBIDDEN,
`Combo "${comboAccess.comboName || modelStr}" is not allowed for this API key`
),
};
} catch (error) {
log.error("API_POLICY", "Combo access check failed. Request blocked.", { error });
return {
comboName: null,
rejection: errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "API key combo policy unavailable"),
};
}
}
function validateBudget(context: PolicyContext): Response | null {
const { apiKeyInfo } = context;
if (!apiKeyInfo.id) return null;
try {
const budgetOk = checkBudget(apiKeyInfo.id);
return budgetOk.allowed
? null
: errorResponse(HTTP_STATUS.RATE_LIMITED, budgetOk.reason || "Budget limit exceeded");
} catch (error) {
log.error("API_POLICY", "Budget check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "Budget policy unavailable");
}
}
function validateTokenLimit(context: PolicyContext): Response | null {
const { apiKeyInfo, modelStr } = context;
if (!apiKeyInfo.id) return null;
try {
const breach = checkTokenLimits(apiKeyInfo.id, undefined, modelStr ?? undefined);
if (!breach) return null;
const scopeLabel =
breach.scopeType === "global" ? "account" : `${breach.scopeType} "${breach.scopeValue}"`;
return errorResponse(
HTTP_STATUS.RATE_LIMITED,
`Token limit exceeded for ${scopeLabel}: ${breach.tokensUsed}/${breach.limitValue} tokens used in the current window. Please try again later.`
);
} catch (error) {
log.error("API_POLICY", "Token limit check failed. Request blocked.", { error });
return errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "Token limit policy unavailable");
}
}
function buildRateLimitRules(apiKeyInfo: ApiKeyMetadata): RateLimitRule[] {
const custom = apiKeyInfo.rateLimits?.length;
const rules = custom
? [...(apiKeyInfo.rateLimits as RateLimitRule[])]
: [...DEFAULT_RATE_LIMITS, ...ENV_DEFAULT_RATE_LIMITS];
if (!custom) {
if (apiKeyInfo.maxRequestsPerDay)
rules.push({ limit: apiKeyInfo.maxRequestsPerDay, window: 86400 });
if (apiKeyInfo.maxRequestsPerMinute)
rules.push({ limit: apiKeyInfo.maxRequestsPerMinute, window: 60 });
}
return rules;
}
async function validateRateLimitAndThrottle(context: PolicyContext): Promise<Response | null> {
const { apiKeyInfo } = context;
if (!apiKeyInfo.id) return null;
const rules = buildRateLimitRules(apiKeyInfo);
if (rules.length) {
const result = await checkRateLimit(apiKeyInfo.id, rules);
if (!result.allowed) {
const window = result.failedWindow ? ` (${result.failedWindow}s window)` : "";
return errorResponse(
HTTP_STATUS.RATE_LIMITED,
`Request limit exceeded${window}. Please try again later.`
);
}
}
if (apiKeyInfo.throttleDelayMs && apiKeyInfo.throttleDelayMs > 0) {
await delay(Math.min(apiKeyInfo.throttleDelayMs, 300_000));
}
return null;
}
export async function enforceApiKeyPolicy(
request: Request,
modelStr: string | null
@@ -338,343 +670,25 @@ export async function enforceApiKeyPolicy(
return { apiKey, apiKeyInfo: null, rejection: null };
}
// ── Check 1: is_active / is_banned ──
if (apiKeyInfo.isActive === false) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(HTTP_STATUS.FORBIDDEN, "This API key is disabled"),
};
}
if (apiKeyInfo.isBanned === true) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.FORBIDDEN,
"This API key is banned due to policy violations"
),
};
}
const context = { request, apiKey, apiKeyInfo, modelStr };
const statusRejection = validateKeyStatus(context);
if (statusRejection) return { apiKey, apiKeyInfo, rejection: statusRejection };
const scheduleRejection = await validateKeyScheduleAndUsage(context);
if (scheduleRejection) return { apiKey, apiKeyInfo, rejection: scheduleRejection };
const endpointRejection = validateEndpointAccess(context);
if (endpointRejection) return { apiKey, apiKeyInfo, rejection: endpointRejection };
// ── Check 1.5: expires_at ──
if (apiKeyInfo.expiresAt) {
const expiry = new Date(apiKeyInfo.expiresAt).getTime();
if (Date.now() > expiry) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(HTTP_STATUS.FORBIDDEN, "This API key has expired"),
};
}
}
const quotaRejection = await validateQuotaAccess(context);
if (quotaRejection) return { apiKey, apiKeyInfo, rejection: quotaRejection };
const modelRejection = await validateModelAccess(context);
if (modelRejection) return { apiKey, apiKeyInfo, rejection: modelRejection };
// ── Check 2: access_schedule — time-based access window ──
if (apiKeyInfo.accessSchedule && apiKeyInfo.accessSchedule.enabled) {
if (!isWithinSchedule(apiKeyInfo.accessSchedule)) {
const { from, until, tz } = apiKeyInfo.accessSchedule;
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.FORBIDDEN,
`Access denied outside allowed hours (${from}${until} ${tz})`
),
};
}
}
// ── Check 2.1: per-key USD fair usage cap ──
if (apiKeyInfo.usageLimitEnabled === true) {
try {
const usageLimitRejection = await buildApiKeyUsageLimitPolicyRejection(request, {
id: apiKeyInfo.id,
usageLimitEnabled: apiKeyInfo.usageLimitEnabled,
dailyUsageLimitUsd: apiKeyInfo.dailyUsageLimitUsd,
weeklyUsageLimitUsd: apiKeyInfo.weeklyUsageLimitUsd,
});
if (usageLimitRejection) {
return { apiKey, apiKeyInfo, rejection: usageLimitRejection };
}
} catch (error) {
log.error("API_POLICY", "API key USD usage limit check failed. Request blocked.", { error });
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.SERVICE_UNAVAILABLE,
"API key usage limit unavailable"
),
};
}
}
// ── Check 2.5: Endpoint restriction ──
if (apiKeyInfo.allowedEndpoints && apiKeyInfo.allowedEndpoints.length > 0) {
try {
const url = new URL(request.url);
const category = resolveEndpointCategory(url.pathname);
if (category && !apiKeyInfo.allowedEndpoints.includes(category)) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.FORBIDDEN,
`Endpoint category "${category}" is not allowed for this API key`
),
};
}
} catch {
// URL parse failure — fail open, let other checks decide
}
}
// ── Check 2.9: qtSd models require a quota-pool allocation ──
//
// quotaShared-* (qtSd/<group>/<provider>/<model>) virtual models are pool-gated:
// a key that is NOT allocated to any quota pool (empty allowedQuotas) must not be
// able to call them — otherwise an ordinary key could route through someone
// else's shared quota. Only allocated keys (allowedQuotas non-empty, further
// validated against their pool scope in Check 3 below) may use qtSd models.
if (
modelStr &&
isQuotaModelName(modelStr) &&
!(Array.isArray(apiKeyInfo.allowedQuotas) && apiKeyInfo.allowedQuotas.length > 0)
) {
const notAllocatedBody = buildErrorBody(
HTTP_STATUS.FORBIDDEN,
`Model "${modelStr}" requires a quota-pool allocation; this API key is not allocated to any quota pool`
);
notAllocatedBody.error.code = "QUOTA_NOT_ALLOCATED";
return {
apiKey,
apiKeyInfo,
rejection: new Response(JSON.stringify(notAllocatedBody), {
status: HTTP_STATUS.FORBIDDEN,
headers: { "Content-Type": "application/json" },
}),
};
}
// ── Check 3: Quota-exclusive enforcement (Phase B4) ──
//
// When a key has allowedQuotas its access is governed exclusively by the
// quotaShared-* virtual models of its pools — raw model names are rejected,
// and quotaShared-* names belonging to OTHER pools are also rejected.
// Normal allowedModels/allowedCombos checks are skipped for these keys.
if (modelStr && apiKeyInfo.allowedQuotas && apiKeyInfo.allowedQuotas.length > 0) {
try {
const scope = await resolveQuotaKeyScope(apiKeyInfo.allowedQuotas);
let quotaRejectionMsg: string | null = null;
if (isQuotaModelName(modelStr)) {
// Virtual quota model — must belong to one of this key's pools AND its provider must be in scope.
const parsed = parseQuotaModelName(modelStr);
const allowed =
parsed !== null &&
scope.poolSlugs.length > 0 &&
scope.poolSlugs.includes(parsed.groupSlug) &&
scope.providers.includes(parsed.provider);
if (!allowed) {
quotaRejectionMsg = `Model "${modelStr}" is not in this key's quota pools`;
}
} else {
// Raw (non-quotaShared) model name — always rejected for quota-exclusive keys.
quotaRejectionMsg = `This quota-exclusive API key may only use quotaShared-* models`;
}
if (quotaRejectionMsg !== null) {
const quotaBody = buildErrorBody(HTTP_STATUS.FORBIDDEN, quotaRejectionMsg);
quotaBody.error.code = "QUOTA_ONLY";
return {
apiKey,
apiKeyInfo,
rejection: new Response(JSON.stringify(quotaBody), {
status: HTTP_STATUS.FORBIDDEN,
headers: { "Content-Type": "application/json" },
}),
};
}
// Model is an in-scope quotaShared-* name — skip allowedModels/allowedCombos.
// Continue to budget / rate-limit checks below.
} catch (error) {
log.error("API_POLICY", "Quota scope check failed. Request blocked.", { error });
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.SERVICE_UNAVAILABLE,
"API key quota policy unavailable"
),
};
}
}
// ── Check 4: Model restriction (skipped when allowedQuotas governs access) ──
let requestedComboName: string | null = null;
const isQuotaExclusive =
Boolean(apiKeyInfo.allowedQuotas) && (apiKeyInfo.allowedQuotas as string[]).length > 0;
if (
!isQuotaExclusive &&
modelStr &&
apiKeyInfo.allowedCombos &&
apiKeyInfo.allowedCombos.length > 0
) {
try {
const comboAccess = await isComboAllowedForKey(apiKeyInfo.allowedCombos, modelStr);
requestedComboName = comboAccess.comboName;
if (!comboAccess.allowed) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.FORBIDDEN,
`Combo "${comboAccess.comboName || modelStr}" is not allowed for this API key`
),
};
}
} catch (error) {
log.error("API_POLICY", "Combo access check failed. Request blocked.", { error });
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.SERVICE_UNAVAILABLE,
"API key combo policy unavailable"
),
};
}
}
const hasModelRestrictions =
!isQuotaExclusive &&
((apiKeyInfo.allowedModels && apiKeyInfo.allowedModels.length > 0) ||
(apiKeyInfo as { disableNonPublicModels?: boolean }).disableNonPublicModels === true);
if (!requestedComboName && modelStr && hasModelRestrictions) {
// Short-circuit: auto/* and qtSd/* are combo-routed (not catalog models).
// They must never be evaluated by the published-model gate.
if (modelStr.startsWith("auto/") || modelStr.startsWith("qtSd/")) {
requestedComboName = modelStr; // non-null sentinel — skips the published-model check
} else {
try {
requestedComboName = await resolveRequestedComboName(modelStr);
} catch {
requestedComboName = null;
}
}
}
if (modelStr && !requestedComboName && hasModelRestrictions) {
const allowed = await isModelAllowedForKey(apiKey, modelStr);
if (!allowed) {
return {
apiKey,
apiKeyInfo,
rejection: policyErrorResponse(
request,
HTTP_STATUS.FORBIDDEN,
`Model "${modelStr}" is not allowed for this API key`,
`Model "${modelStr}" is not enabled or quota is insufficient. Choose another allowed model.`,
"invalid_request_error",
HTTP_STATUS.BAD_REQUEST
),
};
}
}
// ── Check 4: Budget limit ──
if (apiKeyInfo.id) {
try {
const budgetOk = checkBudget(apiKeyInfo.id);
if (!budgetOk.allowed) {
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.RATE_LIMITED,
budgetOk.reason || "Budget limit exceeded"
),
};
}
} catch (error) {
// Fail-closed: budget backend error should block request
log.error("API_POLICY", "Budget check failed. Request blocked.", { error });
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "Budget policy unavailable"),
};
}
}
// ── Check 4.5: Per-model / per-provider token limits (Tier 1) ──
if (apiKeyInfo.id) {
try {
const breach = checkTokenLimits(apiKeyInfo.id, undefined, modelStr ?? undefined);
if (breach) {
const scopeLabel =
breach.scopeType === "global" ? "account" : `${breach.scopeType} "${breach.scopeValue}"`;
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.RATE_LIMITED,
`Token limit exceeded for ${scopeLabel}: ${breach.tokensUsed}/${breach.limitValue} tokens used in the current window. Please try again later.`
),
};
}
} catch (error) {
// Fail-closed: token-limit backend error should block the request,
// consistent with the budget check above.
log.error("API_POLICY", "Token limit check failed. Request blocked.", { error });
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, "Token limit policy unavailable"),
};
}
}
// ── Check 5: Generic Multi-Window Rate Limits ──
if (apiKeyInfo.id) {
const hasCustomRateLimits = Boolean(apiKeyInfo.rateLimits && apiKeyInfo.rateLimits.length > 0);
const rulesToApply = hasCustomRateLimits
? [...(apiKeyInfo.rateLimits as RateLimitRule[])]
: [...DEFAULT_RATE_LIMITS, ...ENV_DEFAULT_RATE_LIMITS];
// Combine with legacy limits if they exist and custom rate limits aren't set
if (!hasCustomRateLimits) {
if (apiKeyInfo.maxRequestsPerDay) {
rulesToApply.push({ limit: apiKeyInfo.maxRequestsPerDay, window: 86400 });
}
if (apiKeyInfo.maxRequestsPerMinute) {
rulesToApply.push({ limit: apiKeyInfo.maxRequestsPerMinute, window: 60 });
}
}
if (rulesToApply.length > 0) {
const rateLimitResult = await checkRateLimit(apiKeyInfo.id, rulesToApply);
if (!rateLimitResult.allowed) {
const failedWindowStr = rateLimitResult.failedWindow
? ` (${rateLimitResult.failedWindow}s window)`
: "";
return {
apiKey,
apiKeyInfo,
rejection: errorResponse(
HTTP_STATUS.RATE_LIMITED,
`Request limit exceeded${failedWindowStr}. Please try again later.`
),
};
}
}
}
// ── Check 6: Soft throttle / slowdown ──
if (apiKeyInfo.throttleDelayMs && apiKeyInfo.throttleDelayMs > 0) {
await delay(Math.min(apiKeyInfo.throttleDelayMs, 300_000));
}
const budgetRejection = validateBudget(context);
if (budgetRejection) return { apiKey, apiKeyInfo, rejection: budgetRejection };
const tokenRejection = validateTokenLimit(context);
if (tokenRejection) return { apiKey, apiKeyInfo, rejection: tokenRejection };
const rateRejection = await validateRateLimitAndThrottle(context);
if (rateRejection) return { apiKey, apiKeyInfo, rejection: rateRejection };
return { apiKey, apiKeyInfo, rejection: null };
}