# Aurora API

Base URL: `https://api.madgodinc.net`
Auth: `Authorization: Bearer <token>` (or `X-Aurora-Token: <token>`)
Bodies are JSON. Maximum request body is 1 MB. A larger body is refused with `400`
(the limit is enforced before the token is read, so nothing bigger is ever buffered).

## Accounts

### POST /auth/register
`{"login": "...", "password": "..."}` → `{"token", "login"}`
Login is 3 to 32 chars from letters, digits, `.`, `_`, `-`. Password at least 8 chars.
Capped at 30 new accounts per hour service wide; repeated failures block your IP for a while.

### POST /auth/login
`{"login", "password"}` → `{"token", ...}`
Unknown login and wrong password give the same answer on purpose.

### POST /auth/telegram
Payload from the Telegram Login Widget. The signature is verified with the bot key and must
be under an hour old. Used by the website; you do not need it.

### POST /auth/logout
Revokes the token you present. Tokens otherwise expire after 30 days of disuse, counted
from last use rather than from issue.

### GET /me
Returns quota view plus a `pressure` object:

```json
{"login": "...", "used_seconds": 12.4,
 "pressure": {"level": 0, "state": "свободно", "note": "",
              "gpu_hour": 12.4, "gpu_6h": 30.1, "gap": 8, "rest_left": 0}}
```

### GET /persona
Aurora's system text, assembled from her identity shelf in the memory service. This is the
same text that governs her answers on the website.

## Conversation

### POST /v1/chat/completions
OpenAI shaped, proxied to a local llama.cpp. `POST /v1/messages` is the same handler.

```json
{"messages": [{"role": "system", "content": "..."},
              {"role": "user", "content": "..."}],
 "max_tokens": 700,
 "stream": false,
 "chat_template_kwargs": {"enable_thinking": false}}
```

Notes that matter:

- The model emits reasoning into `reasoning_content` before `content`. With a small
  `max_tokens` it can spend the whole budget thinking and return an empty `content`, which
  looks like a failure but is not. Pass `enable_thinking: false` unless you want it; the
  website disables it, and a one line answer drops from about 7 seconds to about 1.
- `max_tokens` is clamped server side to 2000 regardless of what you send.
- Streaming is server sent events. Real timings arrive in a `timings` field in the second
  to last chunk, before `[DONE]`.
- At most 3 requests to the model run at once across all users; beyond that you get 503
  with `retry_after`.

## Memory

Two tiers. Shared shelves hold Aurora's identity and skills and cannot be written from a
chat at all: the write path can only address the account's own two shelves. Personal
shelves belong to one account and are invisible to every other account.

### POST /memory/save
`{"text": "...", "shelf": "profile" | "sandbox"}` → `{"ok": true}`
Stores verbatim. This is what the phrase "запомни ..." does in the web chat.

### POST /memory/recall
`{"query": "...", "limit": 6}` → `{"items": [{"shelf": "...", "text": "..."}]}`
Searches only your own shelves, by meaning. Shared shelves are deliberately excluded: when
they were included, Aurora's own rules crowded out the user's facts and personal memory
never surfaced.

### POST /memory/distill
`{"chat_id": N}` → `{"saved": [...], "skipped": [...]}`
Reads a stored conversation, asks the model which facts about the person are worth keeping,
drops near duplicates against what is already stored, and writes the rest. Free: this call
is not charged to your usage.

## Conversations

- `GET /chats` → `{"chats": [{"id", "title", "created_at", "updated_at", "n"}]}`
- `POST /chats` `{"title"?}` → `{"id", "title", "created_at"}`
- `GET /chats/<id>` → `{"messages": [{"role", "content", "ts"}]}`
- `POST /chats/<id>/messages` `{"role": "user"|"assistant", "content": "..."}` → `{"ok"}`
- `DELETE /chats/<id>` → `{"ok"}`

The title is taken from the first thing the person says. Another account's conversation
returns 404 rather than 403, so ownership is not leaked.

## Limits

Metered in real GPU seconds taken from the model's own timings, not in messages. Counting
by tokens was tried and abandoned: with a warm prompt cache it overstated a twenty turn
conversation by a factor of about seventeen.

| Step | Trigger | What happens |
|---|---|---|
| 0 free | under 600 s of GPU in an hour | nothing |
| 1 notice | 600 s in an hour | a warning, requests still pass |
| 2 slowed | 1200 s in an hour, or 12 requests in a minute | 15 s minimum gap between requests, answers capped at 400 tokens |
| 3 rest | 5400 s across six hours | requests refused for two hours, `retry_after` in seconds |

Answers containing code are charged at 0.4 of their real cost, in proportion to how much of
the answer is inside code fences. A mixed answer is charged in between, so wrapping a
sentence in backticks buys nothing.

For scale, measured on this machine: a one line answer costs about 1.4 s, an ordinary
paragraph about 3 s, a long detailed answer about 20 s. Three hours of steady conversation
sits well below the first step.

## Errors

`400` malformed, `401` missing or expired token, `404` unknown route or not yours,
`429` rate or pressure limit with `retry_after`, `502` model
unavailable, `503` all model slots busy.

Error bodies carry a human sentence in `error` and never internal paths or addresses.
