LLM Credit Quota — API contract cho FE (2 màn admin)
Tài liệu chỉ mô tả API + dữ liệu màn hình để FE gen UI theo giao diện Flezi hiện tại (design.md).
- Source of truth:
app/features/rate_limit/(router.py,schemas.py,service.py). - Bỏ qua check quyền / permission matrix — giả định caller đã vào đúng màn.
- Không dùng HTML prototype cũ làm chuẩn UI. Gen theo Members / admin list + tokens
design.md.
0. Hai màn cần gen
| # | Màn | Ai dùng | Việc chính |
|---|---|---|---|
| A | Org credit budgets | System Admin | Set / sửa / xóa / bật-tắt trần credit tháng + default_credit cho từng organization |
| B | User credit quotas | Org Admin | Xem ngân sách org; sửa default_credit org; chia / sửa / xóa / bật-tắt hạn mức cho user trong org mình |
Cả hai chỉ làm việc với tháng UTC hiện tại (period dạng YYYY-MM). Không có filter tháng trên API.
1. Quy ước chung
Base
GET|POST|PUT|PATCH|DELETE /api/v1/rate-limit/...
Authorization: Bearer <token>
Content-Type: application/json
Đơn vị & hành vi
| 1 credit | = $0.01 USD |
| Body create/update org (A3/A4) | { limit_credits (> 0), default_credit (>= 0) }. BE tự tính limit_usd. is_enabled → ignore |
| Body create/update user (B4/B5) | chỉ limit_credits (> 0). Field lạ như is_enabled → ignore |
| Create | luôn gán is_enabled = true |
| Update (PUT) | không đổi is_enabled |
| Toggle | chỉ PATCH .../enabled { "is_enabled": bool } |
| Org default credit (OrgAdmin) | PATCH /org/llm-credit-quota/default-credit { "default_credit": >= 0 } — không đổi limit/enabled |
| Soft-admit | Chat cho chạy hết câu đang gửi ngay cả khi vượt hạn mức, nhưng used_credits luôn được kẹp ≤ limit_credits (atomic clamp) — không bao giờ vượt. ai_llm_costs (ledger nội bộ, không thuộc API này) cũng ghi nhận phần chi phí đã kẹp theo hạn mức còn lại, không phải chi phí LLM thật nếu request đó vượt hạn mức |
| Display | UI dùng display_* (đã kẹp ≤ limit / ≤ 100%) |
| List pagination | page (default 1), page_size (default 20, max 100). Envelope: total, page, page_size, total_pages. Ngoại lệ: B2b (users chưa có quota) trả full list, không phân trang |
| List sort | Org list (A1): Organization.created_at desc (mới nhất trước). User list (B2): User.created_at desc; loại system_admin |
| Chat gate | xem §7 — org disabled theo user.org_id chặn mọi member trước |
| Auto-allocate | Create/update user (/auth/users) có thể tự cấp quota = org default_credit khi còn budget — xem §8 |
Lỗi HTTP
Mọi lỗi quota (admin + chat) cùng shape FastAPI:
{
"detail": {
"error_code": "STRING",
"message": "human readable",
"...extra": "optional"
}
}
FE đọc response.status + body.detail.error_code / body.detail.message.
error_code | HTTP | Nghĩa ngắn |
|---|---|---|
QUOTA_NOT_FOUND | 404 | Chưa có config / org không tồn tại |
QUOTA_ALREADY_EXISTS | 409 | POST khi đã có row (org hoặc 1 user trong batch) |
LIMIT_BELOW_USAGE | 400 | Limit mới < đã dùng; hoặc xóa khi used > 0 |
ALLOCATION_EXCEEDED | 400 | Limit org < tổng đã chia; hoặc tổng user > trần org; xóa org còn user config |
ORG_BUDGET_NOT_SET | 400 | Org chưa có ngân sách (màn B) |
USER_NOT_IN_ORG | 400 hoặc 404 | Bulk = 400; get/put/delete/patch = 404 |
ORG_REQUIRED | 400 | Caller không có org_id (màn B) |
ORG_QUOTA_DISABLED | 403 | Org budget is_enabled=false |
QUOTA_DISABLED | 403 | User quota is_enabled=false |
NO_QUOTA_ASSIGNED | 403 | User chưa được cấp quota |
USER_QUOTA_EXCEEDED | 429 | Hết hạn mức tháng (chat) |
FORBIDDEN | 403 | Chat: không load được user cho quota check |
FE — admin (màn A/B): mọi lỗi API trên → toast (detail.message; có thể map error_code sang copy riêng).
FE — chat: xem §7 (toast lỗi gate + khóa input; threshold 80–100% → noti chuông + toast chat).
Khối số liệu lặp lại
type UsageView = {
limit_credits: number | null;
limit_usd: number | null;
used_credits: number | null; // đã kẹp ≤ limit (atomic clamp) — luôn bằng display_used_credits
display_used_credits: number | null; // min(used, limit)
display_percent: number | null; // 0..100
remaining_credits: number | null; // max(0, limit - used)
is_exceeded: boolean; // used >= limit
is_enabled: boolean | null; // null nếu chưa có config
};
type OrgQuotaItem = UsageView & {
org_id: string;
org_name: string | null;
has_budget: boolean;
default_credit: number; // gợi ý hạn mức khi invite user; 0 nếu chưa có budget
allocated_credits: number | null; // SUM limit user trong org
budget_remaining_credits: number | null; // limit_org - allocated
};
type UserQuotaItem = UsageView & {
user_id: string;
email: string | null;
full_name: string | null;
has_quota: boolean;
};
/** Lean row — chỉ dùng cho picker assign (B2b), không có UsageView. */
type OrgUserWithoutQuotaItem = {
user_id: string;
email: string | null;
full_name: string | null;
};
type PageMeta = {
total: number;
page: number;
page_size: number;
total_pages: number;
};
Khi has_budget=false / has_quota=false: is_enabled = null; các field limit/display thường null (user chưa cấp vẫn có thể có used_credits: 0). default_credit vẫn là 0 khi org chưa có budget.
Search q
- Optional
?q=...; BEstrip(); rỗng sau trim → không lọc - List org: ILIKE tên org
- List user (B2, B2b): ILIKE email OR full_name
- Search user: block org summary không thu hẹp
2. Màn A — System Admin · Org budgets
2.1 Mục tiêu
- List org + trạng thái budget tháng hiện tại (mới nhất trước).
- POST tạo / PUT sửa / DELETE xóa (không upsert) — body gồm
limit_credits+default_credit. - PATCH bật/tắt
is_enabled. - (Tuỳ) detail org + list user read-only.
2.2 API
A1. List orgs
GET /api/v1/rate-limit/admin/orgs/llm-credit-quotas
GET /api/v1/rate-limit/admin/orgs/llm-credit-quotas?q=acme&page=1&page_size=20
200
{
"period": "2026-09",
"total": 2,
"page": 1,
"page_size": 20,
"total_pages": 1,
"items": [
{
"org_id": "11111111-1111-1111-1111-111111111111",
"org_name": "Acme",
"has_budget": true,
"default_credit": 100,
"limit_credits": 1000,
"limit_usd": 10,
"allocated_credits": 1000,
"budget_remaining_credits": 0,
"used_credits": 2100,
"display_used_credits": 1000,
"display_percent": 100,
"remaining_credits": 0,
"is_exceeded": true,
"is_enabled": true
},
{
"org_id": "22222222-2222-2222-2222-222222222222",
"org_name": "Brightside Labs",
"has_budget": false,
"default_credit": 0,
"allocated_credits": 0,
"limit_credits": null,
"limit_usd": null,
"used_credits": null,
"display_used_credits": null,
"display_percent": null,
"remaining_credits": null,
"budget_remaining_credits": null,
"is_exceeded": false,
"is_enabled": null
}
]
}
| Cột UI | Field |
|---|---|
| Tên | org_name |
| Period | period (root) |
| Status | !has_budget / is_enabled===false / is_exceeded / near limit / ok |
| Limit | limit_credits hoặc — |
| Default credit | default_credit (gợi ý khi invite user) |
| Used (progress) | display_used_credits, display_percent |
| Used (audit) | used_credits (đã kẹp ≤ limit, không phải chi phí LLM thật — xem ghi chú soft-admit ở §1) |
| Allocated | allocated_credits |
| Còn chia | budget_remaining_credits |
| Enabled | is_enabled → A4b |
A2. Chi tiết 1 org (org fields only)
GET /api/v1/rate-limit/admin/orgs/{org_id}/llm-credit-quota
200 = OrgQuotaItem + period. Không trả users[] — Edit budget dùng row từ list A1 (hoặc A2) rồi PUT A4.
{
"period": "2026-09",
"org_id": "…",
"org_name": "Acme",
"has_budget": true,
"default_credit": 100,
"limit_credits": 1000,
"limit_usd": 10,
"allocated_credits": 1000,
"budget_remaining_credits": 0,
"used_credits": 2100,
"display_used_credits": 1000,
"display_percent": 100,
"remaining_credits": 0,
"is_exceeded": true,
"is_enabled": true
}
Org không tồn tại → 404 QUOTA_NOT_FOUND.
A3. Tạo budget
POST /api/v1/rate-limit/admin/orgs/{org_id}/llm-credit-quota
{ "limit_credits": 1000, "default_credit": 100 }
- 201 →
OrgQuotaItem(is_enabled: true, allocated/used thường 0). default_creditbắt buộc,>= 0(không ràng buộc vớilimit_credits).- Đã có → 409
QUOTA_ALREADY_EXISTS.
A4. Sửa budget
PUT /api/v1/rate-limit/admin/orgs/{org_id}/llm-credit-quota
{ "limit_credits": 900, "default_credit": 50 }
- 200 →
OrgQuotaItem(giữ nguyênis_enabled; cập nhậtdefault_credit). - Chưa có → 404.
new < used→ 400LIMIT_BELOW_USAGE.new < allocated→ 400ALLOCATION_EXCEEDED.default_credit < 0→ 422 validation.
new_limit >= max(used_credits, allocated_credits)
new_limit > 0
default_credit >= 0
A4b. Bật / tắt budget
PATCH /api/v1/rate-limit/admin/orgs/{org_id}/llm-credit-quota/enabled
{ "is_enabled": false }
- 200 →
OrgQuotaItem. - Chưa có budget → 404.
false→ mọi user org không chat (ORG_QUOTA_DISABLED).
A5. Xóa budget
DELETE /api/v1/rate-limit/admin/orgs/{org_id}/llm-credit-quota
- 204.
- org used > 0 → 400
LIMIT_BELOW_USAGE. - còn user configs → 400
ALLOCATION_EXCEEDED. - chưa có → 404.
Disable Xóa khi: used_credits > 0 hoặc allocated_credits > 0.
2.3 Luồng UI ↔ API
Mount → GET A1
Set (has_budget=false) → POST A3 `{ limit_credits, default_credit }`
Edit → dùng row A1 (used/allocated/limit/default_credit) → PUT A4
Enable/disable → PATCH A4b → refresh A1
Remove → DELETE A5 → refresh A1
Search → GET A1?q=
Detail (optional) → GET A2 (org fields only, không users)
3. Màn B — Org Admin · User quotas
3.1 Mục tiêu
- Ngân sách org (trần / used / allocated /
is_enabled/default_credit). - Sửa
default_creditorg (không đụng limit) — B1b. - List mọi user trong org (kể cả
has_quota: false), có pagination; mới nhất trước; không gồmsystem_admin. - Picker assign: full list user chưa có quota (lean fields, không phân trang).
- Bulk create cùng 1
limit_credits(is_enabled=true). - PUT / DELETE từng user; PUT không đổi
is_enabled. - PATCH bật/tắt từng user.
Org id từ session — path không có {org_id}.
Chưa có budget SysAdmin → hầu hết GET 400 ORG_BUDGET_NOT_SET.
Invite user (POST /auth/users) có thể tự cấp quota = default_credit nếu còn budget — xem §8.
3.2 API
B1. Summary org (optional)
GET /api/v1/rate-limit/org/llm-credit-budget
200 → OrgQuotaItem (có default_credit; không có period trên item; lấy period từ B2).
400 ORG_BUDGET_NOT_SET.
B1b. Sửa default credit (OrgAdmin)
Chỉ đổi gợi ý hạn mức khi invite / auto-allocate — không đổi limit_credits / is_enabled.
PATCH /api/v1/rate-limit/org/llm-credit-quota/default-credit
{ "default_credit": 100 }
- 200 →
OrgQuotaItem(full row, có allocated/used). default_credit >= 0(422 nếu âm).- Chưa có budget → 404
QUOTA_NOT_FOUND. - SysAdmin không gọi API này (permission LC-09 = OrgAdmin only).
B2. List users + org
GET /api/v1/rate-limit/org/users/llm-credit-quotas
GET /api/v1/rate-limit/org/users/llm-credit-quotas?q=alice&page=1&page_size=20
200
{
"period": "2026-09",
"total": 2,
"page": 1,
"page_size": 20,
"total_pages": 1,
"org": {
"org_id": "…",
"org_name": "Acme",
"has_budget": true,
"default_credit": 100,
"limit_credits": 1000,
"limit_usd": 10,
"allocated_credits": 800,
"budget_remaining_credits": 200,
"used_credits": 450,
"display_used_credits": 450,
"display_percent": 45,
"remaining_credits": 550,
"is_exceeded": false,
"is_enabled": true
},
"users": [
{
"user_id": "…",
"email": "a@x.com",
"full_name": "A",
"has_quota": true,
"limit_credits": 400,
"limit_usd": 4,
"used_credits": 390,
"display_used_credits": 390,
"display_percent": 97.5,
"remaining_credits": 10,
"is_exceeded": false,
"is_enabled": true
},
{
"user_id": "…",
"email": "b@x.com",
"full_name": "B",
"has_quota": false,
"limit_credits": null,
"limit_usd": null,
"used_credits": 0,
"display_used_credits": 0,
"display_percent": null,
"remaining_credits": null,
"is_exceeded": false,
"is_enabled": null
}
]
}
| Label | Field |
|---|---|
| Org limit | org.limit_credits |
| Default credit | org.default_credit |
| Org used | org.display_used_credits (+ tooltip used_credits) |
| Allocated | org.allocated_credits |
| Remaining to allocate | org.budget_remaining_credits |
| Org enabled | org.is_enabled |
| Period | period (root) |
| Cột user | Field |
|---|---|
| Name / email | full_name, email |
| Status | !has_quota / is_enabled===false / is_exceeded / near / ok |
| Limit / Used / Remaining | limit_credits, display_used_credits, remaining_credits |
| Enabled | is_enabled → B5b |
B2b. List users chưa có quota (picker assign)
Dùng khi mở modal / multi-select để Assign — chỉ user trong org chưa có UserLlmCreditConfig. Không phân trang (full list). Mỗi row chỉ user_id / full_name / email (không load usage).
GET /api/v1/rate-limit/org/users/without-llm-credit-quotas
GET /api/v1/rate-limit/org/users/without-llm-credit-quotas?q=alice
200
{
"period": "2026-09",
"total": 2,
"org": {
"org_id": "…",
"org_name": "Acme",
"has_budget": true,
"default_credit": 100,
"limit_credits": 1000,
"limit_usd": 10,
"allocated_credits": 800,
"budget_remaining_credits": 200,
"used_credits": 450,
"display_used_credits": 450,
"display_percent": 45,
"remaining_credits": 550,
"is_exceeded": false,
"is_enabled": true
},
"users": [
{
"user_id": "…",
"email": "b@x.com",
"full_name": "B"
},
{
"user_id": "…",
"email": "c@x.com",
"full_name": "C"
}
]
}
| Envelope | Giống B2: period, org, users, total — không có page / page_size / total_pages |
users[] | OrgUserWithoutQuotaItem — chỉ 3 field |
| Lọc | Không gồm user đã có quota; bỏ system_admin; bỏ soft-deleted |
| Search | ?q= ILIKE email / full_name |
| Lỗi | 400 ORG_BUDGET_NOT_SET nếu org chưa có ngân sách |
B3. Get 1 user
GET /api/v1/rate-limit/org/users/{user_id}/llm-credit-quota
200 { period, org: OrgQuotaItem, user: UserQuotaItem }
has_quota: false vẫn 200. User ngoài org → 404 USER_NOT_IN_ORG.
B4. Bulk tạo
POST /api/v1/rate-limit/org/users/llm-credit-quotas
{
"limit_credits": 400,
"user_ids": ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"]
}
- 201 → cùng shape B2 (page mặc định
page=1,page_size=20— không phải “full dump” mọi user). - Candidate list cho picker: GET B2b (không lấy từ B2 rồi filter
has_quota===falsetrên FE nếu có thể tránh — B2b đã lọc + lean). - User đã có quota → 409 (cả batch).
- User không thuộc org → 400
USER_NOT_IN_ORG. - Không đủ budget → 400
ALLOCATION_EXCEEDED. limit < usedtừng user → 400LIMIT_BELOW_USAGE.- Row mới:
is_enabled=true.
user_ids.length >= 1
mọi user: chưa có quota (chọn từ B2b)
limit_credits > 0
allocated + user_ids.length * limit_credits <= org.limit_credits
B5. Sửa 1 user
PUT /api/v1/rate-limit/org/users/{user_id}/llm-credit-quota
{ "limit_credits": 500 }
- 200 → shape B3; không đổi
is_enabled. - Chưa có quota → 404
QUOTA_NOT_FOUND. new < used→ 400LIMIT_BELOW_USAGE.- Tổng limit sau sửa > org → 400
ALLOCATION_EXCEEDED.
new_limit >= user.used_credits
(org.allocated - old_limit + new_limit) <= org.limit_credits
B5b. Bật / tắt 1 user
PATCH /api/v1/rate-limit/org/users/{user_id}/llm-credit-quota/enabled
{ "is_enabled": false }
- 200 → shape B3.
- Chưa có quota / ngoài org → 404.
false→ user không chat (QUOTA_DISABLED); nếu org đã tắt thì chat vẫn báoORG_QUOTA_DISABLEDtrước.true(enable lại) không tự xóaUSER_QUOTA_EXCEEDED: nếuused_creditscủa user vẫn>= limit_credits(đã bị kẹp ở đúng limit từ trước khi disable), user vẫn bị chặn chat — chỉ đổi lý do chặn từQUOTA_DISABLEDsangUSER_QUOTA_EXCEEDED. Muốn user chat lại ngay phải PUT B5 tănglimit_credits(tự động reset threshold alert — xem §7).
B6. Xóa 1 user
DELETE /api/v1/rate-limit/org/users/{user_id}/llm-credit-quota
- 204.
used_credits > 0→ 400LIMIT_BELOW_USAGE.- Xóa xong → chat 403
NO_QUOTA_ASSIGNED.
3.3 Luồng UI ↔ API
Mount → GET B2 (ORG_BUDGET_NOT_SET → empty)
Edit default credit → PATCH B1b → refresh B2 / B1
Open Assign picker → GET B2b (?q= optional)
Assign → POST B4 (user_ids từ B2b) → replace state bằng response B2
Edit → PUT B5
Enable/disable → PATCH B5b → refresh B2
Remove → DELETE B6 → refresh B2
Search/paginate table → GET B2?q=&page=&page_size=
4. Mock data tối thiểu
List org (A1)
{
"period": "2026-09",
"total": 3,
"page": 1,
"page_size": 20,
"total_pages": 1,
"items": [
{
"org_id": "o1", "org_name": "Flezi Galaxy", "has_budget": true,
"default_credit": 500,
"limit_credits": 50000, "limit_usd": 500,
"allocated_credits": 32000, "budget_remaining_credits": 18000,
"used_credits": 18420.5, "display_used_credits": 18420.5,
"display_percent": 36.84, "remaining_credits": 31579.5,
"is_exceeded": false, "is_enabled": true
},
{
"org_id": "o2", "org_name": "Northwind Health", "has_budget": true,
"default_credit": 200,
"limit_credits": 8000, "limit_usd": 80,
"allocated_credits": 8000, "budget_remaining_credits": 0,
"used_credits": 8100, "display_used_credits": 8000,
"display_percent": 100, "remaining_credits": 0,
"is_exceeded": true, "is_enabled": false
},
{
"org_id": "o3", "org_name": "Brightside Labs", "has_budget": false,
"default_credit": 0,
"allocated_credits": 0, "limit_credits": null, "used_credits": null,
"display_used_credits": null, "display_percent": null,
"remaining_credits": null, "budget_remaining_credits": null,
"is_exceeded": false, "is_enabled": null
}
]
}
List users (B2)
{
"period": "2026-09",
"total": 3,
"page": 1,
"page_size": 20,
"total_pages": 1,
"org": {
"org_id": "o1", "org_name": "Flezi Galaxy", "has_budget": true,
"default_credit": 500,
"limit_credits": 50000, "limit_usd": 500,
"allocated_credits": 34000, "budget_remaining_credits": 16000,
"used_credits": 18420.5, "display_used_credits": 18420.5,
"display_percent": 36.84, "remaining_credits": 31579.5,
"is_exceeded": false, "is_enabled": true
},
"users": [
{
"user_id": "u1", "full_name": "Maya Chen", "email": "maya@example.com",
"has_quota": true, "limit_credits": 10000, "limit_usd": 100,
"used_credits": 8200, "display_used_credits": 8200,
"display_percent": 82, "remaining_credits": 1800,
"is_exceeded": false, "is_enabled": true
},
{
"user_id": "u2", "full_name": "Tom Alvarez", "email": "tom@example.com",
"has_quota": true, "limit_credits": 2000, "limit_usd": 20,
"used_credits": 2100, "display_used_credits": 2000,
"display_percent": 100, "remaining_credits": 0,
"is_exceeded": true, "is_enabled": false
},
{
"user_id": "u3", "full_name": "Sam Wu", "email": "sam@example.com",
"has_quota": false, "limit_credits": null, "limit_usd": null,
"used_credits": 0, "display_used_credits": 0,
"display_percent": null, "remaining_credits": null,
"is_exceeded": false, "is_enabled": null
}
]
}
Users chưa có quota — picker (B2b)
{
"period": "2026-09",
"total": 1,
"org": {
"org_id": "o1", "org_name": "Flezi Galaxy", "has_budget": true,
"default_credit": 500,
"limit_credits": 50000, "limit_usd": 500,
"allocated_credits": 34000, "budget_remaining_credits": 16000,
"used_credits": 18420.5, "display_used_credits": 18420.5,
"display_percent": 36.84, "remaining_credits": 31579.5,
"is_exceeded": false, "is_enabled": true
},
"users": [
{
"user_id": "u3",
"full_name": "Sam Wu",
"email": "sam@example.com"
}
]
}
5. Gợi ý gen UI
Theo design.md:
- Font Inter; accent
#F36F21một primary CTA / trang. - Shell: sidebar + main; page header + toolbar (search 38px, count pill + pagination).
- Table trong frame (
--radius-container12px, border--border-subtle). - Status pill: success / warning / danger / muted (Disabled riêng khi
is_enabled===false). - Toggle enabled tách khỏi form Edit limit.
- Form Set/Edit org budget: 2 field
limit_credits+default_credit(>= 0). - OrgAdmin: control sửa
default_creditriêng (B1b), không lẫn form limit user. - Progress theo
display_percent/display_used_credits. - Assign picker: load B2b (full lean list); table chính vẫn B2 + pagination.
Không copy spacing từ docs/prototypes/rate-limit-*.html cũ.
6. Inventory endpoint
Màn A
| Method | Path | Body | Success |
|---|---|---|---|
| GET | /rate-limit/admin/orgs/llm-credit-quotas | — | { period, items[], total, page, page_size, total_pages } |
| GET | /rate-limit/admin/orgs/{org_id}/llm-credit-quota | — | org fields + period (no users) |
| POST | /rate-limit/admin/orgs/{org_id}/llm-credit-quota | { limit_credits, default_credit } | 201 OrgQuotaItem |
| PUT | /rate-limit/admin/orgs/{org_id}/llm-credit-quota | { limit_credits, default_credit } | 200 OrgQuotaItem |
| PATCH | /rate-limit/admin/orgs/{org_id}/llm-credit-quota/enabled | { is_enabled } | 200 OrgQuotaItem |
| DELETE | /rate-limit/admin/orgs/{org_id}/llm-credit-quota | — | 204 |
Màn B
| Method | Path | Body | Success |
|---|---|---|---|
| GET | /rate-limit/org/llm-credit-budget | — | OrgQuotaItem (có default_credit) |
| PATCH | /rate-limit/org/llm-credit-quota/default-credit | { default_credit } | 200 OrgQuotaItem |
| GET | /rate-limit/org/users/llm-credit-quotas | — | { period, org, users[], total, page, page_size, total_pages } |
| GET | /rate-limit/org/users/without-llm-credit-quotas | — | { period, org, users[{user_id,email,full_name}], total } (full list, no page) |
| GET | /rate-limit/org/users/{user_id}/llm-credit-quota | — | { period, org, user } |
| POST | /rate-limit/org/users/llm-credit-quotas | { limit_credits, user_ids[] } | 201 list shape (paginated defaults) |
| PUT | /rate-limit/org/users/{user_id}/llm-credit-quota | { limit_credits } | 200 detail |
| PATCH | /rate-limit/org/users/{user_id}/llm-credit-quota/enabled | { is_enabled } | 200 detail |
| DELETE | /rate-limit/org/users/{user_id}/llm-credit-quota | — | 204 |
Prefix: /api/v1 + path trên.
7. Ngoài 2 màn — /me + chat enforce + toast / noti
GET /api/v1/rate-limit/me/llm-credit-usage
Login only (không LC). Banner chat / trạng thái trước khi gửi.
{
"has_quota": true,
"is_system_admin": false,
"org_id": "uuid",
"period": "2026-09",
"limit_credits": 10,
"limit_usd": 0.1,
"used_credits": 10,
"display_used_credits": 10,
"display_percent": 100,
"remaining_credits": 0,
"is_exceeded": true,
"is_enabled": true,
"blocked_reason": "USER_QUOTA_EXCEEDED"
}
blocked_reason | Khi nào |
|---|---|
ORG_QUOTA_DISABLED | Org budget theo user.org_id tồn tại và is_enabled=false (kể cả chưa có user quota) |
NO_QUOTA_ASSIGNED | Chưa có user config |
QUOTA_DISABLED | User config is_enabled=false |
USER_QUOTA_EXCEEDED | used >= limit |
null | Được chat / SysAdmin bypass (mọi system_admin) |
Chat pre-check
Gắn POST .../messages và POST .../runs/{run_id}/retry — trước start_run.
- Bypass:
system_admin - Org
is_enabled=false→ 403ORG_QUOTA_DISABLED - Không user config → 403
NO_QUOTA_ASSIGNED - User
is_enabled=false→ 403QUOTA_DISABLED used >= limit→ 429USER_QUOTA_EXCEEDED
Ví dụ 429:
{
"detail": {
"error_code": "USER_QUOTA_EXCEEDED",
"message": "Monthly credit quota exceeded. Contact your admin.",
"limit_credits": 10.0,
"used_credits": 10.0,
"display_used_credits": 10.0,
"display_percent": 100.0,
"remaining_credits": 0.0,
"is_exceeded": true
}
}
FE xử lý lỗi chat → toast
| Nguồn | Hành vi UI |
|---|---|
/me với blocked_reason != null | Khóa ô nhập; có thể toast / banner theo blocked_reason (không cần đợi send fail) |
| Send / retry 403/429 quota | Toast lỗi: ưu tiên detail.message; map detail.error_code nếu cần copy i18n |
| Send thành công | Không toast lỗi quota (soft-admit có thể để used vượt sau settle) |
Các error_code chat FE cần toast:
ORG_QUOTA_DISABLED(403)NO_QUOTA_ASSIGNED(403)QUOTA_DISABLED(403)USER_QUOTA_EXCEEDED(429)FORBIDDEN(403 — user missing, hiếm)
Threshold 80 / 85 / 90 / 95 / 100% → noti chuông + toast trong chat
Không phải response lỗi của send. BE settle cost sau LLM xong, so used/limit, rồi bắn alert. FE hiển thị cả hai:
- Noti (chuông / inbox) — luôn có.
- Toast trên màn chat — khi user đang trong conversation (qua SSE).
Chi tiết BE:
- Mốc: 80, 85, 90, 95, 100 (%).
- Mỗi mốc một lần / user / period (
alerts_sent); nhảy cóc (vd. 70% → 95%) chỉ bắn một thông báo — mốc cao nhất vừa vượt (95%); các mốc thấp hơn chưa gửi (80/85/90) được đánh dấu đã gửi âm thầm, không tạo thông báo riêng. - 100% →
level: "error"; 80–95 →level: "warning". - Org Admin tăng
limit_creditscho 1 user (B5 — PUT.../users/{user_id}/llm-credit-quota) → resetalerts_sentcủa user cho period hiện tại, để user có thể nhận lại alert 80–100% theo hạn mức mới. Giảm limit hoặc đổi limit org (A4) không reset. Qua tháng mới,alerts_senttự "reset" vì là row usage riêng theoperiod.
Kênh FE:
| Kênh | Nguồn | UI |
|---|---|---|
| Noti chuông | publish_notification · type: "llm_credit_threshold" | Item trong notification center (title/body từ BE) |
| Toast chat | SSE conversation · event type: "llm_credit_threshold" | Toast (hoặc banner) trong thread đang mở; payload: threshold, level, display_percent, display_used_credits, limit_credits |
Lưu ý:
- Threshold khác toast lỗi gate (403/429). Có thể vừa có toast threshold (sau câu chat thành công) vừa sau đó bị 429 ở câu tiếp.
- Không có
conversation_id→ chỉ còn noti chuông, không toast chat. - Khi user đã
blocked_reason === USER_QUOTA_EXCEEDED/ send 429: không kỳ vọng alert threshold mới từ request bị chặn (không settle). - Reload trang / SSE reconnect không có cursor (
Last-Event-ID) sẽ không replay lại eventllm_credit_thresholdcũ từ Redis stream — tránh toast lặp lại mỗi lần F5. Reconnect có cursor thật (mất mạng giữa chừng) vẫn replay bình thường, không bỏ event nào.
8. Auto-allocate default_credit khi thêm / sửa user (auth)
Không thuộc 2 màn rate-limit UI, nhưng ảnh hưởng list B2 sau khi invite / đổi org.
APIs:
POST /api/v1/auth/users— tạo hoặc reactivate (email soft-deleted → bật lại cùnguser_id)PUT /api/v1/auth/users/{user_id}— cập nhật (đổi org chỉsystem_admin)
Hành vi credit (soft-fail — user API vẫn 200/201):
| Sự kiện | Credit |
|---|---|
Create user, org có default_credit > 0, còn budget (remaining >= default), chưa có usage vượt default | Tạo UserLlmCreditConfig với limit_credits = default_credit |
Create / allocate nhưng thiếu budget hoặc used > default_credit | Không tạo config; notify người gọi API |
default_credit = 0 / không có org budget / org_id null / target system_admin | Skip, không notify |
| Reactivate / update đổi org | Xóa config cũ (nếu có) → thử allocate theo org mới |
| Update không đổi org, user chưa có config | Backfill giống create |
| Update không đổi org, đã có config | Giữ nguyên limit |
Notify (chuông) khi không đủ budget:
| Type | llm_credit_org_budget_insufficient |
| Người nhận | Requester (admin vừa gọi create/update user), không phải user mới |
| Payload | org_id, user_id (target), default_credit, budget_remaining |
Chi tiết use-case: rate-limit-user-lifecycle-credit.md.
Doc đồng bộ code BE app/features/rate_limit/ (router / schemas / service) + hook auth create/update user. Khi schema đổi, ưu tiên code.