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сdevicecpu, а не продолжает отвечать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.