Документация

HTTP API

laya-serve предоставляет Laya по сетевому протоколу TypeSafe Jev /v1/systemone. Клиент, написанный под Jev — hs-jev, typesafe-sdk или ваш собственный, — может направить свой базовый URL на этот сервер и продолжить работать: вывод Laya predict() уже совместим со схемой, а сервер добавляет только HTTP-поверхность: один маршрут решения, проверку работоспособности, необязательную проверку bearer и лимиты запросов.

Существует клиент для PHP, который нацелен на laya-serve, а не на API Jev: marcreichel/laya-php — это SDK для Composer (PHP 8.4+), который отображает класс перечислений и атрибутов на вопросы и возвращает экземпляр, читает GET /health для проверки развёртывания и поставляется с тестовым дублёром, чтобы вызывающие могли писать модульные тесты без запущенного сервера.

pip install "laya[serve]"
laya-serve            # http://0.0.0.0:8000

Тот же входной пункт работает встроенным в любой ASGI-сервер: laya.serve.create_app() собирает приложение FastAPI, необязательно с Router, который вы внедряете (create_app(router)), вместо собранного из окружения.

Конфигурация

Всё — переменные окружения, поэтому один образ обслуживает и запуск разработки на ноутбуке, и юнит systemd.

переменная окружения значение по умолчанию
LAYA_HOST адрес привязки 0.0.0.0
LAYA_PORT порт привязки 8000
LAYA_ROOT_PATH публичный префикс URL при обслуживании за обратным прокси пусто
LAYA_DEVICE устройство torch для каждого чекпойнта auto
LAYA_PRELOAD собирать чекпойнты при старте, а не лениво 1
LAYA_MODELS список через запятую для предзагрузки (english,multilingual,typed-decisions); пусто = все все
LAYA_THREADS ограничивает внутриоперационные потоки torch на CPU; держите <= физических ядер – переподписка логических ядер даёт большую регрессию значение по умолчанию torch
LAYA_AUTO_TASK автоматически маршрутизировать на чекпойнт typed-decisions 0
LAYA_IDLE_UNLOAD_SECONDS выгружает резидентные чекпойнты после стольких секунд простоя; следующий запрос снова загружает свой чекпойнт. Ноль отключает выгрузку 0
LAYA_DEFAULT_MODEL чекпойнт, на который откатывается состояние без языковых свидетельств; псевдонимы вроде ml разрешаются так же, как их разрешает core, а неразрешимое имя останавливает сервер при запуске english
LAYA_API_KEY если задано, требовать Authorization: Bearer <key> нет
LAYA_LOG_LEVEL уровень логирования uvicorn info
LAYA_MAX_CONCURRENT запросов, допущенных после аутентификации одновременно; излишек получает 503 16
LAYA_MAX_BATCH_TOKENS токенов, которые один ПРЯМОЙ ПРОХОД /v1/systemone/batch может собрать (states x вопросы x ширина строки); больший батч разбивается на несколько проходов, а не отклоняется 131072
LAYA_JEV_STRICT обслуживает строгий сетевой контракт Jev: без корневого routing, без action / answer_confidence на каждый ответ, без confidence на noul-ответах и usage, сведённый к input_tokens + output_tokens. Для клиентов, которые проверяют ответ по контракту Jev без лишних полей 0

Для развёртывания, опубликованного под префиксом вроде /laya, задайте LAYA_ROOT_PATH=/laya. FastAPI использует его при генерации URL OpenAPI и Swagger UI. Настройте обратный прокси так, чтобы он срезал /laya перед перенаправлением запросов в Laya; маршруты приложения внутри остаются /health и /v1/systemone.

Для контейнеров, включая образы CUDA и ARM64, см. Краткое руководство по Docker.

Для пиковой локальной нагрузки задайте LAYA_IDLE_UNLOAD_SECONDS=300. Инференс и выгрузка выполняются в одном воркере, и окно простоя начинается заново, когда завершается одиночный или батчевый прямой проход, включая неудавшиеся запросы. Следующее предсказание платит за холодную загрузку. Выгрузка освобождает ссылки на модели и кеши устройств, включая Metal; аллокатор процесса может удерживать страницы RAM, поэтому RSS процесса не обязательно падает на размер чекпойнта.

