Documentação

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 é ok sempre que o processo responde. Não diz nada sobre os checkpoints.
  • loaded lista os checkpoints residentes em memória. Fica vazio até uma solicitação construir um, que é o que LAYA_PRELOAD=0 deixa o processo fazendo.
  • revisions é a revisão de artefato de onde cada checkpoint residente foi carregado, com chave pelos mesmos nomes de loaded, 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 que LAYA_DEVICE pediu: 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 é true exatamente enquanto nada está residente, e false assim 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 diz false com device cpu, em vez de continuar respondendo cuda.
  • checkpoint_devices dá a medição por checkpoint, com chave pelos nomes em loaded; device é o primeiro desses valores.
  • cpu_fallbacks conta, por checkpoint residente, as solicitações que esgotaram a memória da GPU e foram repetidas uma vez na CPU: count desde o início do processo, e last_reason com 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 conta 0 aqui – isso aparece em device.

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.
  • confidence significa algo diferente por tipo: entropia normalizada 1 - H(p)/log(k) em choice e score, e max(p_yes, p_no) em noul (onde ela é igual a answer_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, answers e usage; routing não é enviado;
  • uma resposta choice mantém choice, probabilities e confidence;
  • uma resposta score mantém score, probabilities, confidence e legend;
  • uma resposta noul mantém só noul;
  • usage mantém input_tokens e output_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.