Thiết kế hệ thống giới hạn token cho AI Coding Agent kiểu Codex
OpenAI Codex vừa chuyển sang token-based pricing từ tháng 4/2026. Bài viết này phân tích cách thiết kế hệ thống rate limiting và token quota tương tự — từ data model, đến sliding window counter, đến fair-use enforcement.
Podcast · 3 giọng đọc
Trang
Host
Sơn
Curious Coder
Linh
Chuyên gia
Trang · Host · đoạn 1 / 32
Timeline · 32 lượt nói
Thiết kế hệ thống giới hạn token cho AI Coding Agent kiểu Codex
Từ ngày 2/4/2026, OpenAI chính thức chuyển Codex sang token-based pricing thay vì per-message như trước. Thay đổi này không chỉ ảnh hưởng đến người dùng — nó tiết lộ một bài toán thiết kế hệ thống cực kỳ thú vị mà bất kỳ ai đang xây dựng AI platform cũng sẽ phải đối mặt: làm thế nào để giới hạn và tính phí token usage một cách chính xác, công bằng và scale được?
Bài viết này không phải hướng dẫn dùng Codex. Đây là bài phân tích hệ thống: nếu bạn đang xây một AI SaaS tương tự — một coding agent, một chatbot có plan, một platform multi-tenant — bạn sẽ thiết kế token limit system như thế nào?
Bài toán cụ thể
Codex có một số đặc điểm thú vị từ góc độ hệ thống:
- Multi-tier plans: Free, Plus, Pro, Business, Enterprise — mỗi tier có quota khác nhau.
- Multi-model: GPT-5.3-Codex, GPT-5.4-Mini, GPT-5.3-Codex-Spark — mỗi model có credit rate per token khác nhau.
- Token type khác nhau: Input tokens, cached input tokens, và output tokens có giá khác nhau.
- Reset định kỳ: Quota reset theo tuần hoặc tháng tùy plan.
- Credits overage: Khi hết quota, có thể dùng credits đã mua thêm để tiếp tục.
- Fast mode: Tốn token nhanh hơn nhưng latency thấp hơn.
Đây là một bài toán rate limiting + billing kết hợp — phức tạp hơn nhiều so với rate limiting thông thường (kiểu "max 100 requests/phút").
Lớp 1: Data Model
Trước khi nghĩ đến algorithm, hãy thiết kế data model cho bài toán này.
-- Plan definitions
CREATE TABLE plans (
id VARCHAR PRIMARY KEY, -- 'plus', 'pro', 'business'
name VARCHAR NOT NULL,
reset_period VARCHAR NOT NULL, -- 'weekly', 'monthly'
base_quota_credits BIGINT NOT NULL -- credits included per period
);
-- Model rate card
CREATE TABLE model_rates (
model_id VARCHAR NOT NULL, -- 'gpt-5.3-codex', 'gpt-5.4-mini'
token_type VARCHAR NOT NULL, -- 'input', 'cached_input', 'output'
credits_per_million_tokens NUMERIC NOT NULL,
PRIMARY KEY (model_id, token_type)
);
-- User subscriptions
CREATE TABLE subscriptions (
user_id UUID NOT NULL,
plan_id VARCHAR REFERENCES plans(id),
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
quota_used_credits BIGINT DEFAULT 0,
purchased_credits BIGINT DEFAULT 0, -- credits mua thêm
PRIMARY KEY (user_id, period_start)
);
-- Usage log (append-only)
CREATE TABLE token_usage_log (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL,
session_id UUID NOT NULL,
model_id VARCHAR NOT NULL,
input_tokens INTEGER NOT NULL,
cached_input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER NOT NULL,
credits_consumed NUMERIC NOT NULL,
fast_mode BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ON token_usage_log (user_id, created_at DESC);
Tại sao append-only log? Vì billing data phải auditable — bạn không được xóa hay update. Mọi thứ đều là event, quota_used_credits được tính từ log hoặc cập nhật qua atomic increment.
Lớp 2: Token → Credits Conversion
Codex tính giá theo credits per million tokens, và mỗi token type có rate khác nhau. Đây là business logic quan trọng nhất:
interface TokenUsage {
inputTokens: number;
cachedInputTokens: number;
outputTokens: number;
modelId: string;
fastMode: boolean;
}
interface ModelRate {
inputCreditsPerMillion: number;
cachedInputCreditsPerMillion: number;
outputCreditsPerMillion: number;
fastModeMultiplier: number; // e.g. 1.5x for fast mode
}
function calculateCredits(usage: TokenUsage, rate: ModelRate): number {
const inputCost = (usage.inputTokens / 1_000_000) * rate.inputCreditsPerMillion;
const cachedCost = (usage.cachedInputTokens / 1_000_000) * rate.cachedInputCreditsPerMillion;
const outputCost = (usage.outputTokens / 1_000_000) * rate.outputCreditsPerMillion;
const base = inputCost + cachedCost + outputCost;
return usage.fastMode ? base * rate.fastModeMultiplier : base;
}
Tách biệt việc tính credits ra khỏi việc enforce limit — hai concerns khác nhau, dễ test độc lập hơn.
Lớp 3: Quota Enforcement — Sliding Window vs Fixed Window
Đây là phần nhiều người hay bỏ qua. Có 3 strategy phổ biến:
Fixed Window Counter
|---- period 1 ----|---- period 2 ----|
[used: 800/1000] [used: 0/1000]
Đơn giản nhất. Nhưng có "boundary burst problem": user có thể dùng 1000 credits cuối period 1 và 1000 credits đầu period 2 — tổng 2000 credits trong 1 khoảng thời gian ngắn.
Codex dùng fixed window (weekly/monthly reset) — chấp nhận boundary burst vì đây là developer tool, không phải financial system.
Sliding Window Log
Lưu timestamp của mỗi request, count trong window [now - period, now]:
async function checkQuota(userId: string, creditsNeeded: number): Promise<boolean> {
const windowStart = subWeeks(new Date(), 1); // weekly window
const { data } = await db
.from('token_usage_log')
.select('credits_consumed')
.eq('user_id', userId)
.gte('created_at', windowStart.toISOString());
const usedCredits = data.reduce((sum, row) => sum + row.credits_consumed, 0);
const quota = await getUserQuota(userId);
return usedCredits + creditsNeeded <= quota;
}
Chính xác hơn nhưng expensive — query toàn bộ log mỗi request. Không scale tốt.
Sliding Window Counter (Hybrid — Khuyến nghị)
Kết hợp tốt nhất của cả hai: chia window thành nhiều bucket nhỏ, giữ approximate sliding count trong Redis:
// Redis key pattern: quota:{userId}:{bucketTimestamp}
// Mỗi bucket = 1 giờ, window = 7 ngày = 168 buckets
async function consumeCredits(
userId: string,
credits: number,
quotaLimit: number
): Promise<{ allowed: boolean; remaining: number }> {
const now = Date.now();
const bucketSize = 3600_000; // 1 hour in ms
const windowSize = 7 * 24 * 3600_000; // 7 days
const currentBucket = Math.floor(now / bucketSize) * bucketSize;
const pipeline = redis.pipeline();
// Tính tổng usage trong sliding window (168 buckets)
const buckets: string[] = [];
for (let i = 0; i < 168; i++) {
const bucket = currentBucket - i * bucketSize;
buckets.push(`quota:${userId}:${bucket}`);
}
// Dùng Lua script để atomic check-and-increment
const luaScript = `
local total = 0
for i, key in ipairs(KEYS) do
local val = redis.call('GET', key)
if val then total = total + tonumber(val) end
end
local limit = tonumber(ARGV[1])
local credits = tonumber(ARGV[2])
local currentKey = KEYS[1]
local ttl = tonumber(ARGV[3])
if total + credits > limit then
return {0, limit - total}
end
redis.call('INCRBYFLOAT', currentKey, credits)
redis.call('EXPIRE', currentKey, ttl)
return {1, limit - total - credits}
`;
const result = await redis.eval(
luaScript,
buckets,
[quotaLimit.toString(), credits.toString(), (windowSize / 1000).toString()]
);
return {
allowed: result[0] === 1,
remaining: result[1],
};
}
Tại sao Lua script? Vì check-then-increment phải atomic — nếu không, race condition sẽ cho phép user vượt quota khi nhiều requests đến đồng thời.
Lớp 4: Pre-flight Check vs Post-flight Billing
Đây là một tension thú vị trong thiết kế hệ thống AI:
Vấn đề: Bạn không biết trước output tokens sẽ là bao nhiêu — model mới generate xong mới biết.
Hai approach:
Option A: Pre-flight estimate
1. User gửi request
2. System ước tính max tokens dựa trên input + max_tokens parameter
3. "Reserve" estimated credits
4. Gọi LLM
5. Actual credits = credits thật sự dùng
6. Hoàn trả phần reserve dư
Option B: Post-flight billing
1. User gửi request
2. Kiểm tra user còn quota không (rough check)
3. Gọi LLM
4. Tính credits thật từ response
5. Deduct credits
6. Nếu vượt quota → flag, charge vào credits/overage
Codex dùng Option B với overage protection — nếu bạn hết quota giữa chừng một task dài, Codex sẽ deduct vào purchased credits. Đây là UX tốt hơn cho developer workflow (không muốn task bị cắt ngang), nhưng phức tạp hơn về billing.
Lớp 5: Multi-tier Fair Use
Codex có một cơ chế thú vị: credits "multiplier" theo plan. Plus = baseline, Pro = 20x Plus, Business = custom. Điều này có nghĩa là cùng một "credit unit" nhưng giá trị thực tế khác nhau.
Cách implement đơn giản nhất: normalize về một đơn vị chung.
// Thay vì lưu "raw tokens", lưu "normalized credits"
// Mỗi plan có conversion_factor
const planMultipliers = {
'free': 1,
'plus': 1,
'pro': 20,
'business': 50, // example
};
// Quota limit của mỗi plan (normalized credits)
const planQuotas = {
'free': 100_000, // 100K normalized credits/month
'plus': 1_000_000, // 1M normalized credits/week
'pro': 20_000_000,
};
Normalized thì dễ so sánh, dễ migrate khi thay đổi pricing.
Lớp 6: Observability — Dashboard & Alerts
Hệ thống token limit mà không có observability là hệ thống chết. Những metrics cần track:
// Metrics để expose qua /status hoặc dashboard
interface UsageMetrics {
userId: string;
currentPeriod: {
start: Date;
end: Date;
quotaLimit: number;
quotaUsed: number;
quotaRemaining: number;
percentUsed: number;
};
purchasedCredits: number;
topModels: Array<{
modelId: string;
creditsConsumed: number;
percentOfTotal: number;
}>;
dailyBurnRate: number; // avg credits/day
projectedOverage: number; // ước tính thiếu bao nhiêu cuối kỳ
}
Codex CLI expose điều này qua lệnh /status — một feature nhỏ nhưng cực kỳ hữu ích cho developer.
Alert thresholds nên implement:
- 80% quota used → warning notification
- 95% quota used → urgent warning
- 100% quota → block hoặc charge overage
- Burn rate bất thường → anomaly alert (tránh bug vô tình loop gọi API)
Kiến trúc tổng thể
┌─────────────────────────────────────────────────────┐
│ API Gateway │
│ (auth, basic rate limit: req/sec per user) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ Token Quota Service │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Quota Check │ │Credit Calc │ │Usage Logger│ │
│ │ (Redis) │ │(Rate Card DB)│ │(Postgres) │ │
│ └─────────────┘ └──────────────┘ └────────────┘ │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ LLM Proxy Layer │
│ (gọi OpenAI/Anthropic, stream response) │
│ (đếm tokens từ response headers/usage field) │
└────────────────────┬────────────────────────────────┘
│
┌────────────────────▼────────────────────────────────┐
│ Billing & Credits Service │
│ (deduct credits, handle overage, invoice) │
└─────────────────────────────────────────────────────┘
Những edge case cần xử lý
1. Stream interrupted: User đóng connection giữa chừng — vẫn phải charge tokens đã generate.
2. Model fallback: Nếu model A overloaded, fallback sang model B có giá khác — billing phải dùng model thật sự served.
3. Cached tokens: OpenAI trả về cached_tokens trong usage field — cần parse đúng để charge giá thấp hơn.
4. Concurrent sessions: User mở 3 tab đồng thời — tất cả cùng đọc Redis, cần Lua script atomic (như đã đề cập ở trên).
5. Clock skew: Distributed system với nhiều node — dùng server-side timestamp, không bao giờ trust client clock.
Tổng kết
Thiết kế hệ thống token limit cho AI platform không phải chỉ là "thêm một middleware check quota". Nó đòi hỏi:
| Layer | Công nghệ | Mục đích |
|---|---|---|
| Data model | PostgreSQL | Audit trail, billing accuracy |
| Real-time quota | Redis + Lua | Atomic check, low latency |
| Credit calculation | Business logic layer | Rate card, multi-model |
| Enforcement strategy | Sliding window counter | Balance accuracy vs performance |
| Observability | Metrics + alerts | Transparency, anomaly detection |
Codex đã giải quyết bài toán này ở production scale với hàng triệu developer — việc phân tích cách họ thiết kế pricing model cho ta nhiều gợi ý valuable khi xây platform tương tự.
Nếu bạn đang xây AI SaaS với subscription model, đây là những quyết định thiết kế mà bạn sẽ phải đưa ra sớm hay muộn. Tốt hơn là nghĩ kỹ từ đầu hơn là refactor lại billing system sau khi đã có khách hàng thật.