API HTTP
O laya-serve expõe o Laya através do protocolo wire /v1/systemone do TypeSafe Jev. Um cliente
escrito para o Jev – hs-jev, typesafe-sdk, ou o teu – pode apontar o seu URL base para este
servidor e continuar a funcionar: a saída predict() do Laya já é compatível com o schema, e o
servidor acrescenta apenas a superfície HTTP: uma rota de decisão, uma sonda de saúde, uma
verificação bearer opcional e limites de pedido.
Existe um cliente para PHP que aponta para o laya-serve em vez da API do Jev: marcreichel/laya-php é um SDK do Composer (PHP 8.4+) que mapeia uma classe de enums e atributos para perguntas e devolve uma instância, lê GET /health para uma verificação de implantação e traz um duplo de teste para que quem chama possa fazer testes unitários sem um servidor em execução.
pip install "laya[serve]"
laya-serve # http://0.0.0.0:8000
O mesmo ponto de entrada corre incorporado em qualquer servidor ASGI: laya.serve.create_app()
constrói a app FastAPI, opcionalmente com um Router que injetas (create_app(router)) em vez de
um construído a partir do ambiente.
Configuração
É tudo variáveis de ambiente, por isso uma só imagem serve uma execução de desenvolvimento num portátil e uma unit do systemd.
| variável de ambiente | significado | predefinição |
|---|---|---|
LAYA_HOST |
endereço de bind | 0.0.0.0 |
LAYA_PORT |
porta de bind | 8000 |
LAYA_ROOT_PATH |
prefixo de URL público quando servido atrás de um reverse proxy | vazio |
LAYA_DEVICE |
dispositivo torch para cada checkpoint | auto |
LAYA_PRELOAD |
constrói os checkpoints no arranque, não de forma preguiçosa | 1 |
LAYA_MODELS |
lista separada por vírgulas a pré-carregar (english,multilingual,typed-decisions); vazia = todas |
todas |
LAYA_THREADS |
limita as threads intra-op da torch na CPU; mantém <= núcleos físicos – exceder os núcleos lógicos é uma regressão grande | predefinição da torch |
LAYA_AUTO_TASK |
encaminha automaticamente para o checkpoint typed-decisions | 0 |
LAYA_IDLE_UNLOAD_SECONDS |
descarrega os checkpoints residentes após tantos segundos de inatividade; o pedido seguinte volta a carregar o seu checkpoint. Zero desativa o descarregamento | 0 |
LAYA_DEFAULT_MODEL |
checkpoint para o qual recorre um state sem evidência de idioma; aliases como ml são resolvidos da mesma forma que o core os resolve, e um nome não resolúvel para o servidor no arranque |
english |
LAYA_API_KEY |
se definida, exige Authorization: Bearer <key> |
nenhuma |
LAYA_LOG_LEVEL |
nível de log do uvicorn | info |
LAYA_MAX_CONCURRENT |
pedidos admitidos após autenticação ao mesmo tempo; o excesso recebe 503 |
16 |
LAYA_MAX_BATCH_TOKENS |
tokens que uma PASSAGEM DIRETA de /v1/systemone/batch pode agrupar (states x perguntas x largura da linha); um lote maior é dividido em várias passagens, não recusado |
131072 |
LAYA_JEV_STRICT |
serve o contrato de fio estrito do Jev: sem routing raiz, sem action / answer_confidence por resposta, sem confidence nas respostas noul, e usage reduzido a input_tokens + output_tokens. Para clientes que validam a resposta contra o contrato do Jev sem campos extra |
0 |
Para uma implementação publicada sob um prefixo como /laya, define LAYA_ROOT_PATH=/laya. O
FastAPI usa-o ao gerar os URLs do OpenAPI e do Swagger UI. Configura o reverse proxy para remover
/laya antes de encaminhar pedidos para o Laya; as rotas da app permanecem /health e
/v1/systemone internamente.
Para contentores, incluindo imagens CUDA e ARM64, vê Início rápido com Docker.
Para uso local em rajadas, define LAYA_IDLE_UNLOAD_SECONDS=300. A inferência e o descarregamento correm no mesmo worker, e a janela de inatividade recomeça quando uma passagem direta única ou em lote termina, incluindo pedidos que falharam. A predição seguinte paga um carregamento a frio. O descarregamento liberta referências de modelo e caches de dispositivo, incluindo Metal; o alocador do processo pode reter páginas de RAM, por isso o RSS do processo não tem de descer no tamanho do checkpoint.
Endpoints
GET /health
Sempre aberto (sem autenticação), e mantém-se responsivo durante a inferência porque a passagem
direta limitada pela CPU corre no seu próprio worker, e não no event loop. Os campos abaixo de liveness
não estão abertos numa implementação que definiu LAYA_API_KEY: sem o bearer, /health responde
{"status": "ok"} e nada mais, porque o resto nomeia os checkpoints residentes, os seus SHAs de revisão
exatos, o estado do dispositivo e o último motivo de fallback de cada checkpoint, o que cita o hardware
do anfitrião. Uma sonda só precisa do 200, por isso uma verificação de saúde não é afetada e um bearer
errado continua a ser um 200 em vez de um 401. Sem LAYA_API_KEY definido, todos os que chamam recebem
a carga completa mostrada aqui.
{"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}}}
A resposta de um único servidor, por isso os blocos concordam entre si: cada chave de revisions,
checkpoint_devices e cpu_fallbacks é um nome em loaded. tests/test_serve.py mantém este exemplo
contra o handler que o produz, campo a campo.
Com o descarregamento por inatividade ativado, respostas de saúde autenticadas também incluem
idle_unload_seconds (a janela configurada) e idle_seconds (tempo desde o último pedido de inferência
ou a sua conclusão). As sondas de saúde não reiniciam esse relógio. Uma lista loaded vazia é normal
após um descarregamento por inatividade.
statuséoksempre que o processo responde. Não diz nada sobre os checkpoints.loadedlista os checkpoints residentes em memória. Fica vazio até um pedido construir um, que é o queLAYA_PRELOAD=0deixa o processo a fazer.revisionsé a revisão de artefacto a partir da qual cada checkpoint residente foi carregado, com chave pelos mesmos nomes deloaded, para que uma implementação possa confirmar o que está de facto a servir.deviceé o dispositivo em que um checkpoint residente calcula realmente, que nem sempre é o queLAYA_DEVICEpediu: um checkpoint que quer uma GPU que não consegue obter recorre silenciosamente à CPU e ainda responde corretamente. Sem nada residente, é a preferência configurada.device_is_preferenceétrueexatamente enquanto nada está residente, efalseassim que o handler consegue medir. Essa é a diferença entre um servidor que reporta a sua configuração e um servidor que reporta onde o seu trabalho acontece: um que perdeu a sua GPU em silêncio dizfalsecomdevicecpu, em vez de continuar a respondercuda.checkpoint_devicesdá a medição por checkpoint, com chave pelos nomes emloaded;deviceé o primeiro desses valores.cpu_fallbacksconta, por checkpoint residente, os pedidos que esgotaram a memória da GPU e foram repetidos uma vez na CPU:countdesde o início do processo, elast_reasoncom o texto de erro do último. O rebaixamento está limitado ao pedido que falhou, por isso um checkpoint construído na CPU porque a GPU nunca esteve disponível não é um fallback e conta0aqui – isso aparece emdevice.
POST /v1/systemone
Um pedido transporta um state e qualquer número de perguntas sobre ele:
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"]}
}
}'
| campo | obrigatório | significado |
|---|---|---|
state |
sim | texto, email, ticket ou documento JSON sobre o qual decidir; um state em falta ou null é um 400 |
questions |
sim | objeto indexado por id de pergunta; cada pergunta é choice / score / noul com instructions e criteria |
model |
não | nomeia um checkpoint; um caminho ou id de Hub não publicado é um 422, qualquer outra coisa é ignorada (vê abaixo) |
task |
não | força um checkpoint por nome de fluxo de trabalho em vez de deixar o encaminhamento decidir; um nome desconhecido é um 422 que o nomeia |
lang |
não | um código de idioma (de, en-US) que salta a deteção quando nomeia um idioma; um código em branco ou não reconhecido cai na deteção |
lang_guess |
não | um código de idioma do próprio identificador do cliente, consultado depois de lang e antes da deteção; qualquer código não inglês encaminha para o checkpoint multilingue |
max_len |
não | janela total de tokens para este pedido, limitada por LAYA_MAX_TOKEN_BUDGET |
head_max_len |
não | janela de tokens que o prompt de opções partilha, mesmo limite; vê Alargar o orçamento de tokens para quando uma pergunta precisa disso |
min_confidence |
não | limiar de abstenção em [0.0, 1.0]; uma resposta cujo answer_confidence fique abaixo dele volta marcada como low_confidence, e a resposta em si é mantida |
model, task, lang, lang_guess, max_len, head_max_len e min_confidence são os
argumentos que Router.predict aceita e que um corpo JSON pode indicar; cada um só é encaminhado
quando o pedido o envia, por isso um ausente deixa a definição Router(...) da própria
implementação no comando. Os cinco argumentos de hook que predict também aceita – hooks,
on_predict_start, on_predict_end, hooks_raise, hooks_timeout – são recusados com um 422
em vez de descartados: um hook é um invocável que corre dentro do processo do servidor, e os dois
últimos dizem como os hooks que uma implementação instalou executam, por isso nenhum valor que um
autor de chamada envie tem significado aqui. Os mesmos cinco são recusados do lado do cliente por um
nó LangChain com um base_url (laya.integrations.langchain), por isso uma chain e um cliente
HTTP em bruto recebem agora a mesma resposta.
model é aceite para que um cliente Jev possa continuar a enviá-lo. Os ids públicos do Hugging Face
(convaiinnovations/laya-multilingual, convaiinnovations/laya-typed-decisions), os nomes de
checkpoint (english, multilingual, typed-decisions) e os seus aliases selecionam um checkpoint.
convaiinnovations/laya, e qualquer outro valor que não seja um caminho nem um id de repositório do Hub
– incluindo um id Jev como jev-1 – significa «deixa o router escolher», e o bloco routing da
resposta regista o que foi escolhido e porquê. Um valor que parece um caminho do sistema de ficheiros ou
um id de Hub não publicado (/path/to/checkpoint, org/repo, ~/ckpt, .\ckpt) é um 422 tanto em
/v1/systemone como em /v1/systemone/batch: este servidor não o consegue carregar, e responder com
outro checkpoint esconderia isso. O detail é o mesmo texto unknown model que o core lança, mais o
lembrete de omitir model para deixar o router escolher.
Resposta
{
"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}
}
O exemplo é uma resposta que este servidor deu, literalmente: o pedido acima, o checkpoint english em
cache na CPU. answers e usage são as chaves que os clientes Jev descodificam; model é o nome
constante da cabeça de decisão, e o checkpoint que respondeu está em routing.
| tipo de resposta | chaves |
|---|---|
choice |
choice (a opção argmax), probabilities por opção |
score |
score (índice de nível esperado, pode cair entre níveis), probabilities indexado "0".. "k-1", legend que mapeia índice para o texto do nível |
noul |
noul, a probabilidade da opção sim |
| todas | confidence, answer_confidence, e action.act_probability |
| gate | abstention, abstention_threshold e low_confidence, escritos pela porta de abstenção – vê abaixo |
A linha gate é o relatório de abstenção (#361), e é a única forma de quem chama ver que a porta pela
qual pagou correu. Um pedido que define min_confidence obtém-no; um que não define não obtém nenhuma
das três chaves. abstention é um de três estados, escrito em todas as respostas de um pedido com
porta: passed (a sua confiança passou o limiar), abstained (ficou abaixo, e low_confidence é
true exatamente nessas respostas), ou unevaluated (a resposta não trazia confiança utilizável, por
isso a porta não pôde decidir – reportar isso como uma passagem seria a mesma mentira que reportar como
um flag). abstention_threshold devolve o limiar contra o qual esses estados foram medidos, que é o que
torna uma execução em lote com limiares por classe re-divisível depois do facto. Sem min_confidence
definido, nenhuma das três chaves aparece em nenhuma resposta: a ausência é o relatório, não um quarto
estado, e é assim que quem chama distingue uma execução sem porta de uma porta passada. Um
min_confidence de exatamente 0.0 foi definido, por isso os estados são reportados, e nada pode cair
abaixo dele, por isso todas as respostas leem-se passed – o 0.0 devolvido é o que distingue isso de
uma passagem num limiar real. A resposta em si é mantida em todos os estados; a porta marca, não
descarta.
usage reporta aquilo de que a passagem direta foi construída. Quanto de um state o modelo lê é um
orçamento de tokens, não uma contagem de caracteres, e o orçamento move-se com max_len, head_max_len
e o prompt de opções de cada pergunta (#174), por isso estas chaves são o único lugar onde esse facto é
visível:
chave de usage |
significado |
|---|---|
input_tokens |
tokens não-pad das linhas do state – uma linha por pergunta, por isso cresce com as perguntas em vez de ser um comprimento de contexto |
output_tokens |
sempre 0 – a cabeça responde numa passagem, não gera nada |
state_tokens |
tokens de que o state serializado inteiro precisa |
state_tokens_dropped |
tokens disso que pelo menos uma pergunta não recebeu: o pior caso entre as perguntas, já que cada uma deixa ao state um espaço diferente |
truncated |
true quando esse pior caso descartou algo |
truncated_questions |
os ids das perguntas cuja própria janela foi cortada, [] quando nenhuma |
options |
presente apenas quando as opções de alguma pergunta já não têm cada uma um segmento de tokens: com chave por id de pergunta, com total (as opções que essa pergunta define), distinct (os segmentos que chegaram à sequência) e tokens_per_option |
Uma resposta truncada continua a ser uma resposta – a cabeça decide com a evidência que lhe foi dada – mas quem chama que dimensiona states por contagem de caracteres não consegue ver o corte em nenhum outro lugar da resposta.
routing regista qual checkpoint respondeu e porquê:
chave de routing |
significado |
|---|---|
model |
o checkpoint que respondeu: english, multilingual ou typed-decisions |
repo |
o seu id público do Hugging Face |
reason |
a frase da escolha, nomeando a evidência sobre a qual agiu |
detection |
laya.lang.analyse() sobre o state – script, script_profile, language, is_english, language_undecided, diacritic_rate, non_latin_fraction, mixed_segment – ou null quando a rota decidiu antes de ler o texto |
workflow |
o fluxo de trabalho typed-decisions com que os ids de pergunta coincidem, ou null |
detection é null em todos os caminhos que decidem sem ler o state: um forçado por model ou task,
um respondido por lang ou lang_guess, ou um que coincidiu com um fluxo de trabalho typed-decisions
pelos ids de pergunta. Um lang_guess não deixa chave própria – a dica sobre a qual agiu é nomeada em
reason. Os ramos model e task também reportam workflow como null, porque respondem antes de os
ids de pergunta serem lidos.
Confiança: dois números, não intercambiáveis
answer_confidenceé a massa de probabilidade sobre a resposta reportada (max(p)). É a grandeza que a escala de temperatura ajusta e sobre a qual são calculadas as cifras de ECE deste repositório, por isso transporta a propriedade de gating em que a página Benchmarks e limites conhecidos se apoia – mas só para um checkpoint cujo ajuste de temperatura tenha sido validado no teu tráfego.confidencesignifica algo diferente por tipo: entropia normalizada1 - H(p)/log(k)emchoiceescore, emax(p_yes, p_no)emnoul(onde é igual aanswer_confidence).
Nunca compares os dois com um só limiar. Nota também a diferença ao portar do Jev: a TypeSafe define
a confiança como (n*p_max - 1)/(n - 1), por isso um limiar trazido de uma implementação Jev faz
gating de forma diferente sobre o valor de entropia do Laya.
Contrato estrito do Jev: LAYA_JEV_STRICT
A carga acima é a carga completa do Laya. O contrato Jev ao qual um cliente a pode submeter define menos:
três campos de nível superior (model, answers, usage), as chaves contratadas em cada resposta e
nada mais, e um usage das duas contagens de tokens. Um cliente que valida a resposta contra esse
contrato sem campos extra – o plugin de fornecedor TypeSafe do OpenClaw é um – rejeita a carga
completa, por isso LAYA_JEV_STRICT=1 projeta a resposta sobre o contrato antes de responder, tanto em
/v1/systemone como em /v1/systemone/batch:
- a raiz mantém apenas
model,answerseusage;routingnão é enviado; - uma resposta
choicemantémchoice,probabilitieseconfidence; - uma resposta
scoremantémscore,probabilities,confidenceelegend; - uma resposta
noulmantém apenasnoul; usagemantéminput_tokenseoutput_tokens; os factos de truncagem e o teto de opções colapsadas não são enviados.
A projeção mantém apenas as chaves contratadas e não recalcula nada: cada valor é o que o resultado já
traz, por isso as probabilidades e pontuações que um cliente estrito lê são idênticas às que a carga
completa reporta. O padrão continua a ser a carga completa, e uma implementação que liga a flag perde a
visibilidade de truncagem que usage oferece – um state cortado fica então visível nos logs, não na
resposta. O criteria do score deve continuar a ser strings simples sob o contrato estrito: um cliente
estrito compara o legend devolvido com os critérios que enviou, e o Laya renderiza um critério
estruturado com o JSON do Python, o que um chamador de JavaScript que transforma os seus próprios
critérios em string pode não igualar byte a byte.
As respostas bem-sucedidas também transportam Server-Timing: inference;dur=<ms> e
X-Inference-Time-Ms.
Limites
Os guardrails de pedido são verificados antes da tokenização, por isso um pedido demasiado grande só
custa ao servidor os bytes que leu. Todos eles são um 413; o detail diz que limite foi atingido.
| limite | valor |
|---|---|
| corpo do pedido | 2 MiB, aplicado durante o streaming – um Content-Length em chunked ou subestimado não o consegue contornar |
state |
50,000 caracteres do texto que é dado ao modelo – a própria string para um state em string, json.dumps(state, ensure_ascii=False) para um objeto ou array |
| perguntas por pedido | 64 |
states por pedido em lote |
64 |
opções por pergunta choice |
100 |
níveis por pergunta score |
32 |
| opções em todas as perguntas | 512 |
| pedidos concorrentes admitidos | LAYA_MAX_CONCURRENT (16) |
/v1/systemone/batch é limitado de outra forma, e não por uma recusa. Tokeniza cada state uma vez por
pergunta e agrupa cada linha num único tensor, por isso os tetos de campo multiplicam-se: 64 states de 64
perguntas são 4096 linhas, que todos os outros limites desta página permitem. O que uma linha custa é a
sua largura, e max_len é ele mesmo um campo de pedido, por isso o custo de um lote é
states x questions x width.
Em vez de recusar um lote grande, o endpoint divide-o: quando esse produto excede
LAYA_MAX_BATCH_TOKENS (131.072 por predefinição) escolhe um batch_size para que cada passagem direta
fique dentro do orçamento, e Router.predict_batch executa o lote em várias passagens. Todos os states
continuam a ser respondidos e a resposta não muda. Um pedido cujas linhas já cabem não recebe nenhum
batch_size, por isso comporta-se exatamente como antes – o que importa porque a forma do lote pode
mover resultados de vírgula flutuante. Um batch_size enviado por quem chama ganha sempre: pediu uma
forma.
Na predefinição, 256 linhas passam numa passagem – 64 states de 4 perguntas, ou 8 de 32. Lotes maiores
são divididos, e aumentar max_len torna cada passagem mais estreita em vez de custar 16 vezes o
trabalho. O que isto não limita é quanto tempo um pedido ocupa o servidor; isso é LAYA_MAX_CONCURRENT
e o único worker de inferência, e já é verdade para um pedido /v1/systemone sobre um state de 50.000
caracteres.
Os limites de opções são guardas de amplificação apenas de HTTP; o próprio modelo encaixa os tokens
de opção numa janela head_max_len=192, por isso uma pergunta dentro dos limites HTTP pode ainda
ser recusada como 422 quando os textos das opções juntos excedem esse orçamento. O
Harness de avaliação corre os mesmos pedidos em processo sem a camada HTTP.
Erros
| estado | quando | detail do corpo |
|---|---|---|
400 |
o corpo não é JSON válido, não é um objeto, não tem questions, state falta ou é null, questions não é um objeto, ou uma string em qualquer lugar do corpo contém um escape de substituto \udXXX sem par |
o que está errado |
401 |
LAYA_API_KEY está definida e o token bearer falta ou está errado |
invalid or missing bearer token |
413 |
qualquer limite acima | que limite e por quanto |
422 |
a pergunta é JSON bem formado mas inválida para o Laya (tipo desconhecido, opções acima do orçamento da cabeça), ou um controlo de pedido (lang, min_confidence, um argumento de hook) não está na forma que este ponto final aceita |
nomeia a pergunta ou o campo e o que corrigir |
500 |
a inferência falhou por qualquer outro motivo | inference failed – sempre esta string, para que caminhos, pesos e estado de memória nunca escapem; a causa está no log do servidor |
503 |
LAYA_MAX_CONCURRENT pedidos já estão em voo |
server busy, try again later |
O 400 de substituto sem par é o que parece invulgar. \udXXX sem par é JSON legal, mas o caractere
que nomeia não pode ser codificado em UTF-8, por isso o tokenizador lança um TypeError – a própria
string de quem chama a chegar como uma falha do servidor, com um traceback por pedido. Ambas as rotas de
decisão percorrem, portanto, o corpo analisado à procura de substitutos isolados e recusam um antes de
chegar à inferência. O percurso corre depois das verificações de tamanho, por isso um corpo
demasiado grande continua a ser recusado primeiro e os limites de caracteres e perguntas limitam o que
ele pode alcançar. Um substituto emparelhado é um caractere astral comum quando o analisador termina,
por isso um emoji num state não é afetado.
A carga acima do limite é recusada, não posta em fila: clientes que seguram um slot de admissão
enquanto fazem streaming de um corpo lento não conseguem esfomear o /health, e uma nova tentativa
pode ocupar o slot que um cliente recusado deixou.
Modelo de concorrência
A inferência é uma chamada síncrona da torch que leva de centenas de milissegundos a segundos na
CPU, por isso nunca corre no event loop: os pedidos são entregues a um executor de um só worker, o
que significa uma passagem direta de cada vez – a forma que um único checkpoint num só dispositivo
quer. A admissão (o semáforo LAYA_MAX_CONCURRENT) é verificada antes de ser lido qualquer byte do
corpo e mantida durante a inferência; o gate de inferência só é juntado depois de o corpo estar
completo, por isso um cliente lento segura um slot de admissão mas nunca um slot de inferência.
Ainda não (por agora)
Este servidor fala um só protocolo de propósito. Não há um endpoint compatível com a OpenAI; corre antes
várias perguntas num só pedido, já que partilham uma única passagem direta por conjunto de perguntas. A
outra rota é POST /v1/systemone/batch, que responde a um conjunto questions sobre um array de
states. Ainda não tem secção nesta página – a sua forma de pedido está na secção de self-hosting do
README – e todas as verificações acima se lhe aplicam como a POST /v1/systemone: os mesmos 400 de
forma, a mesma recusa de substituto sem par, a mesma auth, admissão, limites de tamanho, validação de
controlos do corpo e mapeamento de 500. A CLI laya e o servidor MCP cobrem o uso local – vê o
README.