# ApiraCloud API

Base URL: `https://<ваш-домен>/v1`. Локальный сервер по умолчанию: `http://localhost:8787/v1`.
Ключ клиента создаётся в кабинете → API-ключи. Доступы к внешним маршрутам остаются на сервере.

## Доступные методы

- `GET /v1/models` — публичный каталог: ID, название, описание, контекст, максимум ответа, возможности и цены в рублях за миллион токенов. Выбирайте `id` из ответа.
- `POST /v1/chat/completions` — текстовый Chat Completions, с обычным ответом или SSE.
- `GET /healthz` — работоспособность процесса; это не проверка всех внешних поставщиков.

`/api/client/*` и `/api/admin/*` — API кабинета с cookie-сессией; Bearer-ключ для них не подходит.
Responses, embeddings, загрузка файлов, генерация изображений, видео и аудио пока не реализованы.
Метки модальностей в каталоге описывают возможности модели, а не доступность этих операций в шлюзе.

## Первый запрос (curl)

```sh
export APIRACLOUD_BASE_URL='http://localhost:8787/v1'
export APIRACLOUD_API_KEY='mg_live_...'
curl "$APIRACLOUD_BASE_URL/models"
curl "$APIRACLOUD_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $APIRACLOUD_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: example-001' \
  -d '{"model":"apiracloud/free","messages":[{"role":"user","content":"Привет!"}],"max_tokens":256}'
```

`apiracloud/free` автоматически выбирает доступный бесплатный маршрут. Для него действует общий клиентский лимит, указанный в каталоге.

## Python

```python
import os
import uuid
from openai import OpenAI

client = OpenAI(base_url=os.environ['APIRACLOUD_BASE_URL'],
                api_key=os.environ['APIRACLOUD_API_KEY'], max_retries=0)
response = client.chat.completions.create(
    model='apiracloud/free',
    messages=[{'role': 'user', 'content': 'Привет!'}],
    max_tokens=256,
    extra_headers={'Idempotency-Key': str(uuid.uuid4())},
)
print(response.choices[0].message.content)
print(response.usage)
```

## JavaScript (fetch)

```js
const response = await fetch(`${process.env.APIRACLOUD_BASE_URL}/chat/completions`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.APIRACLOUD_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(),
  },
  body: JSON.stringify({model:'apiracloud/free', messages:[{role:'user',content:'Привет!'}], max_tokens:256}),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error?.message || `HTTP ${response.status}`);
if (response.status === 202) console.log('Запрос уже существует:', result.id);
else console.log(result.choices[0].message.content);
```

## Параметры

Обязательны `model` и `messages` (1–200 сообщений). `content` — строка; роли: `system`, `developer`, `user`, `assistant`, `tool`.
Допускаются поля сообщения `name` и `tool_call_id`. Массивы контента с изображениями и `assistant.tool_calls` пока отклоняются. Полный цикл tool calling пока не поддерживается.

- `max_tokens` **или** `max_completion_tokens`: положительное целое; по умолчанию 1024, ограничивается максимумом модели.
- `stream`: boolean, по умолчанию false.
- `temperature`: 0–2; `top_p`: 0–1.
- `presence_penalty`, `frequency_penalty`: от −2 до 2.
- `seed`: целое в диапазоне ±2147483647.
- `stop`, `tools` (до 32), `tool_choice`, `response_format` передаются поставщику; поддержка зависит от модели.
- Другие поля, включая `n`, `plugins`, `provider`, `modalities`, `stream_options`, отклоняются. Usage шлюз запрашивает сам при необходимости.

Ограничения: тело запроса до 2 MiB по умолчанию, текст сообщений до 1 500 000 байт; контекст проверяется предварительной оценкой токенов.

## Потоковые ответы

Передайте `"stream":true`; ответ имеет тип `text/event-stream`. Читайте строки `data: <JSON>`, собирайте `choices[].delta.content`; завершение — `data: [DONE]`.
Сетевые чанки не совпадают с границами SSE-событий: используйте буфер и разделитель пустой строки. Ошибка после начала потока может прийти событием SSE; одного HTTP 200 недостаточно для признания запроса успешным.
Usage приходит в последнем событии. Шлюз продолжает получать его при разрыве клиентского соединения в пределах серверных таймаутов. Если расход не установлен, запрос остаётся `usage_pending`, а резерв не освобождается автоматически.

## Стоимость и идентификаторы

Сохраняйте заголовок `X-Request-Id`. В обычном ответе есть `gateway.request_id`, `gateway.charged_micros`, `gateway.currency`, `gateway.price_version`, а также заголовок `X-ApiraCloud-Cost-Micros`.
Один рубль = 1 000 000 micros. Для SSE итоговую сумму смотрите в кабинете; она ещё неизвестна при отправке HTTP-заголовков.

