API HTTP
laya-serve expose Laya via le protocole filaire /v1/systemone de TypeSafe Jev. Un client écrit contre
Jev – hs-jev, typesafe-sdk, ou le tien – peut pointer son URL de base vers ce serveur et continuer
de fonctionner : la sortie predict() de Laya est déjà compatible avec le schéma, et le serveur n’ajoute
que la surface HTTP : une route de décision, une sonde de santé, une vérification Bearer optionnelle et
des limites de requête.
Il existe un client PHP qui cible laya-serve plutôt que l’API Jev : marcreichel/laya-php est un SDK Composer (PHP 8.4+) qui mappe une classe d’enums et d’attributs sur des questions et renvoie une instance, lit GET /health pour une vérification de déploiement, et fournit un faux de test pour que les appelants puissent faire des tests unitaires sans serveur en marche.
pip install "laya[serve]"
laya-serve # http://0.0.0.0:8000
Le même point d’entrée tourne embarqué dans n’importe quel serveur ASGI : laya.serve.create_app()
construit l’application FastAPI, éventuellement avec un Router que tu injectes (create_app(router))
au lieu d’un construit depuis l’environnement.
Configuration
Tout passe par des variables d’environnement, donc une seule image sert une exécution de dev sur portable et une unité systemd.
| variable d’env | signification | défaut |
|---|---|---|
LAYA_HOST |
adresse d’écoute | 0.0.0.0 |
LAYA_PORT |
port d’écoute | 8000 |
LAYA_ROOT_PATH |
préfixe d’URL public quand servi derrière un reverse proxy | vide |
LAYA_DEVICE |
appareil torch pour chaque checkpoint | auto |
LAYA_PRELOAD |
construit les checkpoints au démarrage, pas paresseusement | 1 |
LAYA_MODELS |
liste séparée par des virgules à précharger (english,multilingual,typed-decisions) ; vide = tous |
tous |
LAYA_THREADS |
plafonne les threads intra-op de torch sur CPU ; reste <= cœurs physiques – sursouscrire les cœurs logiques est une grosse régression | défaut torch |
LAYA_AUTO_TASK |
auto-route vers le checkpoint typed-decisions | 0 |
LAYA_IDLE_UNLOAD_SECONDS |
décharge les checkpoints résidents après ce nombre de secondes d’inactivité ; la requête suivante recharge son checkpoint. Zéro désactive la décharge | 0 |
LAYA_DEFAULT_MODEL |
checkpoint sur lequel retombe un état sans indice de langue ; les alias comme ml se résolvent comme core les résout, et un nom non résoluble arrête le serveur au démarrage |
english |
LAYA_API_KEY |
si définie, exige Authorization: Bearer <key> |
aucune |
LAYA_LOG_LEVEL |
niveau de log uvicorn | info |
LAYA_MAX_CONCURRENT |
requêtes admises d’un coup au-delà de l’auth ; l’excès reçoit 503 |
16 |
LAYA_MAX_BATCH_TOKENS |
jetons qu’une PASSE AVANT de /v1/systemone/batch peut regrouper (states x questions x largeur de ligne) ; un lot plus grand est scindé en plusieurs passes, pas refusé |
131072 |
LAYA_JEV_STRICT |
sert le contrat filaire Jev strict : pas de routing racine, pas d’action / answer_confidence par réponse, pas de confidence sur les réponses noul, et usage réduit à input_tokens + output_tokens. Pour les clients qui valident la réponse contre le contrat Jev sans champs supplémentaires |
0 |
Pour un déploiement publié sous un préfixe comme /laya, définis LAYA_ROOT_PATH=/laya.
FastAPI l’utilise pour générer les URL d’OpenAPI et de Swagger UI. Configure le reverse proxy pour
retirer /laya avant de transmettre les requêtes à Laya ; les routes de l’application restent /health
et /v1/systemone en interne.
Pour les conteneurs, y compris les images CUDA et ARM64, voir Démarrage rapide Docker.
Pour un usage local en rafales, définis LAYA_IDLE_UNLOAD_SECONDS=300. L’inférence et la décharge tournent sur le même worker, et la fenêtre d’inactivité redémarre quand une passe avant, seule ou par lots, se termine, y compris pour les requêtes échouées. La prédiction suivante paie un chargement à froid. La décharge libère les références de modèle et les caches d’appareil, y compris Metal ; l’allocateur du processus peut retenir des pages de RAM, donc le RSS du processus n’a pas forcément à baisser de la taille du checkpoint.
Endpoints
GET /health
Toujours ouvert (sans auth), et reste réactif pendant l’inférence parce que la passe avant liée au CPU
tourne sur son propre worker, pas sur la boucle d’événements. Les champs sous liveness ne sont pas
ouverts sur un déploiement qui a défini LAYA_API_KEY : sans le bearer, /health répond
{"status": "ok"} et rien d’autre, parce que le reste nomme les checkpoints résidents, leurs SHAs de
révision exacts, l’état de l’appareil et le dernier motif de repli de chaque checkpoint, ce qui cite le
matériel de l’hôte. Une sonde n’a besoin que du 200, donc une vérification de santé n’est pas affectée et
un bearer erroné reste un 200 plutôt qu’un 401. Sans LAYA_API_KEY défini, chaque appelant reçoit la
charge complète montrée ici.
{"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}}}
La réponse d’un seul serveur, donc les blocs concordent entre eux : chaque clé de revisions,
checkpoint_devices et cpu_fallbacks est un nom dans loaded. tests/test_serve.py tient cet
exemple contre le gestionnaire qui le produit, champ par champ.
Quand la décharge par inactivité est activée, les réponses de santé authentifiées incluent aussi
idle_unload_seconds (la fenêtre configurée) et idle_seconds (temps depuis la dernière requête
d’inférence ou son achèvement). Les sondes de santé ne réinitialisent pas cette horloge. Une liste
loaded vide est normale après une décharge par inactivité.
statusestokdès que le processus répond, tout simplement. Il ne dit rien sur les checkpoints.loadedliste les checkpoints résidents en mémoire. Il est vide jusqu’à ce qu’une requête en construise un, ce queLAYA_PRELOAD=0laisse le processus faire.revisionsest la révision d’artefact depuis laquelle chaque checkpoint résident a été chargé, indexée par les mêmes noms queloaded, pour qu’un déploiement puisse confirmer ce qu’il sert réellement.deviceest l’appareil sur lequel un checkpoint résident calcule réellement, ce qui n’est pas toujours ce queLAYA_DEVICEa demandé : un checkpoint qui veut un GPU qu’il ne peut pas obtenir retombe silencieusement sur CPU et répond quand même correctement. Sans rien de résident, c’est la préférence configurée à la place.device_is_preferenceesttrueexactement tant que rien n’est résident, etfalsedès que le gestionnaire peut mesurer. C’est la différence entre un serveur qui rapporte sa configuration et un serveur qui rapporte où son travail se fait : un serveur qui a perdu son GPU en silence ditfalseavecdevicecpu, plutôt que de continuer à répondrecuda.checkpoint_devicesdonne la mesure par checkpoint, indexée par les noms dansloaded;deviceest la première de ces valeurs.cpu_fallbackscompte, par checkpoint résident, les requêtes qui ont épuisé la mémoire du GPU et ont été réessayées une fois sur CPU :countdepuis le démarrage du processus, etlast_reasonportant le texte d’erreur de la dernière. La rétrogradation est limitée à la requête qui a échoué, donc un checkpoint construit sur CPU parce que le GPU n’a jamais été disponible n’est pas un repli et compte0ici – cela se voit dansdevice.
POST /v1/systemone
Une requête porte un state et un nombre quelconque de questions dessus :
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"]}
}
}'
| champ | requis | signification |
|---|---|---|
state |
oui | texte, email, ticket ou document JSON sur lequel décider ; un état manquant ou null est un 400 |
questions |
oui | objet indexé par id de question ; chaque question est choice / score / noul avec instructions et criteria |
model |
non | nomme un checkpoint ; un chemin ou un id Hub non publié est un 422, toute autre valeur est ignorée (voir ci-dessous) |
task |
non | force un checkpoint par nom de workflow au lieu de laisser le routage décider ; un nom inconnu est un 422 qui le nomme |
lang |
non | un code de langue (de, en-US) qui saute la détection quand il nomme une langue ; un code vide ou non reconnu retombe sur la détection |
lang_guess |
non | un code de langue issu de l’identificateur propre du client, consulté après lang et avant la détection ; tout code non anglais route vers le checkpoint multilingue |
max_len |
non | fenêtre de jetons totale pour cette requête, plafonnée par LAYA_MAX_TOKEN_BUDGET |
head_max_len |
non | fenêtre de jetons que partage le prompt d’options, même plafond ; voir Élargir le budget de jetons pour savoir quand une question en a besoin |
min_confidence |
non | seuil d’abstention dans [0.0, 1.0] ; une réponse dont l’answer_confidence tombe en dessous revient marquée low_confidence, et la réponse elle-même est conservée |
model, task, lang, lang_guess, max_len, head_max_len et min_confidence sont les arguments
que prend Router.predict qu’un corps JSON peut énoncer ; chacun n’est transmis que quand la requête
l’envoie, donc un absent laisse le réglage propre du déploiement Router(...) aux commandes. Les cinq
arguments de hook que prend aussi predict – hooks, on_predict_start, on_predict_end,
hooks_raise, hooks_timeout – sont refusés avec un 422 plutôt que supprimés : un hook est un
appelable qui tourne à l’intérieur du processus serveur, et les deux derniers disent comment s’exécutent
les hooks qu’un déploiement a installés, donc aucune valeur qu’envoie un appelant n’a de sens ici. Les
mêmes cinq sont refusés côté client par un nœud LangChain avec un base_url
(laya.integrations.langchain), donc une chaîne et un client HTTP brut obtiennent maintenant la même
réponse.
model est accepté pour qu’un client Jev puisse continuer d’en envoyer un. Les ids publics Hugging Face
(convaiinnovations/laya-multilingual, convaiinnovations/laya-typed-decisions), les noms de checkpoint
(english, multilingual, typed-decisions) et leurs alias sélectionnent un checkpoint . convaiinnovations/laya, et toute autre valeur qui n’est ni un chemin ni un id de dépôt Hub – y compris un id Jev comme jev-1 – signifie « laisse le routeur choisir », et le bloc
routing de la réponse enregistre ce qui a été choisi et pourquoi. Une valeur qui ressemble à un chemin de système de fichiers ou à un id Hub non publié (/path/to/checkpoint, org/repo, ~/ckpt, .\ckpt) est un 422 sur /v1/systemone comme sur /v1/systemone/batch : ce serveur ne peut pas le charger, et répondre avec un autre checkpoint le masquerait. Le detail est le même texte unknown model que lève core, plus le rappel d’omettre model pour laisser le routeur choisir.
Réponse
{
"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}
}
L’exemple est une réponse que ce serveur a donnée, mot pour mot : la requête ci-dessus, le checkpoint
english en cache sur CPU. answers et usage sont les clés que décodent les clients Jev ; model est le nom constant de la tête de décision, et le checkpoint qui a répondu est dans routing.
| type de réponse | clés |
|---|---|
choice |
choice (l’option argmax), probabilities par option |
score |
score (index de niveau attendu, peut tomber entre les niveaux), probabilities indexées "0".. "k-1", legend faisant correspondre l’index au texte du niveau |
noul |
noul, la probabilité de l’option oui |
| tous | confidence, answer_confidence, et action.act_probability |
| gate | abstention, abstention_threshold et low_confidence, écrits par la porte d’abstention – voir ci-dessous |
La ligne gate est le rapport d’abstention (#361), et c’est la seule façon pour un appelant de voir que la
porte qu’il a payée a tourné. Une requête qui définit min_confidence l’obtient ; une qui ne le fait pas
n’obtient aucune des trois clés. abstention est l’un de trois états, écrit sur chaque réponse d’une
requête soumise à la porte : passed (sa confiance a franchi le seuil), abstained (elle est tombée en
dessous, et low_confidence est true exactement sur ces réponses), ou unevaluated (la réponse ne
portait aucune confiance exploitable, donc la porte n’a pas pu décider – le rapporter comme un succès
serait le même mensonge que le rapporter comme un flag). abstention_threshold renvoie le seuil contre
lequel ces états ont été mesurés, ce qui rend un lot exécuté avec des seuils par classe re-scindable
après coup. Sans min_confidence défini, aucune des trois clés n’apparaît sur aucune réponse : l’absence
est le rapport, pas un quatrième état, et c’est ainsi qu’un appelant distingue une exécution sans porte
d’une porte franchie. Un min_confidence de exactement 0.0 a bien été défini, donc les états sont
rapportés, et rien ne peut tomber en dessous, donc chaque réponse se lit passed – le 0.0 renvoyé est
ce qui distingue cela d’un succès à un seuil réel. La réponse elle-même est conservée dans chaque état ;
la porte marque, elle ne supprime pas.
usage rapporte ce à partir de quoi la passe avant a été construite. La part d’un état que le modèle lit
est un budget de jetons, pas un nombre de caractères, et le budget bouge avec max_len, head_max_len
et le prompt d’options de chaque question (#174), donc ces clés sont le seul endroit où ce fait est
visible :
clé de usage |
signification |
|---|---|
input_tokens |
jetons non-pad des lignes de l’état – une ligne par question, donc il croît avec les questions plutôt que d’être une longueur de contexte |
output_tokens |
toujours 0 – la tête répond en une passe, elle ne génère rien |
state_tokens |
jetons dont l’état sérialisé entier a besoin |
state_tokens_dropped |
jetons de cela qu’au moins une question n’a pas reçus : le pire cas sur les questions, chacune laissant à l’état un espace différent |
truncated |
true quand ce pire cas a supprimé quelque chose |
truncated_questions |
les ids des questions dont la propre fenêtre a été coupée, [] quand aucune |
options |
présent uniquement quand les options d’une question n’ont plus chacune un intervalle de jetons : indexé par id de question, avec total (les options que cette question définit), distinct (les intervalles qui ont atteint la séquence) et tokens_per_option |
Une réponse tronquée est quand même une réponse – la tête décide sur les éléments qu’on lui a donnés – mais un appelant qui dimensionne les états au nombre de caractères ne peut voir la coupure nulle part ailleurs dans la réponse.
routing enregistre quel checkpoint a répondu et pourquoi :
clé de routing |
signification |
|---|---|
model |
le checkpoint qui a répondu : english, multilingual ou typed-decisions |
repo |
son id Hugging Face public |
reason |
la phrase du choix, qui nomme les éléments sur lesquels il a agi |
detection |
laya.lang.analyse() sur l’état – script, script_profile, language, is_english, language_undecided, diacritic_rate, non_latin_fraction, mixed_segment – ou null quand la route a décidé avant de lire le texte |
workflow |
le workflow typed-decisions auquel les ids de question correspondent, ou null |
detection est null sur chaque chemin qui décide sans lire l’état : un forcé par model ou task, un
répondu par lang ou lang_guess, ou un qui correspondait à un workflow typed-decisions d’après les
ids de question. Un lang_guess ne laisse aucune clé propre – l’indice sur lequel il a agi est nommé
dans reason. Les branches model et task rapportent workflow comme null aussi, parce qu’elles
répondent avant que les ids de question soient lus.
Confiance : deux nombres, pas interchangeables
answer_confidenceest la masse de probabilité sur la réponse rapportée (max(p)). C’est la quantité que l’échelonnage de température ajuste et sur laquelle les chiffres ECE de ce dépôt sont calculés, donc elle porte la propriété de gating sur laquelle s’appuie la page Benchmarks et limites connues – mais seulement pour un checkpoint dont l’ajustement de température a été validé sur ton trafic.confidencesignifie quelque chose de différent selon le type : entropie normalisée1 - H(p)/log(k)surchoiceetscore, etmax(p_yes, p_no)surnoul(où elle égaleanswer_confidence).
Ne compare jamais les deux contre un seul seuil. Note aussi la différence en portant depuis Jev :
TypeSafe définit la confiance comme (n*p_max - 1)/(n - 1), donc un seuil repris d’un déploiement Jev
gate différemment sur la valeur d’entropie de Laya.
Contrat Jev strict : LAYA_JEV_STRICT
La charge ci-dessus est la charge Laya complète. Le contrat Jev auquel un client peut la tenir définit
moins : trois champs de niveau racine (model, answers, usage), les clés contractuelles sur chaque
réponse et rien d’autre, et un usage des deux nombres de jetons. Un client qui valide la réponse contre
ce contrat sans champs supplémentaires – le plugin provider TypeSafe d’OpenClaw en est un – rejette la
charge complète, donc LAYA_JEV_STRICT=1 projette la réponse sur le contrat avant de répondre, sur
/v1/systemone comme sur /v1/systemone/batch :
- la racine ne garde que
model,answersetusage;routingn’est pas envoyé ; - une réponse
choicegardechoice,probabilitiesetconfidence; - une réponse
scoregardescore,probabilities,confidenceetlegend; - une réponse
noulne garde quenoul; usagegardeinput_tokensetoutput_tokens; les faits de troncature et le plafond d’options repliées ne sont pas envoyés.
La projection ne garde que les clés contractuelles et ne recalcule rien : chaque valeur est celle que le
résultat porte déjà, donc les probabilités et les scores qu’un client strict lit sont identiques à ceux
que rapporte la charge complète. Le défaut reste la charge complète, et un déploiement qui active le
drapeau perd la visibilité de troncature que fournit usage – un état coupé est alors visible dans les
logs, pas dans la réponse. Le criteria de score doit rester de simples chaînes sous le contrat strict :
un client strict compare le legend renvoyé aux critères qu’il a envoyés, et Laya rend un critère
structuré avec le JSON de Python, ce qu’un appelant JavaScript qui convertit ses propres critères en
chaîne peut ne pas égaler octet pour octet.
Les réponses réussies portent aussi Server-Timing: inference;dur=<ms> et X-Inference-Time-Ms.
Limites
Les garde-fous de requête sont vérifiés avant la tokenisation, donc une requête surdimensionnée ne coûte
au serveur que les octets qu’il a lus. Chacune d’elles est un 413 ; le detail dit quelle limite a été
atteinte.
| limite | valeur |
|---|---|
| corps de requête | 2 MiB, appliqué pendant le streaming – un Content-Length chunked ou sous-estimé ne peut pas le contourner |
state |
50 000 caractères du texte donné au modèle – la chaîne elle-même pour un état chaîne, json.dumps(state, ensure_ascii=False) pour un objet ou un tableau |
| questions par requête | 64 |
states par requête par lots |
64 |
options par question choice |
100 |
niveaux par question score |
32 |
| options sur toutes les questions | 512 |
| requêtes concurrentes admises | LAYA_MAX_CONCURRENT (16) |
/v1/systemone/batch est borné différemment, et non par un refus. Il tokenise chaque état une fois par
question et regroupe chaque ligne en un seul tenseur, donc les plafonds de champs se multiplient : 64
états de 64 questions font 4096 lignes, que toutes les autres limites de cette page permettent. Ce qu’une
ligne coûte, c’est sa largeur, et max_len est lui-même un champ de requête, donc le coût d’un lot est
states x questions x width.
Plutôt que de refuser un grand lot, l’endpoint le scinde : quand ce produit dépasse
LAYA_MAX_BATCH_TOKENS (131 072 par défaut), il choisit un batch_size pour que chaque passe avant
reste dans le budget, et Router.predict_batch exécute le lot en plusieurs passes. Chaque état est quand
même répondu et la réponse est inchangée. Une requête dont les lignes tiennent déjà ne reçoit aucun
batch_size, donc elle se comporte exactement comme avant – ce qui compte car la forme du lot peut
déplacer des résultats en virgule flottante. Un batch_size envoyé par l’appelant gagne toujours : il a
demandé une forme.
Par défaut, 256 lignes passent en une passe – 64 états de 4 questions, ou 8 de 32. Les lots plus grands
sont scindés, et augmenter max_len rend chaque passe plus étroite plutôt que de coûter 16 fois le
travail. Ce que cela ne borne pas, c’est combien de temps une requête occupe le serveur ; c’est
LAYA_MAX_CONCURRENT et l’unique worker d’inférence, et c’est déjà vrai d’une requête /v1/systemone
sur un état de 50 000 caractères.
Les plafonds d’options ne sont que des garde-fous d’amplification HTTP ; le modèle lui-même fait tenir les
jetons d’options dans une fenêtre head_max_len=192, donc une question dans les plafonds HTTP peut encore
être refusée en 422 quand les textes d’options dépassent ensemble ce budget. Le
Harnais d’évaluation exécute les mêmes requêtes en processus sans la couche HTTP.
Erreurs
| statut | quand | detail du corps |
|---|---|---|
400 |
le corps n’est pas du JSON valide, pas un objet, n’a pas de questions, state est manquant ou null, questions n’est pas un objet, ou une chaîne n’importe où dans le corps contient un échappement de substitut \udXXX non apparié |
ce qui ne va pas |
401 |
LAYA_API_KEY est définie et le jeton Bearer est manquant ou erroné |
invalid or missing bearer token |
413 |
l’une des limites ci-dessus | quelle limite et de combien |
422 |
la question est du JSON bien formé mais invalide pour Laya (type inconnu, options au-delà du budget de tête), ou un contrôle de requête (lang, min_confidence, un argument de hook) n’est pas dans la forme qu’accepte cet endpoint |
nomme la question ou le champ et quoi corriger |
500 |
l’inférence a échoué pour toute autre raison | inference failed – toujours cette chaîne, pour que les chemins, les poids et l’état mémoire ne fuient jamais ; la cause est dans le log serveur |
503 |
LAYA_MAX_CONCURRENT requêtes sont déjà en vol |
server busy, try again later |
Le 400 de substitut non apparié est celui qui semble inhabituel. \udXXX sans paire est du JSON
légal, mais le caractère qu’il nomme ne peut pas être encodé en UTF-8, donc le tokenizer lève un
TypeError – la propre chaîne de l’appelant qui arrive comme une faute du serveur, avec une trace par
requête. Les deux routes de décision parcourent donc le corps analysé à la recherche de substituts isolés
et en refusent un avant qu’il n’atteigne l’inférence. Le parcours s’exécute après les contrôles de
taille, donc un corps surdimensionné est quand même refusé en premier et les limites de caractères et de
questions bornent ce qu’il peut atteindre. Un substitut apparié est un caractère astral ordinaire
quand l’analyseur a fini, donc un emoji dans un état n’est pas affecté.
La charge au-dessus du plafond est refusée, pas mise en file : les clients qui gardent un emplacement
d’admission en streamant un corps lent ne peuvent pas affamer /health, et une nouvelle tentative peut
prendre l’emplacement qu’un client refusé a laissé.
Modèle de concurrence
L’inférence est un appel torch synchrone qui prend des centaines de millisecondes à des secondes sur CPU,
donc elle ne tourne jamais sur la boucle d’événements : les requêtes sont confiées à un exécuteur à un
seul worker, ce qui signifie une passe avant à la fois – la forme que veut un seul checkpoint sur un
appareil. L’admission (le sémaphore LAYA_MAX_CONCURRENT) est vérifiée avant qu’un octet du corps ne soit
lu et gardée pendant l’inférence ; la porte d’inférence n’est rejointe qu’une fois le corps complet, donc
un client lent garde un emplacement d’admission mais jamais un emplacement d’inférence.
Pas (encore) ici
Ce serveur parle un seul protocole exprès. Il n’y a pas d’endpoint compatible OpenAI ; exécute plutôt plusieurs questions en une requête, puisqu’elles partagent une seule passe avant par
ensemble de questions. L’autre route est POST /v1/systemone/batch, qui répond à un ensemble questions sur un tableau de states. Elle n’a pas encore de section sur cette page – sa forme de requête est dans la section self-hosting du README – et chaque contrôle ci-dessus s’y applique comme à POST /v1/systemone : les mêmes 400 de forme, le même refus de substitut non apparié, la même auth, admission, limites de taille, validation des contrôles du corps et mappage 500. La CLI laya et le serveur MCP couvrent l’usage local – voir le
README.