Эндпоинты

GET /health

Всегда открыт (без аутентификации) и остаётся отзывчивым во время инференса, потому что привязанный к CPU прямой проход выполняется в собственном воркере, а не в цикле событий. Поля ниже liveness не открыты на развёртывании, где задан LAYA_API_KEY: без bearer /health отвечает только {"status": "ok"} и больше ничем, потому что остальное называет резидентные чекпойнты, их точные SHA ревизий, состояние устройства и последнюю причину отката каждого чекпойнта, что цитирует аппаратное обеспечение хоста. Пробе нужен только 200, поэтому проверка работоспособности не затрагивается, а неверный bearer всё равно даёт 200, а не 401. Если LAYA_API_KEY не задан, каждый вызывающий получает полную полезную нагрузку, показанную здесь.

{"status": "ok", "loaded": ["english", "multilingual"], "revisions": {"english": "...", "multilingual": "..."},
 "device": "cuda", "device_is_preference": false,
 "checkpoint_devices": {"english": "cuda", "multilingual": "cuda"},
 "cpu_fallbacks": {"english": {"count": 0, "last_reason": null}, "multilingual": {"count": 0, "last_reason": null}}}

Ответ одного сервера, поэтому блоки согласуются друг с другом: каждый ключ revisions, checkpoint_devices и cpu_fallbacks — это имя в loaded. tests/test_serve.py удерживает этот пример против обработчика, который его порождает, поле за полем.

Когда выгрузка по простою включена, аутентифицированные ответы health также включают idle_unload_seconds (настроенное окно) и idle_seconds (время с последнего запроса инференса или его завершения). Проверки работоспособности не сбрасывают эти часы. Пустой список loaded — это норма после выгрузки по простою.

  • status — ok, пока процесс вообще отвечает. О чекпойнтах он ничего не говорит.
  • loaded перечисляет чекпойнты, находящиеся в памяти. Он пуст, пока какой-нибудь запрос не построит один, чем и занят процесс при LAYA_PRELOAD=0.
  • revisions — ревизия артефакта, из которой был загружен каждый резидентный чекпойнт, с ключами по тем же именам, что и loaded, поэтому развёртывание может подтвердить, что оно на самом деле обслуживает.
  • device — устройство, на котором резидентный чекпойнт действительно считает, и это не всегда то, что просил LAYA_DEVICE: чекпойнт, который хочет GPU, но не может его получить, молча откатывается на CPU и всё равно отвечает правильно. Когда ничего не резидентно, это вместо этого настроенное предпочтение.
  • device_is_preference — true ровно пока ничего не резидентно, и false, как только обработчик может измерить. Это разница между сервером, сообщающим свою конфигурацию, и сервером, сообщающим, где происходит его работа: тот, кто тихо потерял GPU, говорит false с device cpu, а не продолжает отвечать cuda.
  • checkpoint_devices даёт измерение по каждому чекпойнту, с ключами по именам в loaded; device — первое из этих значений.
  • cpu_fallbacks считает по каждому резидентному чекпойнту запросы, которые исчерпали память GPU и были один раз повторены на CPU: count с момента запуска процесса и last_reason с текстом ошибки последнего. Понижение ограничено запросом, который не удался, поэтому чекпойнт, построенный на CPU потому что GPU никогда не был доступен, не является откатом и считается 0 здесь – это видно в device.

POST /v1/systemone

Один запрос несёт state и любое число вопросов по нему:

