# Cắm thẳng vào app tương thích OpenAI

Source: https://docs.vieneu.io/vi/docs/integrations/openai-clients

Rất nhiều phần mềm đã nói được `POST /v1/audio/speech` và cho phép bạn trỏ nó tới
một base URL riêng. VieNeu có sẵn endpoint đó, nên những app này dùng được giọng
tiếng Việt mà không cần plugin, không cần adapter.

Trang này là bảng khai báo: với mỗi client, điền giá trị nào vào ô nào.

Phần VieNeu trong mọi mục dưới đây là chính xác. Phần client được viết theo kiểu
"giá trị này thuộc về loại ô nào", vì nhãn của các ô cài đặt thay đổi theo phiên
bản — **hãy đối chiếu tên trường với đúng bản bạn đang chạy**.

## Ba giá trị cần có {#the-three-values}

Client nào cũng cần đúng ba thứ giống nhau.

| Giá trị | Điền gì |
|---|---|
| Base URL | `https://api.vieneu.io/api/v1` (xem cái bẫy bên dưới) |
| API key | key VieNeu của bạn — bắt đầu bằng `vn_sk_` (thật) hoặc `vn_test_` (test) |
| Model | `tts-1`, `tts-1-hd`, `gpt-4o-mini-tts`, `vieneu` hoặc `vieneu-v4` → `v4`, engine duy nhất kể từ khi `v3` ngừng ngày 2026-09-24. Mọi tên khác đều được nhận rồi bỏ qua — **trừ** một tên `vieneu-…` không trỏ tới engine đang chạy, cái đó là 400. `vieneu-v3` giờ là một trong số đó. |

Ngoại lệ duy nhất ở dòng cuối lại chính là chỗ người dùng VieNeu dễ vấp nhất.
`vieneu-v5` hay `vieneu-turbo` **không** rơi về engine mặc định theo kiểu
`whatever-tts`; nó trả về 400 `invalid_request_error` kèm `param: "model"`, liệt
kê những tên engine thực sự tồn tại. Tiền tố đó được đọc như một lựa chọn engine
tường minh, mà âm thầm đọc nó bằng engine khác thì bạn bị tính phí theo mức bạn
chưa từng chọn. Nếu bạn gửi cả `engine` lẫn `model`, `engine` thắng.

`vieneu-v3` mới là trường hợp bạn dễ gặp thật nhất. Nó là lựa chọn hợp lệ cho
tới khi `v3` rút khỏi Cloud API ngày 2026-09-24, và giờ trả về 400 kèm
`Model 'vieneu-v3' is retired. Engine "v3" was retired on 2026-09-24. Use engine
"v4" and a voice from GET /v1/voices?engine=v4 — the two catalogues share no
ids.` Hãy đổi model sang `vieneu-v4` (hoặc `tts-1`) và kiểm lại giọng trong cùng
một lượt — một slug `vieneu-…` lưu từ danh mục `v3` cũ sẽ 400 ở `param: "voice"`
ngay sau đó.

Trường thứ tư, `voice`, không bắt buộc nhưng thường thì bạn vẫn muốn có: bỏ trống
thì server dùng giọng đang bật đầu tiên trên engine được chọn. Thứ bạn không làm
được là dùng lại tên của OpenAI — `alloy` / `nova` v.v. **không** được ánh xạ và
trả về 400. Lấy id thật từ `GET /api/v1/audio/voices` (bên dưới).

### Cái bẫy base URL {#the-base-url-trap}

Path là `/api/v1/audio/speech`. Cái `/api/v1` trông như bị lặp đó là thật —
server đặt một prefix `api` toàn cục *và* gắn API công khai ở `v1`. Nên giá trị
bạn gõ vào phụ thuộc vào phần client tự nối thêm:

| Nếu ô đó có nghĩa là… | Điền |
|---|---|
| "base URL của OpenAI — tôi tự nối `/audio/speech`" | `https://api.vieneu.io/api/v1` |
| "host — tôi tự nối `/v1/audio/speech`" | `https://api.vieneu.io/api` |
| "URL endpoint đầy đủ" | `https://api.vieneu.io/api/v1/audio/speech` |

