Разработчикам

Руководство по API

Используйте любую модель Milly Lab из собственного кода — на любом языке, на любой платформе, с сервера или из браузера — через OpenAI-совместимый API, который выставляет счёт через ваш аккаунт Milly Lab. Здесь описаны первый запрос, настройка SDK, стриминг, живые цены, правила биллинга, ограничения на ключ, лимиты, коды ошибок и эндпоинт использования.

Последняя проверка: 2026-09-10


#Базовый URL

https://aria-web-production-a38d.up.railway.app/v1

Все эндпоинты ниже указаны относительно этого адреса. Интерактивная OpenAPI-справка (/docs) доступна на непродакшн-развёртываниях; те же операции описаны здесь.


#Быстрый старт

  1. Создайте ключ. В веб-приложении: Настройки → Безопасность → API-ключи → Создать ключ (кнопка Управлять API-ключами на странице API открывает этот раздел напрямую). Доступ к API входит в тариф Max и выше. Сырой ключ (mk_…) показывается один раз; хранятся только последние четыре символа.
  2. Направьте OpenAI SDK на базовый URL. Любой клиент, говорящий на протоколе OpenAI chat-completions, работает после замены base URL.
  3. Выберите модель из 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 за 1 млн выходных (изображения: $0.01 за изображение; эмбеддинги: ставка провайдера + $1 за 1 млн входных токенов).
  2. Сначала тариф. Первым расходуется месячный лимит вашего тарифа для этой модели.
  3. Затем баланс по 2×. Всё сверх лимита списывается с пополняемого баланса по двойной стоимости (та же наценка, что и в веб-приложении).
  4. Резерв до расхода. Запрос, который не покрывается остатком лимита плюс балансом, отклоняется с 402 до обращения к провайдеру; на время генерации платформа удерживает худший случай и затем сводит к фактическому использованию. Если провайдер модели не настроен на развёртывании, ответ — 503 provider_unavailable, тоже до любого удержания, так что ничего не списывается. Провайдер, упавший до выдачи результата, — 502 без списания; вывод, успевший прийти в потоке до сбоя, оплачивается.
  5. Ограничения на ключ (ниже) добавляют лимит расходов и список разрешённых моделей поверх правил аккаунта.

Каждый 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.

HTTPcodeЗначение
400invalid_body · invalid_messages · invalid_inputНекорректный запрос, нет текста пользователя, плохой вход эмбеддинга
401missing_api_key · invalid_api_keyНет bearer-ключа, либо он отозван/неизвестен
402insufficient_for_request · insufficient_balanceЛимит + баланс не покрывают запрос
402spend_cap_reachedДостигнут месячный лимит этого ключа
403api_access_requiredВ тарифе аккаунта нет доступа к API
403model_not_allowedМодель не в списке разрешённых для ключа
403account_disabledАккаунт заблокирован
404model_not_foundНеизвестная или снятая модель — вызывать можно только строки /v1/models
429rate_limit_exceededЛимит на ключ (120/мин); Retry-After говорит, сколько ждать
502upstream_errorПровайдер упал, не выдав результата — ничего не списано
503provider_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 на ключ.