screenpipe/screenpipe · warning

hosted AI admission rejected

Error message

hosted AI admission rejected

What it means

Server-side log line emitted by the screenpipe AI gateway worker (`handleRequest`, the worker `fetch` handler) when a request is rejected by the per-minute rate limiter (`checkRateLimit`). It is not thrown at the caller — the caller receives the limiter's 429-style response — but the log records the admission decision with the auth tier and account plan. Paid and free models meter against independent buckets (free weight-0 models use a high freeRpm bucket, paid models a low rpm bucket).

Source

Thrown at packages/ai-gateway/src/index.ts:634

			);
			if (gate === 'downgrade') {
				console.log(`background request for disallowed model "${body.model}" (${authResult.tier}) -> downgraded to auto`);
				body.model = 'auto';
			} else if (gate === 'reject') {
				return modelNotAllowedResponse(authResult, body.model);
			}

			// Per-minute rate limit. Now that the model is resolved (a 'downgrade'
			// already rewrote it to free 'auto'), free weight-0 models meter
			// against the high `freeRpm` bucket — so "switch to a free model to
			// avoid rate limits" actually works. Paid models keep the low `rpm`.
			// The two buckets are independent; the daily cost cap below is the
			// real backstop against runaway free loops.
			const rateLimit = await checkRateLimit(request, env, authResult, {
				freeModel: isFreeModel(body.model),
			});
			if (!rateLimit.allowed && rateLimit.response) {
				console.warn('hosted AI admission rejected', {
					gate: 'per_minute',
					tier: authResult.tier,
					accountPlan: authResult.accountPlan,
				});
				return rateLimit.response;
			}

			const cloudflareGateway = isHostedChatGatewayEnabled(env);
			let legacyRescueFallback = false;
			// Legacy mode retains the paid weighted-query admission gate. In
			// Cloudflare mode the provider-cost spend rules are authoritative for
			// this endpoint; Free's separate two-message lease remains above.
			let usage: Awaited<ReturnType<typeof trackUsage>> | null = null;
			if (!cloudflareGateway) {
				const ipAddress = request.headers.get('cf-connecting-ip') || undefined;
				usage = await trackUsage(env, authResult.deviceId, usageTier, authResult.userId, ipAddress, body.model);
			}
			if (usage && !usage.allowed) {

View on GitHub (pinned to 4ebf712990)

Solutions

  1. Inspect the 429 response body/headers returned with the log — it carries reset/retry info; pause and retry after the window resets instead of immediately re-sending.
  2. Add exponential backoff with jitter to your client's retry logic, and serialize requests instead of firing them in parallel.
  3. If on the free tier, switch the request's `model` to a free weight-0 model (e.g. 'auto') — free models use the separate high freeRpm bucket.
  4. If you legitimately need higher RPM, upgrade the account plan/tier so checkRateLimit uses the paid bucket.

Example fix

// before: tight retry loop
for (;;) { await fetch(GATEWAY, { method: 'POST', body }); }
// after: respect backoff on 429
for (;;) {
  const res = await fetch(GATEWAY, { method: 'POST', body });
  if (res.status !== 429) break;
  const retryAfter = Number(res.headers.get('retry-after') ?? 30);
  await new Promise(r => setTimeout(r, retryAfter * 1000));
}
Defensive patterns

Strategy: retry

Validate before calling

// client-side: throttle before calling
let lastCall = 0;
async function throttledCall(fn, minIntervalMs = 3000) {
  const wait = lastCall + minIntervalMs - Date.now();
  if (wait > 0) await new Promise(r => setTimeout(r, wait));
  lastCall = Date.now();
  return fn();
}

Try / catch

const res = await fetch(GATEWAY, opts);
if (res.status === 429) {
  const retryAfter = Number(res.headers.get('retry-after') ?? 30);
  await sleep(retryAfter * 1000 * (1 + Math.random() * 0.5)); // jittered backoff
  return retry();
}

Prevention

When it happens

Trigger: A client authenticated as a particular tier (anonymous/free/paid) sends chat completion requests to the gateway faster than its tier's per-minute allowance: e.g. an interactive chat client hammering the endpoint, an automation loop polling, or Pi tool-loop calls without session affinity exceeding the free per-account two-message lease's RPM side.

Common situations: Runaway retry loops in user code that immediately retry on errors without backoff; shared egress IPs (NAT, VPN) aggregating many anonymous devices against one key; burst tool-calling agents issuing many parallel completions; switching to a paid model after exhausting the free bucket (independent buckets mean both can trip).

Related errors


AI-assisted analysis of screenpipe/screenpipe@4ebf712990 (2026-09-01). Data as JSON: /api/errors/e3fe11489299e7aa. Report an issue: GitHub.