Перед запросом блокируется предварительный резерв. После ответа токены поставщика умножаются на утверждённые входную/выходную ставки; остаток резерва освобождается. Сумма списания ограничена резервом, превышение регистрируется как потери сервиса.
Розничные ставки базовых токенов при импорте и подтверждении: базовая цена × установленный USD/RUB × 1,40. Курс 95 в примере — настройка, а не актуальная биржевая котировка.
Фактический расход берётся из `usage.cost`; при его отсутствии используется оценка по тарифу. Сохраняется полный usage с cache/reasoning-полями. Reasoning-токены не нужно повторно прибавлять к completion.
Текущий розничный расчёт использует две опубликованные ставки; отдельные скидки кэша и временные тарифы источника не передаются клиенту автоматически. Поэтому фактическая наценка отдельного запроса может отличаться от 40%.

## Повторы и ошибки

Повторяйте один логический запрос с прежним `Idempotency-Key` и тем же JSON. Повтор возвращает **202**, `id`, `status`, `idempotent_replay:true`; текст первоначального ответа не хранится и не воспроизводится. Изменение тела с тем же ключом даёт 409.
Новый ключ означает новый оплачиваемый запрос. Не включайте слепые автоматические повторы после таймаута.

Ошибки имеют форму `{"error":{"code":"...","message":"...","request_id":"req_..."}}`. До создания запроса `request_id` может отсутствовать.

- 400 — неверные параметры, неподдерживаемый контент или превышение контекста.
- 401 — отсутствующий/отозванный/истёкший ключ.
- 402 — недостаточно доступного баланса для резерва.
- 403 — запрет модели для ключа; в кабинете также CSRF/права/2FA.
- 404 — модель или объект не найдены.
- 409 — конфликт идемпотентности, резерва или версии цены.
- 413 — слишком большой запрос.
- 429 — RPM/TPM, дневной или месячный бюджет.
- 502/503/504 — ошибка поставщика, недоступный маршрут, таймаут или глобальный бюджет.

## API кабинета

Сессия получается через `POST /api/auth/login`. `GET /api/auth/me` возвращает пользователя и `csrf_token`; для изменения данных нужен `X-CSRF-Token`, сессионная и CSRF-cookie. Origin должен совпадать с `PUBLIC_ORIGIN`. `POST /api/auth/logout` удаляет сессию и cookies.

- `GET /api/client/usage`: последние 200 запросов текущего клиента.
- `GET /api/client/usage-summary?from=<ISO>&to=<ISO>`: агрегат по моделям за период `[from,to)`, по умолчанию текущий месяц UTC.
- `GET /api/client/requests/<id>`: состояние собственного запроса.
- `GET /api/client/ledger`: последние 200 операций.
- `GET /api/admin/usage-summary?from=<ISO>&to=<ISO>`: по клиенту и модели, вход/выход, выручка, себестоимость, разница, ожидающие usage.
- `POST /api/admin/users/<id>/adjust-balance`: `{ "amount":"100,50", "reason":"Пополнение" }`, роли finance/owner; отрицательная сумма — списание, резерв защищён.
- `GET /api/admin/pricing`: предложения, полные ставки источника, параметры монитора.
- `POST /api/admin/pricing/check-provider-catalog`: проверка источника без публикации тарифов.
- `POST /api/admin/pricing/<id>/approve` или `/reject`: решение finance/owner. Повторное и устаревшее решение даёт 409.

Клиентская сводка не раскрывает себестоимость и данные других клиентов. Административные решения и корректировки попадают в аудит.

## Мониторинг цен и Telegram

Включите `ENABLE_PRICE_MONITOR=1`, `PRICE_CHECK_INTERVAL_HOURS=6` (или 4), `MARKUP_PERCENT=40`, настройте `USD_RUB_RATE`.
При запуске первая проверка через 5 секунд, затем по интервалу. Нужен работающий процесс сервера. Проверяются все ставки из pricing, включая кэш и overrides, а не только вход/выход. Новая цена не публикуется без решения.
Одинаковые ожидающие изменения не создаются повторно. Изменившийся источник заменяет старое предложение; отклонённое предложение не повторяется до нового изменения. Предложение старше суток нельзя подтвердить без новой проверки.

Для бота задайте `TELEGRAM_BOT_TOKEN`, `TELEGRAM_ADMIN_CHAT_ID`. Для кнопок дополнительно: `TELEGRAM_ADMIN_USER_ID`, `TELEGRAM_OWNER_EMAIL` (активный owner в системе), `TELEGRAM_WEBHOOK_SECRET` (случайный секрет). Telegram необязателен: сбой уведомления не отменяет проверку каталога и не мешает finance/owner менять боевые тарифы вручную в админке. Ручная публикация создаёт новую версию цены и запись аудита.
Зарегистрируйте в Telegram `setWebhook`: URL `https://<домен>/api/telegram/webhook`, `secret_token` равен `TELEGRAM_WEBHOOK_SECRET`, `allowed_updates` содержит `callback_query`.
Webhook проверяет секрет, Telegram user ID, chat ID и роль связанного владельца. Telegram-аккаунт становится каналом утверждения цен; доступ к нему должен быть только у владельца. Решения из бота и админки используют одну проверку версий и аудит.
Без этих настроек интеграция остаётся отключённой. Токен бота не храните в исходниках.

Актуальные методы и ограничения описаны в этой документации и в ответах API ApiraCloud.
