API HTTP
O laya-serve expõe o Laya sobre o protocolo de transmissão /v1/systemone do TypeSafe Jev. Um
cliente escrito para o Jev – hs-jev, typesafe-sdk, ou o seu próprio – pode apontar sua URL base
para este servidor e continuar funcionando: a saída de predict() do Laya já é compatível com o
esquema, e o servidor adiciona apenas a superfície HTTP: uma rota de decisão, uma sonda de saúde, uma
checagem opcional de bearer e limites de solicitação.
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 retorna uma instância, lê GET /health para uma checagem de implantação e traz um dublê de teste para que quem chama possa fazer testes de unidade sem um servidor em execução.
pip install "laya[serve]"
laya-serve # http://0.0.0.0:8000
O mesmo ponto de entrada roda embutido em qualquer servidor ASGI: laya.serve.create_app() constrói o
app FastAPI, opcionalmente com um Router que você injeta (create_app(router)) em vez de um
construído a partir do ambiente.
Configuração
Tudo são variáveis de ambiente, então uma única imagem serve uma execução de desenvolvimento em laptop e uma unit do systemd.
| var de ambiente | significado | padrã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 proxy reverso | vazio |
LAYA_DEVICE |
dispositivo torch para todo checkpoint | auto |
LAYA_PRELOAD |
constrói os checkpoints na inicialização, não de forma preguiçosa | 1 |
LAYA_MODELS |
lista separada por vírgulas a pré-carregar (english,multilingual,typed-decisions); vazio = todos |
todos |
LAYA_THREADS |
limita threads intra-op do torch em CPU; mantenha <= núcleos físicos – exceder os núcleos lógicos é uma regressão grande | padrão do torch |
LAYA_AUTO_TASK |
roteia automaticamente para o checkpoint typed-decisions | 0 |
LAYA_IDLE_UNLOAD_SECONDS |
descarrega os checkpoints residentes após tantos segundos ociosos; a próxima solicitação carrega seu checkpoint de novo. 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 do mesmo jeito que o core os resolve, e um nome não resolvível para o servidor na inicialização |
english |
LAYA_API_KEY |
se definido, exige Authorization: Bearer <key> |
nenhum |
LAYA_LOG_LEVEL |
nível de log do uvicorn | info |
LAYA_MAX_CONCURRENT |
solicitações admitidas de uma vez após a autenticação; o excesso recebe 503 |
16 |
LAYA_MAX_BATCH_TOKENS |
tokens que uma PASSADA DIRETA de /v1/systemone/batch pode agrupar (states x perguntas x largura da linha); um lote maior é dividido em várias passadas, 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 extras |
0 |
Para uma implantação publicada sob um prefixo como /laya, defina LAYA_ROOT_PATH=/laya. O FastAPI
o usa ao gerar as URLs do OpenAPI e do Swagger UI. Configure o proxy reverso para remover /laya
antes de repassar as solicitações ao Laya; as rotas do app continuam /health e /v1/systemone
internamente.
Para contêineres, incluindo imagens CUDA e ARM64, veja Início rápido com Docker.
Para uso local em rajadas, defina LAYA_IDLE_UNLOAD_SECONDS=300. A inferência e o descarregamento rodam no mesmo worker, e a janela de ociosidade recomeça quando uma passada direta única ou em lote termina, incluindo solicitações que falharam. A próxima predição paga um carregamento a frio. O descarregamento libera referências de modelo e caches de dispositivo, incluindo Metal; o alocador do processo pode reter páginas de RAM, então o RSS do processo não precisa cair no tamanho do checkpoint.
Endpoints
GET /health
Sempre aberto (sem autenticação), e continua responsivo durante a inferência porque a passada direta
limitada por CPU roda em seu próprio worker, não no event loop. Os campos abaixo de liveness não estão
abertos em uma implantação que definiu LAYA_API_KEY: sem o bearer, /health responde
{"status": "ok"} e nada mais, porque o resto nomeia os checkpoints residentes, 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 host. Uma sonda só precisa do 200, então uma checagem de saúde não é afetada e um bearer errado ainda
é um 200 em vez de um 401. Sem LAYA_API_KEY definido, todo chamador recebe 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, então 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 por campo.
Com o descarregamento por ociosidade ativado, respostas de saúde autenticadas também incluem
idle_unload_seconds (a janela configurada) e idle_seconds (tempo desde a última solicitação de
inferência ou sua conclusão). Sondas de saúde não reiniciam esse relógio. Uma lista loaded vazia é
normal após um descarregamento por ociosidade.
statuséoksempre que o processo responde. Não diz nada sobre os checkpoints.loadedlista os checkpoints residentes em memória. Fica vazio até uma solicitação construir um, que é o queLAYA_PRELOAD=0deixa o processo fazendo.revisionsé a revisão de artefato de onde cada checkpoint residente foi carregado, com chave pelos mesmos nomes deloaded, então uma implantação pode confirmar o que está de fato servindo.deviceé o dispositivo em que um checkpoint residente realmente calcula, que nem sempre é o queLAYA_DEVICEpediu: um checkpoint que quer uma GPU que não consegue obter recai silenciosamente na 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 relatando sua configuração e um servidor relatando onde seu trabalho acontece: um que perdeu sua GPU em silêncio dizfalsecomdevicecpu, em vez de continuar respondendocuda.checkpoint_devicesdá a medição por checkpoint, com chave pelos nomes emloaded;deviceé o primeiro desses valores.cpu_fallbacksconta, por checkpoint residente, as solicitações que esgotaram a memória da GPU e foram repetidas uma vez na CPU:countdesde o início do processo, elast_reasoncom o texto de erro da última. O rebaixamento é limitado à solicitação que falhou, então 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
Uma solicitação carrega 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, e-mail, ticket ou documento JSON sobre o qual decidir; um state ausente 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 (veja abaixo) |
task |
não | força um checkpoint por nome de fluxo de trabalho em vez de deixar o roteamento decidir; um nome desconhecido é um 422 que o nomeia |
lang |
não | um código de idioma (de, en-US) que pula a detecção quando nomeia um idioma; um código em branco ou não reconhecido cai na detecção |
lang_guess |
não | um código de idioma do próprio identificador do cliente, consultado depois de lang e antes da detecção; qualquer código não inglês roteia para o checkpoint multilíngue |
max_len |
não | janela total de tokens para esta solicitação, limitada por LAYA_MAX_TOKEN_BUDGET |
head_max_len |
não | janela de tokens que o prompt de opções compartilha, com o mesmo teto; veja Ampliar 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 cuja answer_confidence fica 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 declarar; cada um é repassado apenas quando a
solicitação o envia, então um ausente deixa em vigor a própria configuração Router(...) da
implantação. 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 callable que roda dentro do processo do servidor, e os dois últimos dizem como os hooks
que uma implantação instalou executam, então nenhum valor que um chamador envie tem significado aqui.
Os mesmos cinco são recusados no lado do cliente por um nó LangChain com um base_url
(laya.integrations.langchain), então uma chain e um cliente HTTP cru agora recebem a mesma
resposta.
model é aceito para que um cliente Jev possa continuar enviando um. Os ids públicos do Hugging Face
(convaiinnovations/laya-multilingual, convaiinnovations/laya-typed-decisions), os nomes de
checkpoint (english, multilingual, typed-decisions) e 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 do Jev como jev-1 – significa “deixe o router escolher”, e o bloco routing da
resposta registra o que foi escolhido e por quê. Um valor que parece um caminho do sistema de arquivos
ou um id de Hub não publicado (/path/to/checkpoint, org/repo, ~/ckpt, .\ckpt) é um 422 tanto em
/v1/systemone quanto em /v1/systemone/batch: este servidor não consegue carregá-lo, 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: a solicitação acima, o checkpoint
english em cache na CPU. answers e usage são as chaves que os clientes Jev decodificam; 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 esperado do nível, pode ficar entre níveis), probabilities indexadas "0".. "k-1", legend mapeando í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 – veja 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 rodou. Uma solicitação que define min_confidence o obtém; uma 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 uma solicitação
com porta: passed (sua confiança passou do limiar), abstained (ficou abaixo, e low_confidence é
true exatamente nessas respostas), ou unevaluated (a resposta não carregava confiança utilizável,
então a porta não pôde decidir – relatar isso como um passe seria a mesma mentira que relatar 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 fato. 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, então os estados são relatados, e nada pode cair
abaixo dele, então toda resposta se lê passed – o 0.0 devolvido é o que distingue isso de um passe
em um limiar real. A resposta em si é mantida em todos os estados; a porta marca, não descarta.
usage relata do que a passada 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 se move com max_len, head_max_len e o prompt de
opções de cada pergunta (#174), então essas chaves são o único lugar onde esse fato é visível:
chave de usage |
significado |
|---|---|
input_tokens |
tokens não-pad das linhas do state – uma linha por pergunta, então cresce com as perguntas em vez de ser um comprimento de contexto |
output_tokens |
sempre 0 – a cabeça responde em uma passada, não gera nada |
state_tokens |
tokens que o state serializado inteiro precisa |
state_tokens_dropped |
tokens disso que ao 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 só quando as opções de alguma pergunta não têm mais cada uma um trecho de tokens: com chave por id de pergunta, com total (as opções que aquela pergunta define), distinct (os trechos que chegaram à sequência) e tokens_per_option |
Uma resposta truncada ainda é uma resposta – a cabeça decide com a evidência que recebeu – mas quem chama que dimensiona states por contagem de caracteres não consegue ver o corte em nenhum outro lugar da resposta.
routing registra qual checkpoint respondeu e por quê:
chave de routing |
significado |
|---|---|
model |
o checkpoint que respondeu: english, multilingual ou typed-decisions |
repo |
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 todo caminho que decide 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 relatam 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 na resposta reportada (max(p)). É a quantidade que o escalonamento por temperatura ajusta e sobre a qual os números de ECE deste repositório são calculados, então carrega a propriedade de gating em que a página Benchmarks e limites conhecidos se apoia – mas apenas para um checkpoint cujo ajuste de temperatura foi validado no seu tráfego.confidencesignifica algo diferente por tipo: entropia normalizada1 - H(p)/log(k)emchoiceescore, emax(p_yes, p_no)emnoul(onde ela é igual aanswer_confidence).
Nunca compare os dois contra um único limiar. Note também a diferença ao portar do Jev: o TypeSafe
define confiança como (n*p_max - 1)/(n - 1), então um limiar trazido de uma implantação Jev aplica
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 pode submetê-la 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 extras – o plugin de provedor TypeSafe do OpenClaw é um – rejeita a carga completa,
então LAYA_JEV_STRICT=1 projeta a resposta sobre o contrato antes de responder, tanto em
/v1/systemone quanto em /v1/systemone/batch:
- a raiz mantém só
model,answerseusage;routingnão é enviado; - uma resposta
choicemantémchoice,probabilitieseconfidence; - uma resposta
scoremantémscore,probabilities,confidenceelegend; - uma resposta
noulmantém sónoul; usagemantéminput_tokenseoutput_tokens; os fatos de truncamento e o teto de opções colapsadas não são enviados.
A projeção mantém só as chaves contratadas e não recalcula nada: cada valor é o que o resultado já
carrega, então as probabilidades e pontuações que um cliente estrito lê são idênticas às que a carga
completa relata. O padrão continua sendo a carga completa, e uma implantação que liga a flag perde a
visibilidade de truncamento que usage oferece – um state cortado fica então visível nos logs, não na
resposta. O criteria do score deve continuar sendo 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 seus próprios critérios
em string pode não igualar byte a byte.
Respostas bem-sucedidas também carregam Server-Timing: inference;dur=<ms> e X-Inference-Time-Ms.
Limites
Os guardrails de solicitação são verificados antes da tokenização, então uma solicitação grande demais
custa ao servidor apenas os bytes que ele leu. Todos eles são um 413; o detail diz qual limite foi
atingido.
| limite | valor |
|---|---|
| corpo da solicitação | 2 MiB, aplicado durante o streaming – um Content-Length em chunks ou subdeclarado não consegue contornar |
state |
50.000 caracteres do texto dado ao modelo – a própria string para um state string, json.dumps(state, ensure_ascii=False) para um objeto ou array |
| perguntas por solicitação | 64 |
states por solicitação em lote |
64 |
opções por pergunta choice |
100 |
níveis por pergunta score |
32 |
| opções em todas as perguntas | 512 |
| solicitações concorrentes admitidas | LAYA_MAX_CONCURRENT (16) |
/v1/systemone/batch é limitado de outro jeito, e não por uma recusa. Ele tokeniza cada state uma vez
por pergunta e agrupa cada linha em um único tensor, então os tetos de campo se multiplicam: 64 states de
64 perguntas são 4096 linhas, que todo outro limite desta página permite. O que uma linha custa é sua
largura, e max_len é ele mesmo um campo de solicitação, então o custo de um lote é
states x questions x width.
Em vez de recusar um lote grande, o endpoint o divide: quando esse produto excede
LAYA_MAX_BATCH_TOKENS (131.072 por padrão) ele escolhe um batch_size para que cada passada direta
fique dentro do orçamento, e Router.predict_batch executa o lote em várias passadas. Todo state ainda
é respondido e a resposta não muda. Uma solicitação cujas linhas já cabem não recebe nenhum
batch_size, então se comporta exatamente como antes – o que importa porque a forma do lote pode mover
resultados de ponto flutuante. Um batch_size enviado por quem chama sempre vence: ele pediu uma forma.
No padrão, 256 linhas passam em uma passada – 64 states de 4 perguntas, ou 8 de 32. Lotes maiores são
divididos, e aumentar max_len torna cada passada mais estreita em vez de custar 16 vezes o trabalho. O
que isso não limita é quanto tempo uma solicitação ocupa o servidor; isso é LAYA_MAX_CONCURRENT e o
único worker de inferência, e já é verdade para uma solicitação /v1/systemone sobre um state de 50.000
caracteres.
Os tetos de opções são guardas de amplificação apenas do HTTP; o modelo em si encaixa os tokens das
opções em uma janela head_max_len=192, então uma pergunta dentro dos tetos HTTP ainda pode ser
recusada como um 422 quando os textos das opções juntos excedem esse orçamento. O
Harness de avaliação roda as mesmas solicitações em processo, sem a camada HTTP.
Erros
| status | quando | detail do corpo |
|---|---|---|
400 |
o corpo não é JSON válido, não é um objeto, não tem questions, state está ausente 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á definido e o bearer token está ausente ou errado |
invalid or missing bearer token |
413 |
qualquer limite acima | qual 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 controle de solicitação (lang, min_confidence, um argumento de hook) não está na forma que este endpoint 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 vazem; a causa está no log do servidor |
503 |
LAYA_MAX_CONCURRENT solicitações já estão em andamento |
server busy, try again later |
O 400 de substituto sem par é o que parece incomum. \udXXX sem par é JSON legal, mas o caractere que
ele nomeia não pode ser codificado em UTF-8, então o tokenizador lança um TypeError – a própria string
de quem chama chegando como uma falha do servidor, com um traceback por solicitação. As duas rotas de
decisão, portanto, percorrem o corpo analisado em busca de substitutos isolados e recusam um antes que
chegue à inferência. O percurso roda depois das checagens de tamanho, então um corpo grande demais ainda
é 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, então um emoji em um state não é
afetado.
A carga acima do teto é recusada, não enfileirada: clientes que seguram um slot de admissão enquanto
fazem streaming de um corpo lento não conseguem deixar o /health sem recursos, e uma nova tentativa
pode pegar o slot que um cliente recusado deixou.
Modelo de concorrência
A inferência é uma chamada síncrona do torch que leva de centenas de milissegundos a segundos em CPU,
então ela nunca roda no event loop: as solicitações são entregues a um executor de worker único, o que
significa uma passada direta por vez – o formato que um único checkpoint em um dispositivo quer. A
admissão (o semáforo LAYA_MAX_CONCURRENT) é verificada antes de qualquer byte do corpo ser lido e é
mantida durante a inferência; o gate de inferência só é adquirido depois que o corpo está completo,
então um cliente lento segura um slot de admissão mas nunca um slot de inferência.
Não está (ainda) aqui
Este servidor fala um protocolo de propósito. Não há endpoint compatível com OpenAI; rode várias
perguntas em uma solicitação em vez disso, já que elas compartilham uma única passada direta por
conjunto de perguntas. A outra rota é POST /v1/systemone/batch, que responde um conjunto de questions
sobre um array de states. Ela ainda não tem seção nesta página – sua forma de solicitação está na
seção de auto-hospedagem do README – e toda checagem acima se aplica a ela 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 controles do corpo e mapeamento de 500. A CLI laya e o
servidor MCP cobrem o uso local – veja o
README.