Router
laya.Router détecte la langue de chaque état et envoie la requête au checkpoint
correspondant, en chargeant les checkpoints au premier usage.
Les noms, types, valeurs par défaut et le code restent en anglais ; le reste est traduit (les entrées non encore traduites s'affichent dans l'original anglais).
Router
Router(
models: Optional[Dict[str, str]] = None,
device: Optional[str] = None,
token: Optional[str] = None,
revision: Optional[str] = None,
revisions: Optional[Dict[str, Optional[str]]] = None,
max_loaded: int = 2,
default: str = "english",
auto_task_detection: bool = False,
standalone_repos: bool = False,
preload: bool = False,
lang_guess: Optional[Any] = None,
hooks=None,
on_predict_start=None,
on_predict_end=None,
hooks_raise: bool = True,
hooks_concurrent: bool = True,
hooks_timeout: Optional[float] = None,
agent_kwargs: Optional[Dict[str, Any]] = None,
sha256_digests: Optional[Dict[str, Optional[Dict[str, str]]]] = None,
)Classes de base: HookRegistry
Charge paresseusement les checkpoints Laya et envoie chaque requête au bon.
from laya import Router
r = Router()
r.predict({"message": "Mein Konto wurde zweimal belastet"}, questions) # -> multilingual
r.predict({"message": "I was charged twice"}, questions) # -> english
r.predict(state, questions, model="typed-decisions") # explicit
Les modèles sont téléchargés et construits au premier usage. max_loaded plafonne combien restent résidents
(le moins récemment utilisé est évincé), car les trois ensemble font ~1,16B paramètres.
Le défaut est 2, car le routage automatique ne choisit jamais qu’entre english et
multilingual : un plafond de un reconstruirait le checkpoint qu’il vient d’évincer à chaque changement de script,
soit des secondes par requête sur exactement le trafic pour lequel le Router existe. Le trafic qui ne
voit jamais qu’une seule langue ne construit jamais le second checkpoint, donc le défaut ne lui coûte rien.
Baisse-le à 1 pour un hôte à mémoire contrainte, et monte-le à 3 (ou précharge) quand
auto_task_detection, un model= explicite ou un task= explicite peuvent aussi atteindre
typed-decisions.
Pour un serveur ou une démo, précharge plutôt : un chargement à froid coûte des secondes, alors que la détection coûte des microsecondes, donc même le défaut paie encore un chargement la première fois qu’une langue apparaît.
r = Router(preload=True) # all three resident, routing is free
r = Router(preload=True, device="cuda")
r.preload(["english", "multilingual"]) # or just the two you serve
Les révisions Hub sont optionnelles. revision applique un commit à chaque modèle ;
revisions={"english": "...", "multilingual": "..."} le remplace par modèle,
ce qui est utile quand des dépôts autonomes ont été revus à des commits différents.
Sans ni l’un ni l’autre, le défaut normal de huggingface_hub et le cache hors ligne existant sont utilisés.
Une entrée revisions qui vaut None ou est vide signifie « aucune substitution pour ce modèle », donc le modèle
hérite de revision -- la forme qu’écrit {"english": os.environ.get("EN_SHA")} quand la
variable n’est pas définie, ce qui ne doit pas coûter à l’appelant l’épinglage qu’il a bien demandé. Rien dans
revisions ne peut détacher un modèle pendant que revision épingle les autres ; laisse revision non défini et
nomme plutôt les modèles que tu veux épingler.
Une revision vide est lue de la même façon, ce qui est un changement délibéré de ce que le Router
transmet : Router(revision=" ") atteignait auparavant resolve_revision, où une chaîne vraie mais vide
supprimait le repli LAYA_REVISION et laissait le défaut de huggingface_hub, et est maintenant
écartée avant d’y arriver, donc un Router configuré avec des espaces se comporte comme un
configuré sans rien et $LAYA_REVISION s’applique. C’est ce que « cette ligne de configuration
n’a jamais été remplie » doit vouloir dire si revision et une entrée revisions doivent être lus de la même façon.
C’est l’un des deux endroits où une source plus faible passe devant un argument explicite. L’autre est une
entrée de condensat par checkpoint issue de LAYA_SHA256_DIGESTS, qui l’emporte sur un expected_sha256
transmis via agent_kwargs ; voir la docstring de la classe.
Tout autre chose qu’accepte laya.Agent est accessible via agent_kwargs, qui est fusionné dans
chaque checkpoint que le Router construit :
Router(agent_kwargs={"lang_temperatures": {"de": {"temperature": [1.0, 1.4, 2.0]}}})
Router(agent_kwargs={"expected_sha256": {"model.safetensors": "a3f1..."}})
Router(agent_kwargs={"fast": True})
expected_sha256 y épingle les mêmes fichiers sur chaque checkpoint, ce que veut un seul
checkpoint résident ou un tokenizer.json partagé. Il n’est jamais écarté en bloc par les
condensats propres d’un checkpoint : là où l’un en a aussi une entrée, de sha256_digests ou d’un
LAYA_SHA256_DIGESTS par checkpoint, les deux tables sont fusionnées fichier par fichier, donc un fichier
qu’une seule des deux nomme est quand même vérifié.
Deux exceptions, toutes deux délibérées et toutes deux testées, car « fusionné fichier par fichier » n’est pas toute l’histoire et la différence est un contrôle de chaîne d’approvisionnement :
- Un
LAYA_SHA256_DIGESTSplat --{artifact: digest}plutôt que{model: {...}}-- n’est pas une couche ici du tout.verify_digestsl’applique lui-même, mais seulement quand rien d’autre n’épingle (if expected is None), donc N’IMPORTE QUELexpected_sha256atteignantAgent, d’ici ou d’une entrée de checkpoint, signifie que la variable plate n’est pas consultée pour ce chargement. Vérifié sur de vrais fichiers : une variable plate épinglantmodel.safetensorsplus une tableagent_kwargsépinglanttokenizer.jsoncharge unmodel.safetensorsaltéré. Utilise la forme par checkpoint, ou nomme chaque fichier qui t’importe dans une seule table, si tu as besoin des deux. Ceci est inchangé par rapport àmain. - Une entrée explicite
{}ouNoneMASQUE ce qui s’appliquerait autrement -- c’est ce que « charger celui-ci sans vérification » doit vouloir dire, ettest_an_explicit_none_entry_masks_a_flat_environment_mapl’épingle.
Et la priorité est par checkpoint sur partagé, peu importe d’où vient chacun, donc une
entrée par checkpoint synthétisée à partir de LAYA_SHA256_DIGESTS l’emporte sur un expected_sha256
passé ici dans le code. Une variable d’environnement qui bat un argument explicite mérite d’être dite
clairement sur un contrôle comme celui-ci ; test_an_environment_pin_overrides_the_shared_one_per_checkpoint
est l’endroit où cela est épinglé.
Pour un fichier que les deux nomment, l’entrée par checkpoint l’emporte. Les deux ne sont pas également spécifiques : la
table agent_kwargs atteint chaque checkpoint que le Router construit, et model.safetensors est le seul
nom que chaque checkpoint utilise pour un fichier différent, donc une entrée partagée pour lui ne peut pas être une
revendication correcte à propos de tous à la fois. Rien ne lève sur ce chevauchement -- le refuser rejetterait
un épinglage partagé plus une substitution par checkpoint, ce qui est la forme ordinaire et qui se chargeait
correctement avant que tout cela existe. Si tu as besoin de savoir contre quel condensat un checkpoint a été vérifié,
relis-le : la table remise à chaque Agent est la fusion décrite ci-dessus.
Tant agent_kwargs que sha256_digests sont publics et mutables, et l’entrée d’un checkpoint est lue
au chargement plutôt qu’à la construction, donc un épinglage assigné ensuite -- ou ajouté à une
entrée existante sur place -- compte.
Les noms que le Router se fixe à lui-même -- model_id_or_path, device, token, subfolder,
revision et les arguments de hook -- sont refusés ici plutôt que masqués en silence, et les
noms restants sont vérifiés contre Agent.__init__ à la construction, donc une option mal orthographiée
échoue sur la ligne Router(...) au lieu de la première requête.
Les empreintes d’artefacts sont optionnelles et toujours par modèle : sha256_digests={"english": {...}}
transmet cette table {path relative to the checkpoint dir: hexdigest} à l’Agent qui
le charge, donc un fichier de poids altéré ou substitué est refusé avant d’être analysé. Il
n’existe pas d’équivalent de revision à l’échelle du Router, car les empreintes, contrairement à un commit SHA, ne sont pas
partageables : le dépôt groupé livre un model.safetensors distinct pour chacun de
english, multilingual et typed-decisions, donc une table plate ne peut jamais correspondre qu’à un
seul d’entre eux. Un modèle listé avec None ou {} n’ajoute aucun fichier propre, ce qui le charge sans vérification
sauf si agent_kwargs["expected_sha256"] l’épingle ; il masque encore un
LAYA_SHA256_DIGESTS plat, ce à quoi sert de le lister ainsi.
La même séparation est disponible pour un processus configuré uniquement par l’environnement : quand
LAYA_SHA256_DIGESTS contient une table indexée par modèle ({"english": {...}, "multilingual": {...}})
celle-ci l’amorce par checkpoint, donc un serveur qui en garde plusieurs résidents peut épingler chacun avec ses
propres empreintes au lieu de refuser de démarrer sur le second. Un LAYA_SHA256_DIGESTS plat
conserve sa signification existante, appliqué par laya.revisions à chaque checkpoint que le processus
charge, ce qui convient à un processus à checkpoint unique. Une entrée d’argument l’emporte sur
l’environnement pour le modèle qu’elle nomme. Une variable imbriquée qui nomme certains checkpoints et pas d’autres ne dit rien sur les autres : ils gardent
ce avec quoi agent_kwargs["expected_sha256"] les épingle, car épingler un checkpoint depuis l’environnement n’est pas une demande
d’arrêter de vérifier le reste.
Les hooks sont optionnels et s’exécutent au niveau du Router : on_route voit la décision de routage,
on_load / on_evict voient le cycle de vie du modèle, et on_predict_start / on_predict_end
enveloppent tout l’appel route+inférence. Voir laya.hooks.
Paramètres
modelsOptional[Dict[str, str]]=NonedeviceOptional[str]=NonetokenOptional[str]=NonerevisionOptional[str]=NonerevisionsOptional[Dict[str, Optional[str]]]=Nonemax_loadedint=2defaultstr="english"auto_task_detectionbool=Falsestandalone_reposbool=Falsepreloadbool=Falselang_guessOptional[Any]=Nonehooks=Noneon_predict_start=Noneon_predict_end=Nonehooks_raisebool=Truehooks_concurrentbool=Truehooks_timeoutOptional[float]=Noneagent_kwargsOptional[Dict[str, Any]]=Nonesha256_digestsOptional[Dict[str, Optional[Dict[str, str]]]]=None
load
load(name: str)Renvoie l’Agent pour name, en le téléchargeant et le construisant au premier usage.
Les appelants concurrents partagent un seul Agent au lieu de construire des doublons.
Paramètres
namestr
attach
attach(name: str, agent: Any)Enregistre un Agent déjà construit sous name au lieu de charger une seconde copie.
Utile quand le processus a un checkpoint chargé pour d’autres raisons : une démo qui a déjà
construit convaiinnovations/laya peut le confier au router plutôt que de payer -- et garder
en mémoire -- un doublon de 421M paramètres.
Paramètres
namestragentAny
preload
preload(names: Optional[List[str]] = None)Télécharge et construit les checkpoints à l’avance pour qu’aucune requête ne paie un chargement de modèle.
Un chargement à froid coûte des secondes ; la détection de langue coûte des microsecondes. Avec chaque
checkpoint résident, le routage est effectivement gratuit -- ce que tu veux dans un
serveur ou une démo. max_loaded est augmenté pour contenir à la fois les checkpoints demandés et
tous les agents déjà résidents, donc le préchargement incrémental n’évince ni les uns ni les autres.
Paramètres
namesOptional[List[str]]=None
unload
unload(name: Optional[str] = None)Libère un modèle, ou tous.
Paramètres
nameOptional[str]=None
loaded_revisions
loaded_revisions: Dict[str, Optional[str]]Commit SHA depuis lequel chaque agent résident a été chargé (None pour les chemins locaux).
route
route(
state: Union[str, dict, list, None],
questions: Optional[Dict[str, Any]] = None,
model: Optional[str] = None,
task: Optional[str] = None,
lang: Optional[str] = None,
lang_guess: Optional[Any] = None,
hooks=None,
hooks_raise: Optional[bool] = None,
hooks_timeout: Optional[float] = None,
) -> RouteDecisionDécide quel checkpoint utiliser, puis laisse les hooks on_route l’observer ou le remplacer.
ctx.decision est la RouteDecision ; un hook peut la remplacer (par exemple pour épingler un
checkpoint) et le remplacement est ce qui est renvoyé et utilisé. hooks sont des hooks par appel,
ajoutés après ceux installés sur le Router.
Paramètres
stateUnion[str, dict, list, None]questionsOptional[Dict[str, Any]]=NonemodelOptional[str]=NonetaskOptional[str]=NonelangOptional[str]=Nonelang_guessOptional[Any]=Nonehooks=Nonehooks_raiseOptional[bool]=Nonehooks_timeoutOptional[float]=None
predict
predict(
state: Union[str, dict, list],
questions: Dict[str, Any],
model: Optional[str] = None,
task: Optional[str] = None,
lang: Optional[str] = None,
lang_guess: Optional[Any] = None,
hooks=None,
on_predict_start=None,
on_predict_end=None,
hooks_raise: Optional[bool] = None,
hooks_timeout: Optional[float] = None,
max_len: Optional[int] = None,
head_max_len: Optional[int] = None,
min_confidence: Optional[float] = None,
) -> Dict[str, Any]Route, puis répond à chaque question en une seule passe avant sur le checkpoint choisi.
Le résultat est la charge habituelle de system_one plus une clé routing qui enregistre la décision.
Les hooks on_predict_start / on_predict_end au niveau du Router enveloppent tout l’appel route+inférence
et voient ctx.decision ; voir laya.hooks. max_len / head_max_len remplacent le budget
de jetons de l’agent pour cet appel (un hook de début peut fixer ctx.max_len / ctx.head_max_len).
Paramètres
stateUnion[str, dict, list]questionsDict[str, Any]modelOptional[str]=NonetaskOptional[str]=NonelangOptional[str]=Nonelang_guessOptional[Any]=Nonehooks=Noneon_predict_start=Noneon_predict_end=Nonehooks_raiseOptional[bool]=Nonehooks_timeoutOptional[float]=Nonemax_lenOptional[int]=Nonehead_max_lenOptional[int]=Nonemin_confidenceOptional[float]=None
predict_long
predict_long(
state: Union[str, dict, list],
questions: Dict[str, Any],
model: Optional[str] = None,
task: Optional[str] = None,
lang: Optional[str] = None,
lang_guess: Optional[Any] = None,
window: Optional[int] = None,
stride: Optional[int] = None,
aggregate: str = "auto",
batch_size: Optional[int] = None,
hooks=None,
on_predict_start=None,
on_predict_end=None,
hooks_raise: Optional[bool] = None,
hooks_timeout: Optional[float] = None,
) -> Dict[str, Any]Route, puis parcourt chaque fenêtre de l’état au lieu de seulement la première.
predict score un état depuis une seule fenêtre : tout ce qui dépasse max_len est coupé (la
première fenêtre, ou pour une liste de conversation la dernière) et n’atteint jamais le modèle. Ceci
route exactement comme le fait predict -- les mêmes indications model/task/lang, les mêmes
hooks au niveau du router, la même clé routing et usage -- et score l’état routé avec
le predict_long de cet agent, qui le découpe en fenêtres chevauchantes et agrège par
question. Les règles d’agrégation sont celles de laya.agent.Agent.predict_long : noul prend la
fenêtre la plus forte, choice/score la plus confiante.
Les hooks par appel (hooks, on_predict_start, on_predict_end, hooks_raise,
hooks_timeout) enveloppent tout le route+scan exactement comme ils enveloppent predict : le parcours s’exécute
en dernier, donc un hook de début qui répond (ctx.skip(...)) ou réécrit l’état l’emporte.
max_len / head_max_len ne sont pas acceptés ici -- une fenêtre est dimensionnée par window ou par le
budget du checkpoint, et remplacer la troncature à fenêtre unique est précisément le rôle de predict_long.
Paramètres
stateUnion[str, dict, list]questionsDict[str, Any]modelOptional[str]=NonetaskOptional[str]=NonelangOptional[str]=Nonelang_guessOptional[Any]=NonewindowOptional[int]=Nonejetons d’état par fenêtre. Par défaut le budget du checkpoint routé (
max_len - head_max_len - 8), plafonné à la place que les questions laissent pour l’état, pour qu’aucune fenêtre ne soit de nouveau tronquée à l’entrée ; une fenêtre plus petite isole un intervalle localisé.strideOptional[int]=Nonepas de jetons entre les fenêtres ; par défaut la moitié de la fenêtre effective (50 % de chevauchement). Un pas au-delà de cette fenêtre est un
ValueError, car les jetons entre les fenêtres n’atteindraient aucun modèle.aggregatestr="auto"« auto » (les règles par type ci-dessus) est le seul mode.
batch_sizeOptional[int]=Noneplafond du nombre de fenêtres par passe avant, pour borner la mémoire sur de très longs états.
hooks=Noneon_predict_start=Noneon_predict_end=Nonehooks_raiseOptional[bool]=Nonehooks_timeoutOptional[float]=None
Renvoie
La charge habituelle de predict, avec usage["windows"] qui compte les fenêtres scorées.
Exceptions
TypeError : l’agent routé n’a pas de predict_long -- un agent attaché à
la main, puisque Agent comme ONNXAgent l’implémentent --, donc il n’y a rien avec quoi parcourir.
Toujours levée, quel que soit hooks_raise, et de même tout autre erreur que le parcours
lui-même lève -- aucune n’est attrapée : le parcours est le travail propre de cette méthode, pas un
hook d’un appelant, donc la politique d’erreurs de hooks ne décide pas s’il peut être
ignoré. Il tournait auparavant comme hook de début, où hooks_raise=False l’avalait
et renvoyait une fenêtre scorée par system_one -- une autre question que
celle posée. Un agent dont le predict_long n’a pas de paramètre lang est parcouru
sans en avoir et averti, pas mis en échec ; c’est une vérification de signature, pas une
erreur avalée.
decide
decide(
state: Union[str, dict, list],
schema: Any = None,
questions: Optional[Dict[str, Any]] = None,
return_details: bool = False,
min_confidence: Optional[float] = None,
predict_kwargs,
) -> AnyRépond à state contre un schéma (schéma JSON ou modèle pydantic) et renvoie des valeurs typées.
Voir laya.structured. Passe exactement l’un de schema ou questions ; les arguments nommés supplémentaires
(par exemple model=, task=, hooks=) sont transmis à predict.
Paramètres
stateUnion[str, dict, list]schemaAny=NonequestionsOptional[Dict[str, Any]]=Nonereturn_detailsbool=Falsemin_confidenceOptional[float]=Nonepredict_kwargs
decide_batch
decide_batch(
states: Sequence[Any],
schema: Any = None,
questions: Optional[Dict[str, Any]] = None,
return_details: bool = False,
min_confidence: Optional[float] = None,
predict_kwargs,
) -> List[Any]Répond à de nombreux états contre un seul schéma (schéma JSON ou modèle pydantic) en un appel par lots.
La forme haut débit de :meth:decide : le schéma est planifié une fois et ses questions
tournent sur chaque état via :meth:predict_batch (passes avant groupées, résultats dans
l’ordre d’entrée), puis les réponses de chaque état sont projetées comme le fait decide. Les arguments
nommés supplémentaires (batch_size=, model=, hooks=, ...) sont transmis à
predict_batch. Voir laya.structured.
Paramètres
statesSequence[Any]schemaAny=NonequestionsOptional[Dict[str, Any]]=Nonereturn_detailsbool=Falsemin_confidenceOptional[float]=Nonepredict_kwargs
route_batch
route_batch(
requests: Sequence[Dict[str, Any]],
hooks_timeout: Optional[float] = None,
hooks=None,
hooks_raise: Optional[bool] = None,
) -> List[RouteDecision]Route un lot de requêtes hétérogènes sans charger aucun checkpoint.
Chaque requête est un mapping avec state et questions plus les mêmes substitutions
d’options de routage acceptées par :meth:route : model, task, lang et
lang_guess. Les décisions renvoyées conservent l’ordre d’entrée.
Ceci est volontairement séparé de l’inférence pour que les appelants puissent inspecter ou agréger les décisions de routage avant de payer le coût de chargement du modèle.
Paramètres
requestsSequence[Dict[str, Any]]Séquence de dictionnaires de requête, chacun exigeant
stateetquestions.hooks_timeoutOptional[float]=NoneRemplace le
hooks_timeoutdu Router pour cet appel, appliqué à la diffusionon_routede chaque requête, comme sur :meth:route.hooksHookArg=NoneHook par appel ou séquence de hooks pour cet appel.
hooks_raiseOptional[bool]=NoneRemplace la politique
hooks_raisedu Router pour cet appel.
predict_batch
predict_batch(
requests: Sequence[Dict[str, Any]],
batch_size: Optional[int] = None,
hooks_timeout: Optional[float] = None,
min_confidence: Optional[float] = None,
sort_by_length: bool = False,
hooks=None,
on_predict_start=None,
on_predict_end=None,
hooks_raise: Optional[bool] = None,
) -> List[Dict[str, Any]]Route et exécute un lot de requêtes hétérogènes avec un minimum de changements de modèle.
Les requêtes sont d’abord routées et groupées par checkpoint. Au sein de chaque checkpoint,
les requêtes qui partagent le même schéma de questions sont transmises à
Agent.predict_batch pour que leurs états puissent partager des passes avant. Les résultats sont
ensuite rétablis dans l’ordre d’origine des requêtes.
Les requêtes peuvent spécifier indépendamment model, task, lang,
lang_guess, max_len ou head_max_len et peuvent utiliser des schémas de questions différents.
max_len / head_max_len sont la forme par requête de la substitution du budget de jetons que
predict prend comme arguments d’appel : ils fixent les budgets d’état et de tête de questions
du checkpoint pour cette seule requête, de sorte qu’une question large peut être posée sans réduire les
autres requêtes du lot à la même fenêtre. Les requêtes qui demandent des budgets différents sont
réparties en passes avant distinctes, puisqu’un appel Agent.predict_batch porte un seul
budget pour tous ses états. Un hook de début peut encore remplacer l’une ou l’autre valeur sur ctx.
Les hooks de prédiction au niveau du Router s’exécutent par requête, comme predict les exécute : chaque requête
obtient son propre PredictContext, donc on_predict_start peut remplacer l’état,
les questions ou le budget de jetons de cette requête, ou lui faire ctx.skip(...), et on_predict_end
voit et peut remplacer son résultat. Les requêtes sont groupées pour la passe avant après
que leurs hooks de début ont tourné, et les requêtes d’un groupe de checkpoint se terminent dans l’ordre inverse
de celui où elles ont commencé. Si un groupe de checkpoint échoue, chaque requête de ce groupe dont le hook de début
a tourné échoue avec l’exception, y compris un succès de cache : chacune reçoit on_error puis
on_predict_end avant que l’exception ne se propage.
Paramètres
requestsSequence[Dict[str, Any]]Séquence de dictionnaires de requête. Chaque élément exige
stateetquestionset peut inclure les substitutions de routagemodel,task,langoulang_guesset les substitutions de budget de jetonsmax_len/head_max_len.batch_sizeOptional[int]=NoneNombre maximal optionnel d’états par lot de passes avant de l’Agent.
hooks_timeoutOptional[float]=NoneRemplace le
hooks_timeoutdu Router pour cet appel.min_confidenceOptional[float]=NoneFloat optionnel ou mapping par bucket pour le gating par confiance.
sort_by_lengthbool=FalseTransmis à chaque appel
Agent.predict_batch, donc chaque groupe de questions complète jusqu’à un maximum plus court ; voirAgent.predict_batch. Les résultats conservent l’ordre d’entrée dans les deux cas. Silencieusement abandonné pour un agent attaché dont lepredict_batchest antérieur à ce réglage (#294).hooksHookArg=NoneHook par appel ou séquence de hooks pour cet appel.
on_predict_startPredictHookArg=NoneCallable simple ou séquence de callables pour les événements de début.
on_predict_endPredictHookArg=NoneCallable simple ou séquence de callables pour les événements de fin.
hooks_raiseOptional[bool]=NoneRemplace la politique
hooks_raisedu Router pour cet appel.
Renvoie
Un résultat de prédiction normal du Router par requête, dans le même ordre que l’entrée.
RouteDecision
RouteDecision()Classes de base: dict
Le résultat du routage : quel modèle, pourquoi et ce qui a été détecté.
Se comporte comme un dict, donc il se sérialise directement dans une réponse d’API.
DEFAULT_MODELS
DEFAULT_MODELS = {
"english": (BUNDLE_REPO, None),
"multilingual": (BUNDLE_REPO, "multilingual"),
"typed-decisions": (BUNDLE_REPO, "typed-decisions"),
}