Documentação

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 é ok sempre que o processo responde. Não diz nada sobre os checkpoints.
  • loaded lista os checkpoints residentes em memória. Fica vazio até um pedido construir um, que é o que LAYA_PRELOAD=0 deixa o processo a fazer.
  • revisions é a revisão de artefacto a partir da qual cada checkpoint residente foi carregado, com chave pelos mesmos nomes de loaded, 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 que LAYA_DEVICE pediu: 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 é true exatamente enquanto nada está residente, e false assim 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 diz false com device cpu, em vez de continuar a responder 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, os pedidos que esgotaram a memória da GPU e foram repetidos uma vez na CPU: count desde o início do processo, e last_reason com 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 conta 0 aqui – isso aparece em device.

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.
  • 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 é igual a answer_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, 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 apenas noul;
  • usage mantém input_tokens e output_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.