Lỗi
Status cho bạn biết mức độ nghiêm trọng. Phần lớn lỗi từ chối còn kèm theo code, và
khi có code thì đó là phần duy nhất trong body an toàn để rẽ nhánh — message là
câu chữ tự nhiên, một phần bằng tiếng Việt, và tất cả đều có thể được viết lại.
Điều cần biết trước tiên là không phải lỗi nào cũng có code. Phần lớn các cách
một lời gọi /v1 có thể bị từ chối là có code; một số ít chỉ trả về status kèm một
câu mô tả. Trang này nói rõ cái nào thuộc nhóm nào, vì một client viết theo giả định
rằng lỗi nào cũng có code sẽ lặng lẽ rơi xuống nhánh mặc định ở những lỗi không
có.
Hai dạng body
Các endpoint gốc trả về dạng của nền tảng:
{ "statusCode": 403, "message": "Insufficient tokens", "traceId": "0f7c…" }
POST /v1/audio/speech thì trả về dạng của OpenAI, để một client OpenAI đọc được mà
không cần lớp chuyển đổi nào:
{ "error": { "message": "…", "type": "rate_limit_exceeded", "param": null, "code": null } }
Khác biệt chỉ có thế: một route, một envelope. Mọi thứ còn lại trên /v1 — kể cả
/v1/vapi/speech, vốn phục vụ một bên thứ ba — đều dùng dạng thứ nhất.
Mọi body dạng gốc đều lặp lại status ở statusCode, và thêm code cùng các trường
đi kèm khi lỗi từ chối có mang code:
{ "statusCode": 429,
"message": "Server is at capacity. Please try again in a few minutes.",
"code": "QUEUE_FULL", "traceId": "0f7c…" }
Trước đây một body có code thì lại thiếu statusCode — tức là đổi lấy một lý do
máy đọc được thì mất đi một trường bạn vẫn luôn đọc được. Khoảng trống đó đã khép
lại: giờ mọi body dạng gốc đều có trường này. Dòng status của HTTP vẫn là bản chuẩn
— chỗ nào đọc được từ response thì hãy đọc từ đó.
Những lỗi từ chối có kèm code
Hạn mức và grant
Sáu route tính tiền theo ký tự hoặc theo thời lượng — POST /v1/tts,
/v1/tts/stream, /v1/dialogue, /v1/dub, /v1/srt và
/v1/vapi/speech — đều trả lời một lỗi hạn mức kèm code, và hai lỗi tự phục hồi
thì nói rõ khi nào:
code | Status | Nghĩa là gì | Cần làm gì |
|---|---|---|---|
GRANT_EXPIRED | 403 | Gói token đứng sau key đã quá hạn. | Gia hạn. Thử lại không giúp được gì, nạp thêm cũng vậy. |
GRANT_TOKENS_EXHAUSTED | 403 | Số dư của grant nhỏ hơn giá của request này. Kèm remaining và required. | Nạp thêm. Thử lại không giúp được gì. |
GRANT_DAILY_LIMIT | 429 | Trần ngày của gói đã dùng hết. Kèm resetAt, remaining, required. | Chờ tới resetAt. |
GRANT_WEEKLY_LIMIT | 429 | Trần tuần của gói đã dùng hết. Kèm resetAt, remaining, required. | Chờ tới resetAt. |
CONCURRENT_CONFLICT | 429 | Hai request của chính bạn tranh nhau cùng một số dư và request này thua. | Thử lại ngay — lỗi này tự hết. |
resetAt theo chuẩn ISO 8601, và nó thay cho việc đọc mốc thời gian ra từ message.
Hãy dùng nó thay vì đoán khoảng chờ, và thay vì mời người dùng nâng gói cho một thứ
sẽ tự reset sau một giờ.
Ranh giới 403/429 là bản thô của cùng quyết định đó: 403 nghĩa là thử lại không
giúp được gì dù trông có vẻ nhất thời, còn 429 thì rồi sẽ được. Thứ status không
nói được là khi nào — lỗi hạn mức không kèm Retry-After (header đó đến từ
lớp rate limit, không phải lớp tính tiền). code mới là thứ tách một pha tranh nhau
đáng thử lại ngay khỏi một cái trần phải chờ, còn resetAt là thứ nói phải chờ bao
lâu.
FREE_DAILY_LIMIT và GRANT_INACTIVE có trong danh sách code nội bộ của nền tảng
nhưng không xuất hiện ở đây: một API key luôn tính tiền trên một grant nên không chạm
tới phần miễn phí, còn grant không còn hoạt động thì trả 403 kèm câu chữ, không có
code.
Năng lực phục vụ
code | Status | Nghĩa là gì | Cần làm gì |
|---|---|---|---|
QUEUE_FULL | 429 | Hàng đợi job bất đồng bộ đã chạm giới hạn độ sâu. | Giãn nhịp rồi gửi lại. Bạn không bị tính tiền — khoản trừ trước được hoàn ngay tại chỗ. |
STREAM_CONCURRENCY | 429 | Bạn đang giữ số stream mở tối đa cho phép. | Đóng bớt một stream, hoặc chờ một stream kết thúc. Không bị tính tiền. |
STREAM_BUSY | 503 | Mọi node đều khoẻ nhưng năng lực stream đã dùng hết. | Kèm theo "fallback": "generate", và đó là một chỉ dẫn thật — đường đi qua hàng đợi có năng lực riêng. Xem Một stream kết thúc thế nào. |
QUEUE_FULL chỉ đến với bạn từ POST /v1/tts; các route đồng bộ không xếp hàng
đợi.
Nhân bản giọng
Nhân bản giọng đã chuyển hẳn về Studio web. Các route tạo giọng —
POST /v1/voices, DELETE /v1/voices/{voiceId}, POST /v1/clone,
POST /v1/upload và POST /v1/prepare — nay chỉ trả một lỗi từ chối, trước khi
tính tiền và trước khi chạm tới GPU:
code | Status | Nghĩa là gì | Cần làm gì |
|---|---|---|---|
CLONE_WEB_ONLY | 410 | Giọng nhân bản được tạo tại vieneu.io/#/clone, không qua API. Kèm cloneUrl. | Tạo giọng một lần trên Studio. Phần còn lại của hệ tích hợp không đổi: giọng đó hiện trong GET /v1/voices khi bạn gửi kèm key, và id clone_… dùng được làm voiceId trên POST /v1/tts và /v1/tts/stream. |
Cố ý dùng 410 chứ không phải 404: các endpoint này từng tồn tại và từng có tài
liệu, nên status phải nói rằng chúng đã dời đi, chứ không phải bạn gõ sai đường
dẫn. Các code nhân bản cũ (CLONE_QUOTA_EXCEEDED, CLONE_MONTHLY_CAP,
CLONE_DAILY_CAP, CLONE_REF_*, CLONE_TRANSCRIPT_DENSITY,
CLONE_TOKENS_INSUFFICIENT…) thuộc về các route đó nên không còn tới được một
API key nữa — những giới hạn ấy vẫn còn nguyên trên Studio, nơi chúng hiện ra
ngay lúc bạn nhân bản.
Việc tạo tiếng bằng một giọng nhân bản không nằm trong nhóm này và không mang
code nhân bản nào. Một id clone_… không tồn tại hoặc không phải của bạn sẽ hỏng
như một lỗi 400 thường, có message và không có code — xem
Giọng nhân bản.
Những lỗi từ chối không có code
Route tương thích OpenAI
POST /v1/audio/speech không mang bất kỳ code nào ở trên, dù envelope của OpenAI có
sẵn một trường cho việc này: trường code của envelope luôn là null trên route
này. Thứ bạn nhận được thay vào đó là type, và nó thô hơn một cách có chủ đích. Lỗi
hạn mức được gán type theo status — insufficient_quota cho các 403, tức grant
đã cạn hoặc hết hạn, và rate_limit_exceeded cho các 429, tức trần đã dùng hết
hoặc một pha tranh nhau. Vậy nên "hết tiền" và "hãy chờ" thì sống sót qua phép chuyển
đổi; còn GRANT_EXPIRED với GRANT_TOKENS_EXHAUSTED thì không, và resetAt — thứ
cho biết phải chờ bao lâu — cũng không. Mọi lỗi phát sinh trước khi handler chạy —
key sai, một trường bị bộ validate từ chối — thì được gán type chỉ theo status, và
phép ánh xạ đó gộp 401 với 403 thành một authentication_error duy nhất.
API công khai không bao giờ trả 402
Không có 402 Payment Required ở bất cứ đâu trên /v1. Mọi đường đi liên quan tới
tiền đều chạy qua cùng một bước trừ token, và bước đó chỉ trả lời 403 hoặc 429.
Bản spec có kèm một ví dụ 402; còn máy chủ thì không gửi. Một client coi 402 là
"hết tiền" sẽ đọc 403 thật thành lỗi phân quyền và ngừng thử lại vì một lý do sai.
Hai phép kiểm văn bản
Trước khi tiêu bất kỳ token nào, văn bản gửi lên được kiểm hai thứ mà mô hình có đọc
thành tiếng cũng không ra nghĩa gì. Cả hai đều trả về 400 trơn, có message và
không có code.
| Thông báo bắt đầu bằng | Quy tắc |
|---|---|
Text appears to be in an unsupported language… | Hơn 34% số chữ cái nằm ngoài hệ chữ Latin. Dấu tiếng Việt và đ đều là Latin, nên luật này bắt tiếng Trung, Nhật, Hàn, chữ Kirin và tương tự — kể cả một đoạn vốn là tiếng Việt nhưng có một trích dẫn dài không phải chữ Latin. |
Text does not look like readable words… | Hoặc có một chuỗi hơn 30 chữ cái liền không ngắt, hoặc hơn 60% số token dạng từ (dài từ bốn chữ cái trở lên, và phải có ít nhất ba token như vậy) không chứa nguyên âm nào. |
Dấu câu cắt đứt một chuỗi chữ cái, nên một URL, một địa chỉ email hay một đường dẫn tệp được đo theo từng phần và lọt qua; còn gõ bừa bàn phím thì vẫn tính là một chuỗi liền và không lọt.
Hai phép kiểm này chỉ chạy trên ba route: POST /v1/tts, POST /v1/dialogue và
POST /v1/srt. POST /v1/tts/stream, POST /v1/audio/speech,
POST /v1/vapi/speech và POST /v1/dub không chạy phép kiểm nào — nên đoạn văn bản
mà route này từ chối, route kia vẫn tổng hợp và vẫn tính tiền. Đáng biết trước khi
bạn coi một 400 từ một route là bằng chứng rằng văn bản đó hỏng ở mọi nơi.
Số stream đồng thời
POST /v1/tts/stream từ chối mở stream mới khi bạn đang giữ quá nhiều stream:
| Tính theo | Trần | Khi vượt |
|---|---|---|
| Token grant đứng sau API key của bạn | 4 | 429, code: STREAM_CONCURRENCY |
| Một người dùng đã đăng nhập trong web app | 2 | 429, cùng code đó |
Suất được giữ trước khi động tới số dư, nên một stream bị từ chối không để lại dấu vết gì trên token của bạn. Hai chi tiết về cách đếm: đếm theo từng tiến trình backend, và khoá đếm là token grant, không phải API key — nên nhiều key phát hành trên cùng một grant sẽ chia nhau bốn suất chứ không phải mỗi key được bốn suất riêng.
X-Request-Id
Mọi response đều mang X-Request-Id. Đây chính là id mà log của chúng tôi đánh chỉ
mục theo: một giá trị phủ cả request tới API, lời gọi worker mà nó tạo ra, và mọi
dòng log do cả hai sinh ra.
Phần lớn body lỗi có lặp lại giá trị đó dưới tên traceId, nhưng bản đáng đọc là
cái header — chỉ nó mới luôn có mặt. traceId do bộ lọc exception đóng vào, nên
một body được ghi thẳng ra response thì không bao giờ có:
POST /v1/audio/speech, ở mọi lỗi. Chính handler tự ghi envelope của OpenAI, nên bộ lọc lẽ ra thêmtraceIdkhông hề chạy — mà hình dạng của OpenAI cũng chẳng có trường nào cho nó.POST /v1/vapi/speech, ở lỗi502khi worker không sinh ra audio nào. Body đó được ghi giữa chừng response và chỉ mangstatusCodevớimessage.
Hãy dẫn nó ra khi gửi yêu cầu hỗ trợ. Không có nó, "một request lỗi khoảng 3 giờ chiều" là một cuộc lục tìm; có nó thì chỉ là một lần tra.
Giá trị này luôn là của chúng tôi. Proxy ở biên đặt X-Request-ID cho mọi request
nó chuyển tiếp, vô điều kiện, nên id bạn gửi lên sẽ bị ghi đè chứ không được giữ
lại — hãy ghi log cái nhận về cạnh correlation id của bạn, đừng trông đợi id của
bạn sống sót.
Tổng hợp status
| Status | Ý nghĩa |
|---|---|
| 400 | Request sai — giọng không tồn tại, format không hợp lệ, văn bản bị bộ kiểm từ chối, một lỗi từ chối khi nhân bản giọng |
| 401 | API key thiếu, sai dạng hoặc đã bị thu hồi |
| 403 | Hết token, grant hết hạn, hoặc gói của bạn không bao gồm engine hay tính năng này |
| 413 | File tải lên quá 10 MB |
| 422 | Nội dung bị kiểm duyệt từ chối (chỉ khi aiRefine đang bật) |
| 429 | Rate limit, hạn mức token, độ sâu hàng đợi hoặc số stream đồng thời |
| 500 | Tổng hợp thất bại. Khoản đã trừ được hoàn tự động |
| 502 | Mọi worker của engine này đều lỗi. Đã hoàn tiền |
| 503 | Không có worker cho engine hoặc format được yêu cầu, hoặc năng lực stream đã đầy — thử lại sau chốc lát |
429 là status duy nhất có những nguyên nhân chẳng liên quan gì tới nhau đứng sau,
và header sẽ cho biết là nguyên nhân nào — xem
Giới hạn tần suất.