curl -s localhost:8000/v1/systemone -H 'content-type: application/json' -d '{
  "state": "I was charged twice this month, I want my money back",
  "questions": {
    "queue":   {"type": "choice", "instructions": "Which team?",
                "criteria": {"billing": "billing and refunds", "tech": "login and app issues",
                             "other": "everything else"}},
    "urgency": {"type": "score",  "instructions": "How urgent?",
                "criteria": ["calm", "firm", "angry", "furious"]}
  }
}'
поле обязательно значение
state да текст, письмо, тикет или JSON-документ, по которому принимается решение; отсутствующий или null state — это 400
questions да объект с ключами по id вопроса; каждый вопрос — choice / score / noul с instructions и criteria
model нет называет чекпойнт; путь или непубликованный Hub id — это 422, всё остальное игнорируется (см. ниже)
task нет принудительно задаёт чекпойнт по имени рабочего процесса вместо того, чтобы позволить маршрутизации решить; неизвестное имя — это 422 с его указанием
lang нет код языка (de, en-US), который пропускает определение, когда называет язык; пустой или нераспознанный код переходит к определению
lang_guess нет код языка из собственного идентификатора клиента, запрашиваемый после lang и до определения; любой неанглийский код маршрутизируется на многоязычный чекпойнт
max_len нет общее окно токенов для этого запроса, ограниченное LAYA_MAX_TOKEN_BUDGET
head_max_len нет окно токенов, которое делит промпт вариантов, тот же предел; см. Расширение бюджета токенов, когда вопрос в нём нуждается
min_confidence нет порог воздержания в [0.0, 1.0]; ответ, чей answer_confidence падает ниже него, возвращается помеченным как low_confidence, а сам ответ сохраняется

model, task, lang, lang_guess, max_len, head_max_len и min_confidence — это аргументы, которые принимает Router.predict и которые может задать JSON-тело; каждый пересылается только когда запрос его отправляет, поэтому отсутствующий оставляет за настройкой Router(...) самого развёртывания. Пять аргументов хуков, которые также принимает predict, — hooks, on_predict_start, on_predict_end, hooks_raise, hooks_timeout — отклоняются с 422, а не отбрасываются: хук — это callable, который выполняется внутри процесса сервера, а последние два говорят, как выполняются хуки, установленные развёртыванием, поэтому никакое значение, отправленное вызывающей стороной, не имеет здесь смысла. Те же пять отклоняются на стороне клиента узлом LangChain с base_url (laya.integrations.langchain), поэтому цепочка и сырой HTTP-клиент теперь получают одинаковый ответ.

model принимается, чтобы клиент Jev мог продолжать отправлять его. Публичные id Hugging Face (convaiinnovations/laya-multilingual, convaiinnovations/laya-typed-decisions), имена чекпойнтов (english, multilingual, typed-decisions) и их псевдонимы выбирают чекпойнт. convaiinnovations/laya, и любое другое значение, которое не является путём или id репозитория Hub — включая id Jev вроде jev-1 — означает «пусть выбирает router», и блок routing в ответе записывает, что было выбрано и почему. Значение, которое выглядит как путь файловой системы или непубликованный Hub id (/path/to/checkpoint, org/repo, ~/ckpt, .\ckpt), — это 422 как на /v1/systemone, так и на /v1/systemone/batch: этот сервер не может его загрузить, а ответ другим чекпойнтом скрыл бы это. detail — тот же текст unknown model, который поднимает core, плюс напоминание опустить model, чтобы выбор сделал router.

Ответ

{
  "model": "laya-rl-agent",
  "answers": {
    "queue": {"type": "choice", "choice": "billing",
              "probabilities": {"billing": 0.9519, "tech": 0.0327, "other": 0.0154},
              "confidence": 0.797, "answer_confidence": 0.9519,
              "action": {"act_probability": 1.0}},
    "urgency": {"type": "score", "score": 1.6994,
                "legend": {"0": "calm", "1": "firm", "2": "angry", "3": "furious"},
                "probabilities": {"0": 0.0249, "1": 0.4136, "2": 0.3985, "3": 0.1629},
                "confidence": 0.1925, "answer_confidence": 0.4136,
                "action": {"act_probability": 1.0}}
  },
  "usage": {"input_tokens": 83, "output_tokens": 0, "state_tokens": 12,
            "state_tokens_dropped": 0, "truncated": false, "truncated_questions": []},
  "routing": {"model": "english", "repo": "convaiinnovations/laya", "reason": "English Latin text",
              "detection": {"script": "latin", "script_profile": {"latin": 1.0}, "language": "en",
                            "is_english": true, "language_undecided": false, "diacritic_rate": 0.0,
                            "non_latin_fraction": 0.0, "mixed_segment": null},
              "workflow": null}
}