Phần lớn client hiểu theo dòng đầu. Chọn sai thì luôn là **404, và luôn là
JSON** — nhưng theo cấu trúc lỗi nền tảng của VieNeu, chứ không phải cái phong bì
OpenAI mà endpoint này vẫn dùng ở mọi chỗ khác:

```json
{ "statusCode": 404, "message": "Cannot POST /api/v1/v1/audio/speech", "error": "Not Found" }
```

Đọc được hai điều từ đó:

- Thông báo `Cannot POST …` và việc **vắng mặt** lớp bọc
  `{"error":{"message","type","param","code"}}` mà mọi lỗi thật của
  `/v1/audio/speech` đều mang, có nghĩa là hình dạng URL sai chứ không phải key
  sai. Đừng đi lục API key vì lỗi này.
- Path nằm trong thông báo chính là URL client của bạn thực sự dựng ra. So nó với
  `/api/v1/audio/speech`, chỗ lệch sẽ cho biết bạn cần dòng nào ở bảng trên — ví
  dụ ở đây bị lặp `/v1`, nên client đó cần `https://api.vieneu.io/api`.

### Không có `/v1/models` {#there-is-no-v1models}

VieNeu chỉ phục vụ TTS. Không có route `/v1/models`, cũng không có
`/v1/chat/completions`. Hai hệ quả:

- **Nút "Test connection" / "Verify" nào dò `/models` sẽ báo thất bại**, dù việc
  tổng hợp giọng vẫn chạy. Cứ kệ nó và gửi một request thật.
- **Ô chọn model lấy dữ liệu từ `/models` sẽ trống rỗng.** Hãy gõ tay tên model.

Trong Open WebUI, SillyTavern và LobeChat, base URL này **chỉ** thuộc về phần cài
đặt provider audio/TTS — đừng bao giờ đặt vào ô endpoint OpenAI/LLM chung, đặt
vào đó là hỏng model chat.

### Chứng minh nó chạy trước đã {#prove-it-works-first}

Trước khi đụng vào bất kỳ client nào, hãy xác nhận base URL và key bằng curl. Gửi
`$VIENEU_API_KEY` — key `vn_sk_…` hoặc `vn_test_…` của chính bạn — và **không gửi
`voice`**, để ngoài hai giá trị đó ra không còn gì có thể hỏng:

```bash
curl https://api.vieneu.io/api/v1/audio/speech \
  -H "Authorization: Bearer $VIENEU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "tts-1", "input": "Xin chào, đây là VieNeu." }' \
  --output speech.mp3
```

Bỏ `voice` đi thì server tự chọn giọng đang bật đầu tiên trên engine được chọn,
và đó là thứ giữ cho bước này trung thực: một voice id bạn chưa kiểm chứng sẽ 400
ở `param: "voice"` và bạn hết đường phân biệt giọng sai với key sai hay base URL
sai. Chọn một giọng thật ở bước sau, khi bước này đã qua.

Nếu lệnh đó cho ra audio nghe được thì mọi thứ sau đây chỉ là chuyện cấu hình
client.

### Lấy voice id {#getting-voice-ids}

```bash
# Which engine will my requests land on? No API key needed.
curl https://api.vieneu.io/api/v1/engines

# Voice ids for that engine. API key IS required here.
curl "https://api.vieneu.io/api/v1/audio/voices?engine=v4" \
  -H "Authorization: Bearer $VIENEU_API_KEY"
```

`GET /api/v1/engines` trả về mỗi engine đang bật một mục, kèm `key`, `isDefault`,
`billingMultiplier` và `features`. Hãy dùng cái có `isDefault: true` — từ khi
`v3` ngừng ngày 2026-09-24 thì đó là mục duy nhất, `v4`.

:::warning Voice id lưu trước 2026-09-24 có thể đã chết
`audio/voices` giờ chỉ liệt kê `v4`, có lọc hay không cũng vậy, và `?engine=v3`
là 400. Nhưng id của danh mục V3 đã ngừng là slug `vieneu-…`, id của V4 là tên
hiển thị (`Ngọc Lan`), và hai bên gần như không chung không gian id nào — nên
một giọng mà client lưu từ danh sách chưa lọc trước ngày ngừng sẽ 400 ở
`param: "voice"` bây giờ. Hãy nạp lại ô chọn giọng từ lời gọi ở trên, và cứ
truyền `?engine=v4`: không tốn gì mà request lại tường minh.
:::

