SPB Git

spb/llmindex Public

The discriminative, contamination-resistant, fully transparent LLM ranking — updated live.

TypeScript 77.9% TeX 15.2% Python 3.7% SQL 1.4% JavaScript 1.1% Shell 0.5%
2.2 KB · 66 lines typescript
Raw Blame History
1/**2 * llmindex.io — Redis fixed-window rate limiting for /api/v13 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * License: Proprietary — © Simon-Pierre Boucher, all rights reserved6 *7 * 60 req/min anonymous, 600 req/min with a valid API key (§8).8 * Fails open if Redis is unavailable (availability over strictness).9 */10import 'server-only';11import Redis from 'ioredis';12import { NextResponse, type NextRequest } from 'next/server';1314const ANON_LIMIT = 60;15const KEYED_LIMIT = 600;16const WINDOW_S = 60;1718let redis: Redis | null | undefined;1920function getRedis(): Redis | null {21  if (redis !== undefined) return redis;22  const url = process.env.REDIS_URL;23  if (!url) {24    redis = null;25    return redis;26  }27  redis = new Redis(url, { maxRetriesPerRequest: 1, lazyConnect: false, enableOfflineQueue: false });28  redis.on('error', () => {29    /* logged by ioredis once; keep the API failing open */30  });31  return redis;32}3334function validApiKey(req: NextRequest): boolean {35  const key = req.headers.get('x-api-key');36  if (!key) return false;37  const configured = (process.env.API_KEYS ?? '').split(',').map((k) => k.trim()).filter(Boolean);38  return configured.includes(key);39}4041/** Returns a 429 response when over limit, otherwise null. */42export async function rateLimit(req: NextRequest): Promise<NextResponse | null> {43  const client = getRedis();44  if (!client) return null;45  const keyed = validApiKey(req);46  const limit = keyed ? KEYED_LIMIT : ANON_LIMIT;47  const ip =48    req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? req.headers.get('x-real-ip') ?? 'unknown';49  const identity = keyed ? `key:${req.headers.get('x-api-key')}` : `ip:${ip}`;50  const windowKey = `rl:${identity}:${Math.floor(Date.now() / 1000 / WINDOW_S)}`;51  try {52    const count = await client.incr(windowKey);53    if (count === 1) await client.expire(windowKey, WINDOW_S + 5);54    if (count > limit) {55      const retryAfter = WINDOW_S - (Math.floor(Date.now() / 1000) % WINDOW_S);56      return NextResponse.json(57        { error: 'rate_limited', limit, window_seconds: WINDOW_S },58        { status: 429, headers: { 'Retry-After': String(retryAfter) } },59      );60    }61  } catch {62    return null; // fail open63  }64  return null;65}66