Разработчикам
Руководство по API
Используйте любую модель Milly Lab из собственного кода — на любом языке, на любой платформе, с сервера или из браузера — через OpenAI-совместимый API, который выставляет счёт через ваш аккаунт Milly Lab. Здесь описаны первый запрос, настройка SDK, стриминг, живые цены, правила биллинга, ограничения на ключ, лимиты, коды ошибок и эндпоинт использования.
Последняя проверка: 2026-09-10
#Базовый URL
https://aria-web-production-a38d.up.railway.app/v1
Все эндпоинты ниже указаны относительно этого адреса. Интерактивная OpenAPI-справка (/docs) доступна на непродакшн-развёртываниях; те же операции описаны здесь.
#Быстрый старт
- Создайте ключ. В веб-приложении: Настройки → Безопасность → API-ключи → Создать ключ (кнопка Управлять API-ключами на странице API открывает этот раздел напрямую). Доступ к API входит в тариф Max и выше. Сырой ключ (
mk_…) показывается один раз; хранятся только последние четыре символа. - Направьте OpenAI SDK на базовый URL. Любой клиент, говорящий на протоколе OpenAI chat-completions, работает после замены base URL.
- Выберите модель из
GET /v1/modelsи отправьте первый запрос.
curl https://aria-web-production-a38d.up.railway.app/v1/chat/completions \
-H "Authorization: Bearer $MILLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-5",
"messages": [{"role": "user", "content": "Три факта о Душанбе."}]}'
Ответ — стандартный объект chat.completion с choices[0].message.content и usage (токены, за которые списаны средства).
#Настройка SDK
Python
from openai import OpenAI
client = OpenAI(
base_url="https://aria-web-production-a38d.up.railway.app/v1",
api_key="mk_...",
)
r = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Три факта о Душанбе."}],
)
print(r.choices[0].message.content, r.usage)
Node.js
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://aria-web-production-a38d.up.railway.app/v1",
apiKey: process.env.MILLY_API_KEY,
});
const r = await client.chat.completions.create({
model: "claude-sonnet-5",
messages: [{ role: "user", content: "Три факта о Душанбе." }],
});
console.log(r.choices[0].message.content, r.usage);
Обычный fetch (любая среда)
const res = await fetch("https://aria-web-production-a38d.up.railway.app/v1/chat/completions", {
method: "POST",
headers: { Authorization: "Bearer " + MILLY_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ model: "claude-sonnet-5",
messages: [{ role: "user", content: "Три факта о Душанбе." }] }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
Поддерживаемые поля запроса: model, messages (роли system/developer, user, assistant; строка или части text), stream, max_tokens / max_completion_tokens, reasoning_effort (low · medium · high). Поля сэмплирования (temperature, top_p, …) принимаются для совместимости; сэмплирование выбирает маршрутизатор платформы. tools, tool_choice и сообщения tool/function принимаются, но пока не выполняются — см. Что дальше.
#Стриминг
Укажите stream: true, чтобы получать server-sent events. Каждое событие — chat.completion.chunk; последний чанк содержит finish_reason и usage, затем data: [DONE].
stream = client.chat.completions.create(model="claude-sonnet-5", stream=True,
messages=[{"role": "user", "content": "Напиши хайку о горах."}])
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
if chunk.usage:
print("\n", chunk.usage)
Если клиент отключится посреди стрима, генерация на платформе всё равно доходит до конца, и произведённые токены оплачиваются — провайдеру за них уже заплачено.
#Модели и живые цены
GET /v1/models — публичный (ключ не нужен) и возвращает все доступные через API модели с точными ставками, по которым платформа выставляет счёт:
{
"id": "claude-sonnet-5",
"display_name": "Claude Sonnet 5",
"modality": "chat",
"capabilities": { "vision": true, "tools": true, "streaming": true },
"context_window": 1000000,
"max_output_tokens": 64000,
"pricing": {
"unit": "per_1m_tokens",
"input_per_1m_usd": 2.0,
"output_per_1m_usd": 10.0,
"platform_fee_per_1m_usd": 1.0,
"effective_input_per_1m_usd": 3.0,
"effective_output_per_1m_usd": 11.0
}
}
У моделей изображений pricing.unit = "per_image" с полями per_image_usd, platform_fee_per_image_usd и effective_per_image_usd. Модель эмбеддингов указана с modality: "embedding". При вызове /v1/models с ключом каждая строка дополнительно содержит allowed_for_key — может ли этот ключ вызывать модель — и included_in_plan — выделяет ли тариф аккаунта эту модель (сначала лимит) или каждый вызов оплачивается с баланса по 2×; поле верхнего уровня plan называет тариф. При балансе $0 невключённая модель отвечает 402 с первого токена.
Вызывать можно только перечисленные модели. Снятый ключ без преемника (gpt-4o, gpt-4o-mini) отвечает 404 model_not_found; устаревший псевдоним, указывающий на текущую модель (claude-sonnet → claude-sonnet-5), по-прежнему работает и оплачивается по опубликованной ставке.
Живая таблица на странице API внутри веб-приложения строится по этому эндпоинту.
#Правила биллинга
- Стоимость запроса = ставка провайдера × токены + комиссия платформы $1 за 1 млн входных токенов и $1 за 1 млн выходных (изображения: $0.01 за изображение; эмбеддинги: ставка провайдера + $1 за 1 млн входных токенов).
- Сначала тариф. Первым расходуется месячный лимит вашего тарифа для этой модели.
- Затем баланс по 2×. Всё сверх лимита списывается с пополняемого баланса по двойной стоимости (та же наценка, что и в веб-приложении).
- Резерв до расхода. Запрос, который не покрывается остатком лимита плюс балансом, отклоняется с 402 до обращения к провайдеру; на время генерации платформа удерживает худший случай и затем сводит к фактическому использованию. Если провайдер модели не настроен на развёртывании, ответ — 503
provider_unavailable, тоже до любого удержания, так что ничего не списывается. Провайдер, упавший до выдачи результата, — 502 без списания; вывод, успевший прийти в потоке до сбоя, оплачивается. - Ограничения на ключ (ниже) добавляют лимит расходов и список разрешённых моделей поверх правил аккаунта.
Каждый API-запрос записывает строку использования с меткой ключа; эти строки питают Настройки → Биллинг, показатель потрачено в этом месяце у ключа и GET /v1/usage.
#Ограничения на ключ
Задаются при создании или позже в Настройки → Безопасность → API-ключи (или PATCH /api/keys/{id} из веб-сессии):
| Ограничение | Поведение |
|---|---|
Месячный лимит расходов (USD, целые центы — 0.05, а не 0.001; более точное значение отклоняется с 422) | Когда списанные с ключа за месяц средства достигают лимита, дальнейшие вызовы возвращают 402 spend_cap_reached. Запрос, пересекающий черту, ещё выполняется (его стоимость неизвестна до завершения), поэтому превышение — не больше одного запроса. Сброс 1-го числа каждого месяца (UTC). |
| Разрешённые модели | Список ключей каталога. Любая другая модель возвращает 403 model_not_allowed. Пусто = все API-модели. Устаревшие псевдонимы приводятся к текущему ключу. |
Используйте оба ограничения для любого ключа, покидающего вашу инфраструктуру.
#Эмбеддинги
POST /v1/embeddings — OpenAI-совместимый. Модель text-embedding-3-small (1536 измерений). input — строка или список до 256 строк (по 12 000 символов). Оплата по входным токенам; эмбеддинги не входят ни в один тарифный лимит, поэтому оплачиваются с баланса по 2×. encoding_format — float (по умолчанию) или base64 (float32 little-endian — именно его официальные SDK запрашивают по умолчанию, поэтому client.embeddings.create(...) работает без изменений).
emb = client.embeddings.create(model="text-embedding-3-small", input=["Milly Lab", "public API"])
print(len(emb.data[0].embedding), emb.usage.prompt_tokens)
#Изображения
POST /v1/images/generations — OpenAI-совместимые запрос и ответ. model — ключ модели изображений из /v1/models (gpt-image-2, gemini-nano-banana, fal-ai/flux-pro, fal-ai/stable-diffusion-xl), n 1–4, size 1024x1024 · 1536x1024 · 1024x1536 (приводится к ближайшему соотношению сторон модели), quality low · standard · high (medium/hd принимаются). Вызов синхронный (до 180 с) и возвращает размещённые url; usage.billed_usd — списанная сумма.
img = client.images.generate(model="gpt-image-2", prompt="Акварельная карта Памира", size="1024x1024")
print(img.data[0].url)
Изображения, созданные через API, не добавляются в историю Light Studio.
#Эндпоинт использования
GET /v1/usage?from=YYYY-MM-DD&to=YYYY-MM-DD[&key=<id>] возвращает запросы, токены и списанные доллары (тариф + баланс, с комиссией) по всем ключам аккаунта — один отчётный ключ может наблюдать за всеми. Диапазон по умолчанию — с начала месяца; максимум 92 дня. Чат веб-приложения никогда не включается.
{
"object": "usage",
"from": "2026-09-01T00:00:00", "to": "2026-09-10T12:00:00",
"totals": { "requests": 412, "tokens_in": 1830000, "tokens_out": 210000, "usd": 12.41 },
"by_key": [{ "key_id": "…", "name": "prod", "hint": "a1b2", "requests": 400, "usd": 12.10 }],
"by_model": [{ "model": "claude-sonnet-5", "requests": 300, "usd": 10.20 }]
}
#Лимиты
| Лимит | Значение |
|---|---|
| Запросов на ключ | 120 в минуту → 429 rate_limit_exceeded с Retry-After (секунды) |
| Активных ключей на аккаунт | 5 |
| Сообщений в запросе | 200 |
| Символов в запросе | 400 000 |
| Входов эмбеддинга за вызов | 256 × 12 000 символов |
| Изображений за вызов | 4 |
Каждый ответ /v1 несёт x-request-id (укажите его при обращении в поддержку) и, после аутентификации ключа, x-ratelimit-limit, x-ratelimit-remaining и x-ratelimit-window (секунды). 429 добавляет Retry-After — подождите столько секунд и повторите. 402 не повторяйте, ничего не изменив (пополните баланс, поднимите лимит, выберите более дешёвую модель).
#Коды ошибок
Каждая ошибка в формате OpenAI: {"error": {"message": "…", "type": "…", "code": "…"}} — ветвитесь по code.
| HTTP | code | Значение |
|---|---|---|
| 400 | invalid_body · invalid_messages · invalid_input | Некорректный запрос, нет текста пользователя, плохой вход эмбеддинга |
| 401 | missing_api_key · invalid_api_key | Нет bearer-ключа, либо он отозван/неизвестен |
| 402 | insufficient_for_request · insufficient_balance | Лимит + баланс не покрывают запрос |
| 402 | spend_cap_reached | Достигнут месячный лимит этого ключа |
| 403 | api_access_required | В тарифе аккаунта нет доступа к API |
| 403 | model_not_allowed | Модель не в списке разрешённых для ключа |
| 403 | account_disabled | Аккаунт заблокирован |
| 404 | model_not_found | Неизвестная или снятая модель — вызывать можно только строки /v1/models |
| 429 | rate_limit_exceeded | Лимит на ключ (120/мин); Retry-After говорит, сколько ждать |
| 502 | upstream_error | Провайдер упал, не выдав результата — ничего не списано |
| 503 | provider_unavailable | Провайдер модели (чат, эмбеддинги или изображения) не настроен на этом развёртывании — ничего не списано |
#Вызов из браузера
CORS открыт на /v1 для любого источника (без credentials), так что браузерные приложения могут обращаться к API напрямую с Authorization: Bearer mk_…. Ключ, отправленный в браузерном коде, виден каждому посетителю. Для таких ключей: задайте жёсткий лимит расходов и список разрешённых моделей, регулярно меняйте их либо проксируйте через собственный бэкенд, где ключ остаётся приватным.
#Рекомендации
- Храните ключи в переменных окружения или менеджере секретов, по одному ключу на приложение или среду; отзывайте неиспользуемые.
- Ставьте лимит расходов на каждый ключ; настройте оповещение на
402 spend_cap_reachedв логах. - Читайте
usageиз каждого ответа (или последнего чанка стрима) и сверяйте сGET /v1/usage. - Подбирайте модель под задачу: цены живые, и меньшей модели часто достаточно.
- Передавайте
max_tokens, когда знаете размер ответа — это ограничивает резерв и счёт. - На 429 подождите
Retry-Afterсекунд (экспоненциальная пауза сверху не помешает); 402 — сигнал конфигурации, а не временная ошибка.
#Что дальше
- Проброс вызова инструментов (
tools/tool_choiceпринимаются, но пока не выполняются). - Генерация видео и 3D через
/v1. - Списки разрешённых IP на ключ.