Việc khớp giọng không phân biệt hoa thường, nhưng **có** phân biệt dấu, và id của
V4 có dấu cách. Client nào slugify hoặc bỏ dấu trước khi gửi sẽ 400 ở mọi request.

Giờ chạy lại phép thử nhanh với một id lấy từ danh sách đó, chép đúng y như lúc
nhận về:

```bash
curl https://api.vieneu.io/api/v1/audio/speech \
  -H "Authorization: Bearer $VIENEU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "tts-1", "input": "Xin chào, đây là VieNeu.", "voice": "PASTE_AN_ID_HERE" }' \
  --output speech.mp3
```

Nếu lệnh curl đầu chạy được mà lệnh này 400 ở `param: "voice"` thì id là thứ duy
nhất đã thay đổi — hãy kiểm bộ lọc engine và dấu tiếng Việt trước đã.

:::note Giọng nhân bản không dùng được ở đây
Id `clone_…` bị `/v1/audio/speech` từ chối, kèm thông báo nêu tên hai route nhận
chúng: `POST /v1/tts` và `POST /v1/tts/stream`. Không client tương thích OpenAI
nào chạm tới được giọng nhân bản.
:::

## Open WebUI {#open-webui}

Phần cài đặt Audio/TTS, engine OpenAI:

| Trường | Giá trị |
|---|---|
| TTS engine / provider | tuỳ chọn tương thích OpenAI |
| API base URL | `https://api.vieneu.io/api/v1` |
| API key | `vn_sk_…` |
| Model | `tts-1` |
| Voice | một id thật từ `audio/voices`, ví dụ `Ngọc Lan` |

Ghi chú:

- Đặt cái này vào phần cài đặt **audio**, không phải phần model/connection.
- Ô voice phải nhận được chữ nhập tự do. Nếu bản bạn chạy chỉ đưa ra sáu cái tên
  của OpenAI thì cả sáu đều 400 — hãy kiểm trên bản của bạn.
- Response format: cứ để mặc định. Open WebUI phát mp3, cũng đúng là mặc định của
  VieNeu.

## SillyTavern {#sillytavern}

Extension TTS, provider tương thích OpenAI:

| Trường | Giá trị |
|---|---|
| Provider | tuỳ chọn OpenAI TTS |
| API / base URL | `https://api.vieneu.io/api/v1` |
| API key | `vn_sk_…` |
| Model | `tts-1` |
| Voice (theo từng nhân vật) | một id thật từ `audio/voices` |

Ghi chú:

- SillyTavern gán giọng cho từng nhân vật. Mọi nhân vật đều phải mang id VieNeu —
  một nhân vật còn để `alloy` sẽ hỏng trong khi số còn lại chạy, nhìn vào cứ
  tưởng tích hợp lúc được lúc không.
- Nếu extension chỉ cho một ô chọn giọng cố định thay vì ô nhập chữ thì nó không
  gọi được giọng VieNeu. Hãy kiểm trên bản của bạn.
