Documentation

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é.

  • status est ok dès que le processus répond, tout simplement. Il ne dit rien sur les checkpoints.
  • loaded liste les checkpoints résidents en mémoire. Il est vide jusqu’à ce qu’une requête en construise un, ce que LAYA_PRELOAD=0 laisse le processus faire.
  • revisions est la révision d’artefact depuis laquelle chaque checkpoint résident a été chargé, indexée par les mêmes noms que loaded, pour qu’un déploiement puisse confirmer ce qu’il sert réellement.
  • device est l’appareil sur lequel un checkpoint résident calcule réellement, ce qui n’est pas toujours ce que LAYA_DEVICE a 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_preference est true exactement tant que rien n’est résident, et false dè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 dit false avec device cpu, plutôt que de continuer à répondre cuda.
  • checkpoint_devices donne la mesure par checkpoint, indexée par les noms dans loaded ; device est la première de ces valeurs.
  • cpu_fallbacks compte, 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 : count depuis le démarrage du processus, et last_reason portant 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 compte 0 ici – cela se voit dans device.

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_confidence est 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.
  • confidence signifie quelque chose de différent selon le type : entropie normalisée 1 - H(p)/log(k) sur choice et score, et max(p_yes, p_no) sur noul (où elle égale answer_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, answers et usage ; routing n’est pas envoyé ;
  • une réponse choice garde choice, probabilities et confidence ;
  • une réponse score garde score, probabilities, confidence et legend ;
  • une réponse noul ne garde que noul ;
  • usage garde input_tokens et output_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.