Skip to main content

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ànAi dùngViệc chính
AOrg credit budgetsSystem AdminSet / sửa / xóa / bật-tắt trần credit tháng + default_credit cho từng organization
BUser credit quotasOrg AdminXem 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
Createluôn gán is_enabled = true
Update (PUT)không đổi is_enabled
Togglechỉ 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-admitChat 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
DisplayUI dùng display_* (đã kẹp ≤ limit / ≤ 100%)
List paginationpage (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 sortOrg list (A1): Organization.created_at desc (mới nhất trước). User list (B2): User.created_at desc; loại system_admin
Chat gatexem §7 — org disabled theo user.org_id chặn mọi member trước
Auto-allocateCreate/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_codeHTTPNghĩa ngắn
QUOTA_NOT_FOUND404Chưa có config / org không tồn tại
QUOTA_ALREADY_EXISTS409POST khi đã có row (org hoặc 1 user trong batch)
LIMIT_BELOW_USAGE400Limit mới < đã dùng; hoặc xóa khi used > 0
ALLOCATION_EXCEEDED400Limit org < tổng đã chia; hoặc tổng user > trần org; xóa org còn user config
ORG_BUDGET_NOT_SET400Org chưa có ngân sách (màn B)
USER_NOT_IN_ORG400 hoặc 404Bulk = 400; get/put/delete/patch = 404
ORG_REQUIRED400Caller không có org_id (màn B)
ORG_QUOTA_DISABLED403Org budget is_enabled=false
QUOTA_DISABLED403User quota is_enabled=false
NO_QUOTA_ASSIGNED403User chưa được cấp quota
USER_QUOTA_EXCEEDED429Hết hạn mức tháng (chat)
FORBIDDEN403Chat: 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=...; BE strip(); 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​

  1. List org + trạng thái budget tháng hiện tại (mới nhất trước).
  2. POST tạo / PUT sửa / DELETE xóa (không upsert) — body gồm limit_credits + default_credit.
  3. PATCH bật/tắt is_enabled.
  4. (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 UIField
Tênorg_name
Periodperiod (root)
Status!has_budget / is_enabled===false / is_exceeded / near limit / ok
Limitlimit_credits hoặc —
Default creditdefault_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)
Allocatedallocated_credits
Còn chiabudget_remaining_credits
Enabledis_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_credit bắt buộc, >= 0 (không ràng buộc với limit_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ên is_enabled; cập nhật default_credit).
  • Chưa có → 404.
  • new < used → 400 LIMIT_BELOW_USAGE.
  • new < allocated → 400 ALLOCATION_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​

  1. Ngân sách org (trần / used / allocated / is_enabled / default_credit).
  2. Sửa default_credit org (không đụng limit) — B1b.
  3. List mọi user trong org (kể cả has_quota: false), có pagination; mới nhất trước; không gồm system_admin.
  4. Picker assign: full list user chưa có quota (lean fields, không phân trang).
  5. Bulk create cùng 1 limit_credits (is_enabled=true).
  6. PUT / DELETE từng user; PUT không đổi is_enabled.
  7. 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
}
]
}
LabelField
Org limitorg.limit_credits
Default creditorg.default_credit
Org usedorg.display_used_credits (+ tooltip used_credits)
Allocatedorg.allocated_credits
Remaining to allocateorg.budget_remaining_credits
Org enabledorg.is_enabled
Periodperiod (root)
Cột userField
Name / emailfull_name, email
Status!has_quota / is_enabled===false / is_exceeded / near / ok
Limit / Used / Remaininglimit_credits, display_used_credits, remaining_credits
Enabledis_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"
}
]
}
EnvelopeGiống B2: period, org, users, total — không có page / page_size / total_pages
users[]OrgUserWithoutQuotaItem — chỉ 3 field
LọcKhông gồm user đã có quota; bỏ system_admin; bỏ soft-deleted
Search?q= ILIKE email / full_name
Lỗi400 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===false trê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 < used từng user → 400 LIMIT_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 → 400 LIMIT_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áo ORG_QUOTA_DISABLED trước.
  • true (enable lại) không tự xóa USER_QUOTA_EXCEEDED: nếu used_credits củ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_DISABLED sang USER_QUOTA_EXCEEDED. Muốn user chat lại ngay phải PUT B5 tăng limit_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 → 400 LIMIT_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 #F36F21 một primary CTA / trang.
  • Shell: sidebar + main; page header + toolbar (search 38px, count pill + pagination).
  • Table trong frame (--radius-container 12px, 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_credit riê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​

MethodPathBodySuccess
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​

MethodPathBodySuccess
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_reasonKhi nào
ORG_QUOTA_DISABLEDOrg budget theo user.org_id tồn tại và is_enabled=false (kể cả chưa có user quota)
NO_QUOTA_ASSIGNEDChưa có user config
QUOTA_DISABLEDUser config is_enabled=false
USER_QUOTA_EXCEEDEDused >= 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.

  1. Bypass: system_admin
  2. Org is_enabled=false → 403 ORG_QUOTA_DISABLED
  3. Không user config → 403 NO_QUOTA_ASSIGNED
  4. User is_enabled=false → 403 QUOTA_DISABLED
  5. used >= limit → 429 USER_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ồnHành vi UI
/me với blocked_reason != nullKhóa ô nhập; có thể toast / banner theo blocked_reason (không cần đợi send fail)
Send / retry 403/429 quotaToast lỗi: ưu tiên detail.message; map detail.error_code nếu cần copy i18n
Send thành côngKhô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:

  1. Noti (chuông / inbox) — luôn có.
  2. 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_credits cho 1 user (B5 — PUT .../users/{user_id}/llm-credit-quota) → reset alerts_sent củ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_sent tự "reset" vì là row usage riêng theo period.

Kênh FE:

KênhNguồnUI
Noti chuôngpublish_notification · type: "llm_credit_threshold"Item trong notification center (title/body từ BE)
Toast chatSSE 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 event llm_credit_threshold cũ 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ùng user_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ệnCredit
Create user, org có default_credit > 0, còn budget (remaining >= default), chưa có usage vượt defaultTạo UserLlmCreditConfig với limit_credits = default_credit
Create / allocate nhưng thiếu budget hoặc used > default_creditKhông tạo config; notify người gọi API
default_credit = 0 / không có org budget / org_id null / target system_adminSkip, không notify
Reactivate / update đổi orgXóa config cũ (nếu có) → thử allocate theo org mới
Update không đổi org, user chưa có configBackfill giống create
Update không đổi org, đã có configGiữ nguyên limit

Notify (chuông) khi không đủ budget:

Typellm_credit_org_budget_insufficient
Người nhậnRequester (admin vừa gọi create/update user), không phải user mới
Payloadorg_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.