- Nếu SillyTavern của bạn gọi TTS từ **trình duyệt** chứ không từ server của
  chính nó, xem [Client chạy phía trình duyệt](#browser-side-clients).

## LobeChat {#lobechat}

Phần cài đặt TTS / audio, provider OpenAI:

| Trường | Giá trị |
|---|---|
| OpenAI TTS base URL / proxy URL | `https://api.vieneu.io/api/v1` |
| API key | `vn_sk_…` |
| Model | `tts-1` |
| Voice | một id thật từ `audio/voices` |

Ghi chú:

- LobeChat giữ hai phần cài đặt riêng cho provider chat và provider TTS. URL này
  **chỉ** đặt vào phần **TTS**.
- LobeChat thường được triển khai theo kiểu trình duyệt gọi thẳng tới provider
  TTS — hãy xem [Client chạy phía trình duyệt](#browser-side-clients) trước khi
  gỡ bất cứ thứ gì khác.

## LiteLLM {#litellm}

Thêm VieNeu như một model trong `config.yaml`:

```yaml
model_list:
  - model_name: vieneu-tts
    litellm_params:
      model: openai/tts-1
      api_base: https://api.vieneu.io/api/v1
      api_key: os.environ/VIENEU_API_KEY
```

Rồi khi proxy đang chạy, gọi nó y hệt như gọi OpenAI:

```bash
curl http://localhost:4000/v1/audio/speech \
  -H "Authorization: Bearer $LITELLM_MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "vieneu-tts", "input": "Xin chào", "voice": "Ngọc Lan" }' \
  --output speech.mp3
```

Ghi chú:

- **`LITELLM_MASTER_KEY` không phải key VieNeu của bạn.** Đó là credential proxy
  của riêng LiteLLM, là bất cứ thứ gì bạn đặt lúc khởi động proxy — cổng
  (`:4000` ở trên) cũng là của LiteLLM. Key `vn_sk_…` của bạn chỉ xuất hiện trong
  `config.yaml`, lấy qua `os.environ/VIENEU_API_KEY`; chính proxy mới là bên gắn
  nó vào lời gọi lên upstream.
- Tiền tố `openai/` trong `model` bảo LiteLLM chuyển tiếp request theo hình dạng
  của OpenAI, đúng thứ VieNeu trả lời được. Cái tên đứng sau nó (`tts-1`) mới là
  thứ đi tới VieNeu.
- Các giá trị VieNeu ở trên là chính xác; những key LiteLLM bao quanh chúng là
  hình dạng `model_list` chuẩn của nó — hãy đối chiếu với phiên bản LiteLLM bạn
  dùng.
- **Đừng bật bất kỳ tính năng làm giàu request nào có thêm field vào body.**
  VieNeu từ chối field lạ bằng 400 chứ không bỏ qua chúng — xem
  [400](#400-invalid_request_error).

## LiveKit Agents {#livekit-agents}

Plugin OpenAI nhận một base URL và một key:

```python
from livekit.plugins import openai

tts = openai.TTS(
    model="vieneu-v4",
    voice="Ngọc Lan",
    base_url="https://api.vieneu.io/api/v1",
    api_key="vn_sk_...",
)
```

Ghi chú:

- **Hãy ghim `vieneu-v4`, hoặc bỏ trống `model`.** Cả hai đều rơi vào `v4`,
  engine duy nhất kể từ khi `v3` ngừng ngày 2026-09-24. `vieneu-v3` — thứ mà
  endpoint này từng nhận kèm streaming, trả 200 rồi để v3 nhả hết chunk ra một
  lượt ở cuối mà không có lỗi nào báo cho bạn biết — giờ là 400 kèm
  `param: "model"` nói rằng engine đã ngừng.
- `GET /v1/engines` trả về hệ số tính phí đang chạy; xem
  [Engine](../cloud-api/overview#engines).
- Muốn streaming, VieNeu cần **cả hai**: `response_format: "pcm"` và
  `stream_format`. `response_format` mặc định là mp3, nên chỉ đặt mỗi
  `stream_format` là 400. Plugin có gửi cái nào trong hai cái đó không, và có cho
  bạn thêm vào không, chính là thứ cần kiểm trên bản bạn chạy. Thiếu chúng thì
  lời gọi vẫn chạy — chỉ là nó trả về một file mp3 hoàn chỉnh thay vì một luồng.
- `pcm` không có header và mặc định **24000 Hz** trên route này (tần số gốc của
  engine là 48000). Hãy đọc tần số thật từ header phản hồi `X-Sample-Rate` rồi
  cấu hình pipeline cho khớp, không thì audio phát sai cao độ.

## Pipecat {#pipecat}

```python
import os
from pipecat.services.openai.tts import OpenAITTSService

tts = OpenAITTSService(
    api_key=os.environ["VIENEU_API_KEY"],
    base_url="https://api.vieneu.io/api/v1",
    model="vieneu-v4",
    voice="Ngọc Lan",
)
```

Ghi chú:

- Đường dẫn import cho service OpenAI TTS của Pipecat đã đổi chỗ qua các bản phát
  hành — hãy kiểm bản của bạn. Thứ quan trọng là các tham số constructor và những
  giá trị ở trên.
- Ba quy tắc streaming giống hệt LiveKit vẫn áp dụng: ghim `vieneu-v4`, gửi
  `response_format: "pcm"` **và** `stream_format`, và lấy tần số từ
  `X-Sample-Rate` thay vì mặc định coi là 48 kHz.
- Nếu phiên bản của bạn hard-code một `response_format` mà VieNeu không nhận
  (`aac`, `flac`) thì request 400 kèm tên field đó. Các giá trị được nhận là
  `wav`, `mp3`, `opus`, `pcm` và `ulaw`.

## Client chạy phía trình duyệt {#browser-side-clients}

Trên production, chính sách CORS của VieNeu chỉ cho phép các origin của chính
VieNeu. Client nào gọi `/v1/audio/speech` **từ trang web** chứ không từ server
của chính nó sẽ chết ngay ở bước preflight, không có body phản hồi nào giải thích.

Một bản triển khai LobeChat hay SillyTavern cụ thể có gọi TTS ở phía server hay
không là chuyện tuỳ từng bản — hãy kiểm trên bản của bạn. Nếu nó chạy phía trình
duyệt, hãy dựng một proxy của riêng bạn đứng trước (LiteLLM hợp cho việc này) rồi
trỏ client vào đó.

JavaScript trong trình duyệt đọc được `X-Request-Id`, `X-Sample-Rate`,
`X-Output-Format`, các header `X-RateLimit-*` và `Retry-After`; mọi thứ còn lại
bị trình duyệt che. (`X-Stream-Format` cũng được mở qua CORS, nhưng đừng viết code
đọc nó ở đây — chỉ route gốc `POST /v1/tts/stream` mới gửi nó. Trên route này
phần đóng khung đã được bóc sẵn cho bạn rồi, nên header đó luôn vắng mặt.)

## Gỡ rối theo triệu chứng {#troubleshooting-by-symptom}

### Không có audio {#no-audio}

Đi lần lượt xuống danh sách — mỗi mục một nguyên nhân khác nhau.

- **Tệp lưu về chứa JSON.** Trên đường stream thô, một lượt sinh không ra được gì
  sẽ trả **502** kèm body lỗi ở đúng chỗ lẽ ra là audio. Client nào ghi thẳng
  body phản hồi vào tệp `.pcm` sẽ có JSON nằm trong đó. Hãy xem những byte đầu
  tiên của tệp.
- **Tệp bị tải về thay vì phát.** Phản hồi đồng bộ mang
  `Content-Disposition: attachment`. Client nào fetch phần body thì không bị ảnh
  hưởng; client nào điều hướng thẳng tới URL sẽ nhận một lượt tải về.
- **Audio phát sai cao độ hoặc sai tốc độ.** `pcm` và `ulaw` không có header —
  chuỗi byte không mang theo sample rate. Ở đây `pcm` là 24000 Hz trừ khi bạn yêu
  cầu khác; `ulaw` luôn là 8000. Hãy đọc `X-Sample-Rate` thay vì đoán.
- **Trình phát từ chối một tệp `.mp3`.** Nếu bạn bỏ trống `response_format` mà
  worker đang phục vụ bạn không mã hoá được mp3, VieNeu lùi về **byte WAV** thay
  vì báo lỗi. Hãy đọc `X-Output-Format` để biết bạn thực sự nhận được gì. (Nếu
  bạn đã yêu cầu mp3 tường minh thì thay vào đó bạn sẽ nhận 503 kèm tên định dạng
  — một lựa chọn tường minh không bao giờ bị âm thầm thay thế.)

### 400 `invalid_request_error` {#400-invalid_request_error}

Với hầu hết các trường hợp này, field `param` trong body lỗi nêu đích danh thủ
phạm — trừ đúng một ngoại lệ, và đó là nguyên nhân đầu tiên trong danh sách dưới
đây. Các nguyên nhân thường gặp:

- **Một field lạ trong body.** VieNeu từ chối những field nó không khai báo chứ
  không bỏ qua chúng. Bộ field được nhận đúng bằng: `model`, `input`, `voice`,
  `response_format`, `sample_rate`, `speed`, `instructions`, `stream_format`,
  `emotion`, `aiRefine`, `engine`. Lưu ý là **không có boolean `stream`** —
  client nào gửi `stream: true` (như với chat completions) sẽ nhận 400. `user`,
  `language` hay bất kỳ phần mở rộng riêng nào của client cũng vậy. Đây là lý do
  phổ biến nhất khiến một client chạy tốt với OpenAI lại hỏng với VieNeu: hãy soi
  đúng cái body mà nó thực sự gửi đi.

  **Với trường hợp này, hãy đọc `message` chứ không phải `param`.** Lỗi bị chặn ở
  bộ validator của request chứ không ở handler, nên `param` trả về đúng chuỗi
  `"property"` chứ không bao giờ là tên field vi phạm. Tên nằm trong message:
  `property stream should not exist`. Mỗi request chỉ báo field vi phạm đầu tiên,
  nên nếu client của bạn thêm mấy field một lúc thì cứ sửa rồi thử lại cho tới
  khi qua.
- **Một tên model `vieneu-…` không nhận ra.** `param: "model"`. Các tên model lạ
  khác thì bị bỏ qua; riêng tiền tố này thì không.
- **Một tên giọng của OpenAI.** `alloy`, `echo`, `fable`, `onyx`, `nova`,
  `shimmer` không được ánh xạ. `param: "voice"`.
- **Một voice id nhân bản.** `clone_…` chỉ chạy trên `POST /v1/tts` và
  `POST /v1/tts/stream`.
- **`stream_format` đi với một định dạng không stream được.** Chỉ `pcm` và `ulaw`
  stream được, mà `response_format` lại mặc định là **mp3** — nên chỉ đặt mỗi
  `stream_format` thì luôn luôn 400.
- **`aac` hoặc `flac`.** Định dạng có thật của OpenAI nhưng VieNeu không mã hoá
  được; thông báo nói thẳng ra điều đó.
- **Một `sample_rate` mâu thuẫn với định dạng.** `opus` luôn là 48000 và `ulaw`
  luôn là 8000; một tần số xung đột sẽ bị từ chối chứ không bị ghi đè. Các tần số
  hợp lệ là 8000, 16000, 22050, 24000, 44100, 48000.
- **`speed` nằm ngoài 0.25–4.0.** Nằm trong dải đó thì nó bị kẹp về 0.5–2.0 và
  không bao giờ báo lỗi; nằm ngoài thì bộ kiểm tra từ chối.
- **Key test vượt quá 100 từ.** Key `vn_test_` chặn mỗi request ở 100 từ tách
  bằng khoảng trắng. Thông báo nêu đúng số từ thực tế của bạn.

### 401 `authentication_error` {#401-authentication_error}

- **`Invalid API key format.`** — key phải bắt đầu đúng bằng `vn_sk_` hoặc
  `vn_test_`. Chỗ này được kiểm trước mọi thao tác tra cứu, nên những key giữ chỗ
  mà vài client kèm sẵn hoặc bắt phải điền (`sk-...`, `none`, `ollama`) đều chết
  ở đây. Nếu client không chịu lưu khi ô key để trống thì nó cần một key VieNeu
  thật.
- **`API key required.`** dù bạn đã điền — một bearer token có chứa dấu chấm sẽ
  bị coi là JWT và bị bỏ qua với tư cách API key. Key VieNeu thật là hex và không
  bao giờ có dấu chấm, nên lỗi này nghĩa là có thứ gì đó đã bọc hoặc thay mất key
  của bạn.
- **`Invalid or revoked API key.`** — một key đã thu hồi thì không phân biệt được
  với một key lạ.

Key đặt ở `Authorization: Bearer <key>` hoặc `X-API-Key: <key>`; cả hai đều được.
Nếu client của bạn gửi cả hai với giá trị khác nhau thì `X-API-Key` âm thầm thắng.

### 403 — thường là "hết token" {#403-out-of-credit}

**Ở đây hết token là 403, không phải 402.** Không chỗ nào trong API công khai
phát ra 402, nên một client chỉ canh mã đó sẽ chẳng bao giờ biết mình đã hết
tiền. Hãy canh 403. Có ba thông báo, đều mang type `insufficient_quota`:

- `Insufficient tokens. You have <n> tokens remaining but this request costs <m> tokens.`
  — đây là lúc một grant hết theo cách bình thường, và là lỗi đầu tiên bạn gặp
  khi hạn mức dùng thử cạn.
- `Your API token package has expired. Please renew your Developer plan.` —
  chuyện thời hạn, không phải chuyện tiêu thụ. Mua thêm token không chữa được;
  gia hạn mới chữa được.
- `API token grant is not active. Check your Developer plan status.`

Hai lỗi 403 khác thì *không* liên quan tới token, và field `type` là thứ phân
biệt chúng:

- `Your plan does not include engine "v4". Upgrade your Developer plan to
  use it.` — mang type `invalid_request_error`, `param: "engine"`. Chỉ phát sinh
  khi bạn ghim engine tường minh mà gói không bao gồm nó; với `v4` là engine duy
  nhất từ 2026-09-24 thì hiếm khi bạn gặp — bỏ `model`/`engine` đi thì engine
  mặc định chạy được.
- `This feature is not available yet.` — mang type `authentication_error`, đến từ
  một cổng tính năng chứ không phải từ khâu tính phí.

Vậy nên đừng rẽ nhánh chỉ theo `type` để nhận ra "hết token", mà cũng đừng rẽ
nhánh chỉ theo 403. Hai thứ đi cùng nhau thì không còn nhập nhằng.

Trần theo ngày hoặc theo tuần là **429**, không phải 403 — xem bên dưới.

### 429 `rate_limit_exceeded` {#429-rate_limit_exceeded}

Đọc header để phân biệt nguồn gốc:

| Header có mặt | Nguồn | Đếm vào |
|---|---|---|
| `Retry-After` + `X-RateLimit-*` | bộ throttle của VieNeu trên các route tổng hợp giọng — 300 request/phút **theo mặc định** | API key của bạn |
| không có cái nào, body không phải JSON | proxy ở biên — 3000 request/phút | **IP nguồn** của bạn, dùng chung với mọi người đứng sau nó |
| JSON, message có chứa `Resets at <ISO>` | trần ngày hoặc trần tuần của token grant | grant của bạn |

Cả ba trường hợp đều phải giãn nhịp. Trường hợp giữa đáng để biết: nếu client của
bạn đi ra qua một đường egress dùng chung hoặc có NAT, bạn có thể chạm phải một
giới hạn mà chỉnh bao nhiêu ở phía mình cũng không chữa được.

**Đừng hard-code số 300.** Đó là một cài đặt theo bản triển khai
(`PUBLIC_API_SYNTHESIS_RPM`), không phải hằng số, và các trang khác nêu con số
khác vì chúng được viết dựa trên những bản triển khai khác. Phần bền vững là cột
đầu của dòng đó: hãy đọc hạn mức đang chạy của bạn từ `X-RateLimit-Limit` và
`X-RateLimit-Remaining` trên bất kỳ phản hồi thành công nào.

Dòng thứ ba là dòng người ta hay nhầm thành dòng đầu. Trần ngày hoặc trần tuần
của grant là một lỗi 429 mà message kết thúc bằng `Resets at <ISO timestamp>` —
chậm lại cỡ nào cũng không gỡ được nó trước thời điểm đó. Còn hết sạch token thì
là [403](#403-out-of-credit), không bao giờ là 402.

### 503 — lỗi ở đội máy, không phải ở request của bạn {#503--the-fleet-not-your-request}

Không có gì trong request của bạn cần sửa; cứ thử lại sau chốc lát. Bốn nguyên
nhân:

- **Không có worker nào cho engine bạn đã ghim.** `No V4 TTS worker is available
  right now. Please try again shortly.` (đường streaming thì nói `…available for
  streaming right now`.) Chỉ phát sinh với engine **không phải mặc định**: một
  request đi theo engine mặc định sẽ lùi về worker đã cấu hình chứ không trả 503,
  nên bỏ `model`/`engine` là cách chữa cháy hợp lệ ở đây.
- **Không có giọng nào đang bật trên engine đó**, khi bạn bỏ trống `voice`. `No
  voice is currently available on engine "v4". Pass an explicit voiceId from GET
  /v1/voices.` Lỗi này về dưới dạng 503 nhưng mang type `invalid_request_error`
  kèm `param: "voice"` — một vấn đề của danh mục giọng lại đeo cái nhãn của lỗi
  request. Nó phát sinh trước khâu tính phí, nên bạn chưa bị trừ gì.
- **Một định dạng bạn yêu cầu tường minh mà worker không mã hoá được.** Nếu bạn
  bỏ trống `response_format` thì thay vào đó bạn đã nhận được byte WAV; một lựa
  chọn tường minh không bao giờ bị âm thầm thay thế. Xem
  [Không có audio](#no-audio).
- **Một `sample_rate` worker không tạo ra được**, trên đường streaming. Thông báo
  nêu cả thứ bạn yêu cầu lẫn thứ worker đưa ra được.

Mọi trường hợp trong số này mà đã đi tới khâu tính phí đều được hoàn tự động —
bạn không bị tính tiền cho audio bạn không nhận được.

### Luồng dừng giữa chừng câu {#the-stream-stops-mid-sentence}

- **Với SSE, hãy đợi sự kiện kết thúc.** Không có dấu hiệu `data: [DONE]` nào cả.
  Luôn luôn có đúng một `speech.audio.done` (audio trọn vẹn, mang theo `usage`)
  hoặc `speech.audio.error` (thì không trọn vẹn) đi tới. Nếu chẳng cái nào tới
  thì luồng đã bị cắt cụt — hãy bỏ đoạn audio đi; request được hoàn tự động.
- **Trên đường stream thô thì không có tín hiệu như vậy.** Một luồng bị cắt cụt
  không tài nào phân biệt được với một luồng vốn dĩ ngắn. Nếu bạn cần bằng chứng
  về sự trọn vẹn, hãy dùng `stream_format: "sse"` hoặc route gốc
  [`POST /v1/tts/stream`](../cloud-api/streaming).
- **Nó "treo", rồi đổ ra tất cả một lượt.** Trước đây đó là `vieneu-v3` đi kèm
  `stream_format`: endpoint này nhận, trả 200, rồi v3 nhả hết chunk ra một lượt
  ở cuối. Từ khi `v3` ngừng ngày 2026-09-24, request đó là 400, nên nếu bạn vẫn
  thấy kiểu này thì việc đệm đang xảy ra ở một proxy hay client của chính bạn,
  không phải ở engine. Hãy ghim `vieneu-v4` và kiểm xem có gì nằm giữa bạn và
  API.
- **Một lời gọi không streaming bị timeout với text dài.** Trên endpoint này
  `input` **không có trần độ dài** — request chỉ đơn giản là chặn đó suốt trọn
  thời gian tổng hợp, rồi cái timeout HTTP của chính client bỏ cuộc trước. Trần
  thực sự là body request 2 MB và read timeout 600 giây ở proxy. Với text dài,
  hãy dùng đường bất đồng bộ `POST /api/v1/tts` + `GET /api/v1/tts/{jobId}` thay
  cho nó.

Mọi phản hồi đều mang `X-Request-Id`. Hãy dẫn nó ra khi gửi yêu cầu hỗ trợ.

## Xem thêm {#see-also}

- [Tổng quan tích hợp](./overview.md) — chọn đường nào nếu công cụ của bạn không
  có trong trang này.
- [Endpoint TTS tương thích OpenAI](../cloud-api/openai-compatible) — hợp đồng
  đầy đủ theo từng field cho `/v1/audio/speech`: mọi field được nhận, quy tắc về
  định dạng và sample rate, cùng danh sách lỗi trọn vẹn.
- [Streaming](../cloud-api/streaming) — so sánh hai endpoint streaming, và ngữ
  nghĩa của sự kiện kết thúc đã tóm ở trên.
- [Hạn mức và request id](../cloud-api/overview#rate-limits-and-request-ids) —
  các header, và vì sao hạn mức đếm theo từng key.
- [API reference](/api-reference) — sinh từ chính đặc tả OpenAPI của server.
