Mirai / docs /API.md
Tamek's picture
Update: Reformat
ae99ec3
|
Raw History Blame Contribute Delete
7.44 kB
# Mirai Backend API
Canonical contract of the FastAPI service in `app/`. The web client never calls it directly: the
Cloudflare Worker in the `mirai-chat` repo (`src/worker.js`) sits in front, validates and clamps
requests, and adds the API key. Its browser-facing differences are listed in that repo's
`SETUP_SERVER.md`.
When you change a field, a limit or a status code here, update that file and the Worker constants
in the same change (see [Limits](#limits-and-where-they-are-enforced)).
## Authentication
| Env var | Effect |
| --- | --- |
| `SPACE_API_KEY` | When set, `/chat` and `/tts` require it as `Authorization: Bearer <key>` or `X-Api-Key: <key>`. Mismatch gives `401`. |
| `REQUIRE_API_KEY=1` | Fail closed: if `SPACE_API_KEY` is missing, every `/chat` and `/tts` request gets `503 Server is not configured`. **Set this in production.** Without it and without a key, both endpoints are open to anyone who knows the Space URL. |
`/health` never requires a key. CORS allows any origin for `GET` and `POST` (no credentials).
`/docs`, `/redoc` and `/openapi.json` are off unless `ENABLE_DOCS=1`.
## `GET /health`
```json
{ "status": "ok", "model": "mirai_model-q4_k_m.gguf" }
```
| HTTP | `status` | Meaning |
| --- | --- | --- |
| 200 | `ok` | Model loaded and ready. |
| 503 + `Retry-After: 15` | `loading` | Model still loading in the background thread. |
| 503 + `Retry-After: 15` | `error` | Model failed to load (`model` may be empty; details are in the logs). |
## `POST /chat`
Runs the LLM and returns the reply. Unknown request fields are ignored (the server never trusts
client-supplied instructions).
### Request
| Field | Type | Notes |
| --- | --- | --- |
| `message` | string, 1-2000 chars | Required. Special chat-template tokens and `User:`/`Mirai:` prefixes are stripped. |
| `history` | `{role: "user" \| "assistant", content: string (max 2000)}[]`, max 24 items | Optional. Only the last 12 are considered, and old turns are dropped to fit `PROMPT_TOKEN_BUDGET`. |
| `max_tokens` (alias `maxTokens`) | int | Optional. Clamped to **96-192**, default 128. |
| `max_words` (alias `maxWords`) | int | Optional. Clamped to **10-60**, default 40. Becomes the length hint in the persona prompt. |
| `tts` | bool, default `true` | `false` skips speech synthesis so the text returns as soon as the LLM finishes; fetch audio from `/tts`. |
### Response `200`
```json
{
"text": "Chào bạn! ...",
"reply": "Chào bạn! ...",
"emotion": "happy",
"audio": null,
"audioFormat": "mp3",
"audioError": null,
"lipSync": null,
"sources": [],
"meta": { "model": "...", "finish_reason": "stop", "truncated": false, "history_used": 4,
"prompt_tokens": 512, "completion_tokens": 40, "latency_ms": 9000, "queue_ms": 0,
"llm_ms": 8800, "first_token_ms": 4200, "tts_ms": 0 }
}
```
| Field | Notes |
| --- | --- |
| `text`, `reply` | Same string. `reply` is kept for older clients; new code should read `text`. |
| `emotion` | `happy`, `sad`, `angry`, `surprised`, `relaxed`, `neutral`, `shy`, `thinking`, or `null` when there is no clear signal (the client then leaves the face alone). A `[smile]`-style tag emitted by the model wins, otherwise keywords decide. Tags are removed from `text`. |
| `audio` | Base64 MP3 or `null` (when `tts: false` or synthesis failed). |
| `audioError` | `"tts_unavailable"` when synthesis failed, else `null`. TTS failure never fails the request. |
| `lipSync` | See [Lip-sync payload](#lip-sync-payload), or `null`. |
| `sources` | Always `[]` for now. Reserved. |
| `meta.truncated` | `true` when generation hit the token limit or deadline; the text is cut back to the last whole sentence. |
### Errors
Error bodies are `{ "detail": "<message>" }` (FastAPI). Through the Worker they become `{ "error": "<message>" }`.
| HTTP | When | Retry-After |
| --- | --- | --- |
| 400 | Message empty after trimming/sanitizing. | |
| 401 | Missing or wrong API key. | |
| 413 | Prompt does not fit the context window because the **message** is too long. | |
| 422 | Request fails schema validation (length limits, types). | |
| 500 | Model failed to load, generation failed, or the **server prompt** alone exceeds `PROMPT_TOKEN_BUDGET` (a config problem, not the user's fault). | |
| 503 | Model still loading, or queue full (one request generating plus one waiting). | 15 / 10 |
## `POST /tts`
Request `{ "text": string, 1-1000 chars }`. Response:
```json
{ "audio": "<base64 mp3>", "audioFormat": "mp3", "audioError": null, "lipSync": { } }
```
On synthesis failure the status is still `200` with `audio: null` and `audioError: "tts_unavailable"`.
`503` (+ `Retry-After: 5`) when 4 syntheses are already running. Voice is `vi-VN-HoaiMyNeural`;
markup, URLs, `[tags]` and emoji are removed from the text before synthesis.
## Lip-sync payload
```json
{
"metadata": { "duration": 3.42, "text": "...", "source": "word", "version": 2 },
"mouthCues": [
{ "start": 0.0, "end": 0.12, "value": { "A": 1.0 } },
{ "start": 0.12, "end": 0.2, "value": {} }
]
}
```
- `source` is `word`, `sentence` or `estimated`, depending on the timing marks edge-tts provided.
The client only rescales cue time for non-`word` sources.
- `value` maps vowel channels `A`, `I`, `U`, `E`, `O` to 0-1 weights (summing to at most 1). An empty object means mouth closed.
- Cues are non-overlapping and ordered by `start`; times are seconds from the start of the audio.
## Limits and where they are enforced
The same limit appears in several layers. Change all of them together.
| Limit | Worker (`mirai-chat/src/worker.js`) | Backend | Client (`chatApi.js`) |
| --- | --- | --- | --- |
| `message` length | 1000 (silently truncated) | 2000 (422) | |
| `history` items / chars each | 12 / 500 (truncated) | 24 / 2000 (422), uses last 12 | 12 (`HISTORY_MESSAGES`) |
| `max_tokens` | cap 192, default 128 | clamp 96-192, default 128 | default 128 (`VITE_MIRAI_MAX_TOKENS`) |
| `max_words` | cap 60, default 40 | clamp 10-60, default 40 | default 40 (`VITE_MIRAI_MAX_WORDS`) |
| `/tts` text | 1000 | 1000 | |
| Request body | 16 KB | | |
| Timeouts | `/chat` upstream 110 s, `/tts` 30 s, `/health` 10 s | generation deadline `GEN_DEADLINE_SECONDS` (80), TTS 15 s | `/chat` 120 s, `/tts` 30 s, `/health` 5 s |
Timeouts must stay ordered: backend < Worker < client, so the user gets a meaningful `504` instead of an abort.
## Configuration (backend env vars)
| Var | Default | Purpose |
| --- | --- | --- |
| `MODEL_REPO`, `MODEL_FILE` | set in `Dockerfile` | GGUF model to download from Hugging Face. |
| `CHAT_FORMAT` | `llama-3` | llama-cpp chat template; `auto` uses the one embedded in the GGUF. Use `chatml` for the Qwen3 model from `mirai-train`. |
| `EMOTION_TAGS` | off | `1` adds the emotion-tag rule to the system prompt. Needed for models trained by `mirai-train`; leave off for the old Llama 3 model. |
| `N_CTX` | 2048 | Context window. |
| `PROMPT_TOKEN_BUDGET` | 900 | Extra cap on prompt tokens (slow CPU prefill). |
| `N_THREADS`, `N_THREADS_BATCH`, `N_BATCH` | 2, = `N_THREADS`, 256 | CPU tuning. |
| `TEMPERATURE` | 0.5 | Sampling temperature. |
| `GEN_DEADLINE_SECONDS` | 80 | Hard stop for one generation. |
| `KNOWLEDGE_HIDE_PREFIXES` | `Tên thật` | Lines of `app/data/knowledge_data.txt` starting with these (comma separated) are kept out of the prompt. |
| `SPACE_API_KEY`, `REQUIRE_API_KEY`, `ENABLE_DOCS` | unset | See above. |