Этот пример — один ответ, который дал этот сервер, дословно: запрос выше, закешированный чекпойнт english на CPU. answers и usage — это ключи, которые декодируют клиенты Jev; model — постоянное имя головы решения, а чекпойнт, который ответил, находится в routing.

тип ответа ключи
choice choice (вариант argmax), probabilities по каждому варианту
score score (индекс ожидаемого уровня, может попасть между уровнями), probabilities с ключами "0".. "k-1", legend, сопоставляющий индекс с текстом уровня
noul noul, вероятность варианта «да»
все confidence, answer_confidence и action.act_probability
gate abstention, abstention_threshold и low_confidence, записываемые воротами воздержания – см. ниже

Строка gate — это отчёт о воздержании (#361), и единственный способ, которым вызывающий может увидеть, что оплаченные им ворота сработали. Запрос, задающий min_confidence, получает его; тот, кто этого не делает, не получает ни одного из трёх ключей. abstention — одно из трёх состояний, записываемое в каждый ответ запроса с воротами: passed (его уверенность превысила порог), abstained (она упала ниже, и low_confidence равен true ровно на этих ответах) или unevaluated (ответ не несёт пригодной уверенности, поэтому ворота не могли решить – сообщить это как проход было бы той же ложью, что и сообщить как флаг). abstention_threshold возвращает порог, по которому эти состояния измерялись, что и позволяет батчевому прогону с порогами по классам быть заново разбитым задним числом. Если min_confidence не задан, ни один из трёх ключей не появляется ни на одном ответе: отсутствие — это и есть отчёт, а не четвёртое состояние, и именно так вызывающий отличает прогон без ворот от пройденных ворот. min_confidence ровно 0.0 был задан, поэтому состояния сообщаются, и ничто не может упасть ниже него, поэтому каждый ответ читается как passed – возвращённый 0.0 и есть то, что отличает это от прохода при реальном пороге. Сам ответ сохраняется в любом состоянии; ворота помечают, но не отбрасывают.

usage сообщает, из чего был построен прямой проход. Сколько состояния читает модель — это бюджет токенов, а не число символов, и бюджет движется вместе с max_len, head_max_len и собственным промптом вариантов каждого вопроса (#174), поэтому эти ключи — единственное место, где этот факт виден:

ключ usage значение
input_tokens не-pad токены строк состояния – по одной строке на вопрос, поэтому растёт с вопросами, а не является длиной контекста
output_tokens всегда 0 – голова отвечает за один проход, она ничего не генерирует
state_tokens токены, которые нужны всему сериализованному состоянию
state_tokens_dropped токены из них, которых не получил хотя бы один вопрос: худший случай по вопросам, так как каждый оставляет состоянию разное место
truncated true, когда этот худший случай что-то отбросил
truncated_questions id вопросов, чьё собственное окно было обрезано, [] когда таких нет
options присутствует только когда варианты какого-то вопроса больше не имеют каждый свой отрезок токенов: с ключом по id вопроса, с total (варианты, которые определяет этот вопрос), distinct (отрезки, дошедшие до последовательности) и tokens_per_option

Обрезанный ответ — всё ещё ответ – голова решает по тем свидетельствам, которые ей дали – но вызывающий, измеряющий состояния числом символов, не может увидеть обрез нигде больше в ответе.

routing записывает, какой чекпойнт ответил и почему:

ключ routing значение
model чекпойнт, который ответил: english, multilingual или typed-decisions
repo его публичный id Hugging Face
reason предложение о выборе, называющее свидетельство, на которое он опирался
detection laya.lang.analyse() по состоянию – script, script_profile, language, is_english, language_undecided, diacritic_rate, non_latin_fraction, mixed_segment – или null, когда маршрут решил до чтения текста
workflow рабочий процесс typed-decisions, которому соответствуют id вопросов, или null

detection равен null на каждом пути, который решает, не читая состояние: принудительно заданный model или task, отвечённый через lang или lang_guess, или совпавший с рабочим процессом typed-decisions по id вопросов. lang_guess не оставляет собственного ключа – подсказка, на которую он опирался, названа в reason. Ветви model и task тоже сообщают workflow как null, потому что отвечают до того, как прочитаны id вопросов.

Уверенность: два числа, не взаимозаменяемые

  • answer_confidence — это масса вероятности на сообщённом ответе (max(p)). Это величина, которую подбирает масштабирование по температуре, и та, по которой вычисляются цифры ECE этого репозитория, поэтому она несёт свойство gating, на которое опирается страница Бенчмарки и известные ограничения, — но только для чекпойнта, температурный подбор которого проверен на вашем трафике.
  • confidence означает разное в зависимости от типа: нормированная энтропия 1 - H(p)/log(k) на choice и score и max(p_yes, p_no) на noul (где она равна answer_confidence).

Никогда не сравнивайте эти два с одним порогом. Учтите также различие при переносе с Jev: TypeSafe определяет уверенность как (n*p_max - 1)/(n - 1), поэтому порог, перенесённый из развёртывания Jev, применяет gating иначе к значению энтропии Laya.

Строгий контракт Jev: LAYA_JEV_STRICT

Полезная нагрузка выше — это полная нагрузка Laya. Контракт Jev, которому клиент может её подчинить, определяет меньше: три поля верхнего уровня (model, answers, usage), контрактные ключи на каждом ответе и ничего больше, и usage из двух счётчиков токенов. Клиент, который проверяет ответ по этому контракту без лишних полей – плагин провайдера TypeSafe от OpenClaw один из таких – отвергает полную нагрузку, поэтому LAYA_JEV_STRICT=1 проецирует ответ на контракт перед ответом, как на /v1/systemone, так и на /v1/systemone/batch:

  • корень сохраняет только model, answers и usage; routing не отправляется;
  • ответ choice сохраняет choice, probabilities и confidence;
  • ответ score сохраняет score, probabilities, confidence и legend;
  • ответ noul сохраняет только noul;
  • usage сохраняет input_tokens и output_tokens; факты обрезки и потолок свёрнутых вариантов не отправляются.

Проекция сохраняет только контрактные ключи и ничего не пересчитывает: каждое значение — то, которое результат уже несёт, поэтому вероятности и оценки, которые читает строгий клиент, идентичны тем, что сообщает полная нагрузка. По умолчанию остаётся полная нагрузка, а развёртывание, включившее флаг, теряет видимость обрезки, которую даёт usage – обрезанное состояние тогда видно в логах, а не в ответе. criteria для score должен оставаться простыми строками при строгом контракте: строгий клиент сравнивает возвращённый legend с критериями, которые отправил, а Laya рендерит структурированный критерий через JSON Python, что вызывающий на JavaScript, преобразующий собственные критерии в строку, может не совпасть байт в байт.

Успешные ответы также несут Server-Timing: inference;dur=<ms> и X-Inference-Time-Ms.

Лимиты

Ограничители запросов проверяются до токенизации, поэтому слишком большой запрос стоит серверу только прочитанных байтов. Каждый из них — это 413; detail говорит, какой лимит был превышен.

лимит значение
тело запроса 2 MiB, применяется во время потоковой передачи – фрагментированный или заниженный Content-Length не может его обойти
state 50,000 символов текста, который получает модель – сама строка для строкового state, json.dumps(state, ensure_ascii=False) для объекта или массива
вопросов на запрос 64
states на батч-запрос 64
вариантов на choice-вопрос 100
уровней на score-вопрос 32
вариантов по всем вопросам 512
одновременно допущенных запросов LAYA_MAX_CONCURRENT (16)

/v1/systemone/batch ограничен иначе, и не отказом. Он токенизирует каждое состояние один раз на вопрос и собирает каждую строку в один тензор, поэтому границы полей перемножаются: 64 состояния по 64 вопроса — это 4096 строк, что допускает любой другой лимит на этой странице. Стоимость строки — её ширина, а max_len сам является полем запроса, поэтому стоимость батча — states x questions x width.

Вместо отказа от большого батча эндпоинт его разбивает: когда это произведение превышает LAYA_MAX_BATCH_TOKENS (по умолчанию 131 072), он выбирает batch_size так, чтобы каждый прямой проход оставался в бюджете, и Router.predict_batch выполняет батч за несколько проходов. Каждое состояние всё равно получает ответ, и ответ не меняется. Запрос, строки которого уже помещаются, не получает никакого batch_size, поэтому ведёт себя точно как раньше – это важно, потому что форма батча может двигать результаты с плавающей точкой. batch_size, отправленный вызывающим, всегда побеждает: он просил форму.

По умолчанию 256 строк проходят за один проход – 64 состояния по 4 вопроса или 8 по 32. Большие батчи разбиваются, а увеличение max_len делает каждый проход уже, а не стоит 16-кратной работы. Чего это не ограничивает — так это то, как долго один запрос занимает сервер; это LAYA_MAX_CONCURRENT и единственный воркер инференса, и это уже верно для одного запроса /v1/systemone по состоянию в 50 000 символов.

Ограничения на варианты — это только HTTP-защиты от усиления; сама модель укладывает токены вариантов в окно head_max_len=192, поэтому вопрос в пределах HTTP-лимитов всё ещё может быть отклонён как 422, когда тексты вариантов вместе превышают этот бюджет. Harness оценки выполняет те же запросы в процессе, без слоя HTTP.

Ошибки

статус когда detail тела
400 тело не является допустимым JSON, не является объектом, не имеет questions, state отсутствует или null, questions не объект, или строка где угодно в теле содержит непарный суррогатный escape \udXXX что не так
401 LAYA_API_KEY задан, а bearer-токен отсутствует или неверен invalid or missing bearer token
413 любой лимит выше какой лимит и насколько
422 вопрос — правильно сформированный JSON, но недопустимый для Laya (неизвестный тип, вариантов больше бюджета головы), или управляющий элемент запроса (lang, min_confidence, аргумент хука) не в той форме, которую принимает этот эндпоинт называет вопрос или поле и что исправить
500 инференс не удался по любой другой причине inference failed – всегда эта строка, чтобы пути, веса и состояние памяти никогда не утекали; причина — в логе сервера
503 уже выполняются LAYA_MAX_CONCURRENT запросов server busy, try again later

400 о непарном суррогате — тот, что выглядит необычно. \udXXX без пары — легальный JSON, но символ, который он называет, нельзя закодировать в UTF-8, поэтому токенизатор поднимает TypeError – собственная строка вызывающего приходит как сбой сервера, с трассировкой на каждый запрос. Поэтому оба маршрута решения проходят по разобранному телу в поисках одиночных суррогатов и отвергают его до того, как он достигнет инференса. Проход выполняется после проверок размера, поэтому слишком большое тело всё равно отвергается первым, а ограничения по символам и вопросам ограничивают то, до чего он может дойти. Парный суррогат к моменту, когда парсер закончил, — один обычный астральный символ, поэтому эмодзи в состоянии не затрагивается.

Нагрузка сверх предела отклоняется, а не ставится в очередь: клиенты, удерживающие место допуска, пока передают медленное тело, не могут заморить /health, и повторная попытка может занять место, которое оставил отклонённый клиент.

Модель конкурентности

Инференс — это синхронный вызов torch, который занимает от сотен миллисекунд до секунд на CPU, поэтому он никогда не выполняется в цикле событий: запросы передаются исполнителю с одним воркером, что означает один прямой проход за раз — форма, которую хочет один чекпойнт на одном устройстве. Допуск (семафор LAYA_MAX_CONCURRENT) проверяется до чтения любого байта тела и удерживается на протяжении всего инференса; шлюз инференса подключается только после того, как тело получено целиком, поэтому медленный клиент удерживает место допуска, но никогда — место инференса.

Чего здесь (пока) нет

Этот сервер намеренно говорит на одном протоколе. Нет эндпоинта, совместимого с OpenAI; вместо этого запускайте несколько вопросов в одном запросе, поскольку они делят один прямой проход на набор вопросов. Другой маршрут — POST /v1/systemone/batch, который отвечает на один набор questions по массиву states. У него ещё нет раздела на этой странице – его форма запроса в разделе self-hosting в README – и каждая проверка выше применяется к нему так же, как к POST /v1/systemone: те же 400 формы, тот же отказ по непарному суррогату, та же auth, admission, ограничения размера, валидация управляющих полей тела и отображение 500. CLI laya и сервер MCP покрывают локальное использование — см. README.