AI Rate Limits: امنع مستخدم واحد يحرق فاتورة النموذج
مستوى القارئ: متوسط
هتكسب من المقال ده بوابة بسيطة تمنع طلبات الذكاء الاصطناعي من استنزاف الميزانية، بدل ما تكتشف المشكلة بعد ظهور الفاتورة.
المشكلة باختصار
الـ rate limit العام بتاع مزود النموذج مهم، لكنه مش كفاية لتطبيق إنتاجي. OpenAI مثلًا توضح أن الحدود قد تُقاس بعدة طرق مثل RPM وTPM وRPD وTPD، وغالبًا تكون على مستوى المؤسسة أو المشروع. ده يحمي المنصة، لكنه لا يضمن أن كل مستخدم عندك أخذ نصيبه العادل.
اللي بيحصل فعلاً: مستخدم واحد يفتح تبويب فيه retry loop، أو سكربت داخلي يرسل نفس البرومبت 3000 مرة، فتلاقي كل المستخدمين اتأثروا. الافتراض هنا إن عندك تطبيق SaaS فيه 200 مستخدم نشط يوميًا، وكل طلب AI متوسطه 1200 توكن إدخال و600 توكن إخراج.
مثال بسيط قبل التعريف العلمي
ركز في المثال ده. اعتبر إن عندك كارت شحن يومي لكل مستخدم. المستخدم المجاني معه 2 دولار في اليوم. المستخدم المدفوع معه 20 دولار. كل مرة يطلب تلخيص، بتخصم تكلفة تقديرية قبل ما تبعت الطلب للنموذج. لو الكارت خلص، التطبيق يرجع 429 مع وقت انتظار واضح.
علميًا، ده اسمه cost-aware rate limiting. بدل ما تعد الطلبات فقط، أنت بتعد “وحدات تكلفة”. ممكن الوحدة تكون طلب واحد، أو 1000 توكن، أو سنتات تقديرية. أفضل طريقة لتطبيقه بسرعة هي Redis counter بمفتاح لكل مستخدم ولكل يوم. Redis يدعم نمط العد باستخدام INCR، ومع EXPIRE تقدر تخلي المفتاح ينتهي تلقائيًا بعد نافذة زمنية.
الحل: حد يومي + حد دقيق لكل طلب
ابدأ بحدين، مش حد واحد. الأول حد يومي يمنع الكارثة. الثاني حد لكل طلب يمنع البرومبت الضخم من أكل الميزانية مرة واحدة. الـ trade-off هنا واضح: هتكسب تحكمًا في التكلفة واستقرارًا أعلى، مقابل احتمال أن مستخدمًا شرعيًا يحتاج زيادة حد مؤقتة.
import express from "express";
import Redis from "ioredis";
const app = express();
const redis = new Redis(process.env.REDIS_URL);
app.use(express.json({ limit: "256kb" }));
const DAILY_CENTS = {
free: 200,
pro: 2000,
};
function estimateCents({ inputTokens, outputTokens }) {
// عدّل الأسعار حسب الموديل الحقيقي عندك.
const inputCost = inputTokens * 0.0000005;
const outputCost = outputTokens * 0.0000015;
return Math.ceil((inputCost + outputCost) * 100);
}
async function aiCostGuard(req, res, next) {
const userId = req.user?.id ?? "anonymous";
const plan = req.user?.plan ?? "free";
const estimated = estimateCents({
inputTokens: Number(req.body.input_tokens ?? 1200),
outputTokens: Number(req.body.max_output_tokens ?? 600),
});
if (estimated > 80) {
return res.status(413).json({ error: "prompt_too_expensive" });
}
const day = new Date().toISOString().slice(0, 10);
const key = `ai:cost:${userId}:${day}`;
const used = await redis.incrby(key, estimated);
if (used === estimated) await redis.expire(key, 36 * 60 * 60);
const limit = DAILY_CENTS[plan] ?? DAILY_CENTS.free;
if (used > limit) {
return res.status(429).json({
error: "ai_daily_budget_exceeded",
retry_after: "tomorrow",
used_cents: used,
limit_cents: limit,
});
}
if (used > limit * 0.8) {
console.warn("AI budget above 80%", { userId, used, limit });
}
next();
}
app.post("/api/ai/summarize", aiCostGuard, async (req, res) => {
// هنا فقط تستدعي مزود النموذج.
res.json({ ok: true });
});