Router¶
laya.Router detects the language of each state and sends the request to the matching
checkpoint, loading checkpoints on first use.
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 = "multilingual",
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,
)
Bases: HookRegistry
Lazily loads Laya checkpoints and sends each request to the right one.
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
Models are downloaded and built on first use. max_loaded caps how many stay resident
(least-recently-used is evicted), because all three together are ~1.16B parameters.
The default is 2, because automatic routing only ever chooses between english and
multilingual: a cap of one rebuilds the checkpoint it just evicted on every script switch,
which is seconds per request on exactly the traffic the Router exists for. Traffic that only
ever sees one language never builds the second checkpoint, so the default costs it nothing.
Lower it to 1 for a memory-constrained host, and raise it to 3 (or preload) when
auto_task_detection, an explicit model= or an explicit task= can reach
typed-decisions as well.
For a server or a demo, preload instead: a cold load costs seconds, while detection costs microseconds, so even the default still pays a load the first time a language appears.
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
Hub revisions are opt-in. revision applies one commit to every model;
revisions={"english": "...", "multilingual": "..."} overrides that per model,
which is useful when standalone repositories were reviewed at different commits.
Without either, huggingface_hub's normal default and existing offline cache are used.
A revisions entry that is None or blank is "no override for this model", so the model
inherits revision -- the shape {"english": os.environ.get("EN_SHA")} writes when the
variable is unset, which must not cost the caller the pin it did ask for. Nothing in
revisions can unpin one model while revision pins the rest; leave revision unset and
name the models you want pinned instead.
A blank revision is read the same way, which is a deliberate change in what the Router
passes on: Router(revision=" ") used to reach resolve_revision, where a truthy but blank
string suppressed the LAYA_REVISION fallback and left huggingface_hub's default, and is now
dropped before it gets there, so a Router configured with whitespace behaves like one
configured with nothing and $LAYA_REVISION applies. That is what "this configuration line was
never filled in" has to mean if revision and a revisions entry are to be read the same way.
It is one of two places a weaker source ends up ahead of an explicit argument. The other is a
per-checkpoint digest entry from LAYA_SHA256_DIGESTS, which wins over an expected_sha256
passed through agent_kwargs; see the class docstring.
Anything else laya.Agent accepts is reachable through agent_kwargs, which is merged into
every checkpoint the Router builds:
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 there pins the same files on every checkpoint, which is what a single
resident checkpoint or a shared tokenizer.json wants. It is never discarded wholesale by a
checkpoint's own digests: where one also has an entry, from sha256_digests or from a
per-checkpoint LAYA_SHA256_DIGESTS, the two maps are merged file by file, so a file
only one of them names is still verified.
Two exceptions, both deliberate and both tested, because "merged file by file" is not the whole story and the difference is a supply-chain control:
- A flat
LAYA_SHA256_DIGESTS--{artifact: digest}rather than{model: {...}}-- is not a layer here at all.verify_digestsapplies it itself, but only when nothing else pins (if expected is None), so ANYexpected_sha256reachingAgent, from here or from a checkpoint entry, means the flat variable is not consulted for that load. Verified on real files: a flat variable pinningmodel.safetensorsplus anagent_kwargsmap pinningtokenizer.jsonloads a tamperedmodel.safetensors. Use the per-checkpoint shape, or name every file you care about in one map, if you need both. This is unchanged frommain. - An explicit
{}orNoneentry MASKS what would otherwise apply -- that is what "load this one unverified" has to mean, andtest_an_explicit_none_entry_masks_a_flat_environment_mappins it.
And the precedence is per-checkpoint over shared regardless of where each came from, so a
per-checkpoint entry synthesised from LAYA_SHA256_DIGESTS wins over an expected_sha256
passed here in code. An environment variable beating an explicit argument is worth stating
plainly on a control like this; test_an_environment_pin_overrides_the_shared_one_per_checkpoint
is where that is pinned.
For a file both name, the per-checkpoint entry wins. The two are not equally specific: the
agent_kwargs map reaches every checkpoint the Router builds, and model.safetensors is the one
name every checkpoint uses for a different file, so a shared entry for it cannot be a correct
claim about all of them at once. Nothing raises over that overlap -- refusing it would reject
a shared pin plus a per-checkpoint override, which is the ordinary shape and which loaded
correctly before any of this existed. If you need to know which digest a checkpoint was verified
against, read it back: the map handed to each Agent is the merge described above.
Both agent_kwargs and sha256_digests are public and mutable, and a checkpoint's entry is read
on the load rather than at construction, so a pin assigned afterwards -- or added to an
existing entry in place -- counts.
The names the Router sets for itself -- model_id_or_path, device, token, subfolder,
revision and the hook arguments -- are refused here rather than silently shadowed, and the
remaining names are checked against Agent.__init__ at construction, so a misspelled option
fails on the Router(...) line instead of on the first request.
Artifact digests are opt-in and always per model: sha256_digests={"english": {...}}
passes that {path relative to the checkpoint dir: hexdigest} map to the Agent that
loads it, so a tampered or substituted weight file is refused before it is parsed. There
is no Router-wide equivalent of revision because digests, unlike a commit SHA, are not
shareable: the bundled repository ships a separate model.safetensors for each of
english, multilingual and typed-decisions, so one flat map can only ever match one
of them. A model listed with None or {} adds no files of its own, which loads it
unverified unless agent_kwargs["expected_sha256"] pins it; it still masks a flat
LAYA_SHA256_DIGESTS, which is what listing it that way is for.
The same split is available to a process configured only by environment: when
LAYA_SHA256_DIGESTS holds a model-keyed map ({"english": {...}, "multilingual": {...}})
this seeds it per checkpoint, so a server that keeps several resident can pin each with its
own digests instead of refusing to start on the second one. A flat LAYA_SHA256_DIGESTS
keeps its existing meaning, applied by laya.revisions to every checkpoint the process
loads, which is right for a single-checkpoint one. An argument entry wins over the
environment for the model it names. A nested variable that names some checkpoints and not
others says nothing about the others: they keep whatever agent_kwargs["expected_sha256"]
pins them with, because pinning one checkpoint from the environment is not a request to stop
verifying the rest.
Hooks are opt-in and run at the Router level: on_route sees the routing decision,
on_load / on_evict see model lifecycle, and on_predict_start / on_predict_end
wrap the whole route+infer call. See laya.hooks.
registered
property
¶
The checkpoints registered beside the built-ins: name -> source and description.
Serialisable as it is.
loaded_revisions
property
¶
Commit SHA each resident agent was loaded from (None for local paths).
resolve ¶
Canonical key of a checkpoint this Router knows: built-in, aliased, or registered.
The instance counterpart of :func:normalise_name. Everything on the Router that takes a
checkpoint name goes through here, so a checkpoint registered under papers is accepted
wherever english is: model=, task=, load, unload, preload, revisions.
register ¶
Add a checkpoint under name, to be served beside the built-in ones.
source is what laya.load accepts: a Hub repo id, a (repo, subfolder) pair, or a local
directory holding model.safetensors and rl_agent_config.json -- a fine-tune exported by
the training recipe, for instance. Nothing is downloaded or built here; the checkpoint loads
on first use like the built-ins, is evicted and unloaded like them, and counts toward
max_loaded like them.
description is free text about the checkpoint for whoever reads registered.
A name within one edit of a built-in is still registered, with a warning that names both and leaves the built-in unchanged.
A nested LAYA_SHA256_DIGESTS entry for this name is picked up here; an entry for a name the
constructor did not know still fails at construction, as before.
Returns the canonical name. Registering an existing name replaces its source and unloads the resident Agent, so the next load builds from the new source.
unregister ¶
Remove a registered checkpoint: its source, description, revision and digest entries, and resident Agent.
A built-in name is refused (re-point it with register, it cannot be removed), and so is a
name the router does not know. A resident Agent is unloaded first, waiting for an in-flight
build of it, so a request that already holds it finishes; a request that names it afterwards
gets the usual unknown-model error.
load ¶
Return the Agent for name, downloading and building it on first use.
Concurrent callers share a single Agent instead of building duplicates.
attach ¶
Register an already-built Agent under name instead of loading a second copy.
Useful when the process has a checkpoint loaded for other reasons: a demo that already
built convaiinnovations/laya can hand it to the router rather than pay for -- and hold
in memory -- a duplicate 421M parameters.
A name the Router does not know yet is registered on the spot, with no source: the agent
serves under that name while resident, and loading it again after an unload needs
register(name, source) first.
preload ¶
Download and build checkpoints up front so no request ever pays a model load.
A cold load costs seconds; language detection costs microseconds. With every
checkpoint resident, routing is effectively free -- which is what you want in a
server or a demo. max_loaded is raised to fit both the requested checkpoints and
all already-resident agents, so incremental preloading does not evict either.
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,
) -> RouteDecision
Decide which checkpoint to use, then let on_route hooks observe or replace it.
ctx.decision is the RouteDecision; a hook may replace it (for example to pin a
checkpoint) and the replacement is what gets returned and used. hooks are per-call
hooks, appended after any installed on the Router.
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, then answer every question in one forward pass on the chosen checkpoint.
The result is the usual system_one payload plus a routing key recording the decision.
Router-level on_predict_start / on_predict_end hooks wrap the whole route+infer call
and see ctx.decision; see laya.hooks. max_len / head_max_len override the agent
token budget for this call (a start hook may set ctx.max_len / ctx.head_max_len).
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, then scan every window of the state instead of only its first one.
predict scores a state from a single window: anything past max_len is cut off (the
first window, or for a conversation list the last) and never reaches the model. This
routes exactly as predict does -- the same model/task/lang hints, the same
router-level hooks, the same routing key and usage -- and scores the routed state with
that agent's predict_long, which splits it into overlapping windows and aggregates per
question. The aggregation rules are laya.agent.Agent.predict_long's: noul takes the
strongest window, choice/score the most confident one.
Per-call hooks (hooks, on_predict_start, on_predict_end, hooks_raise,
hooks_timeout) wrap the whole route+scan exactly as they wrap predict: the scan runs
last, so a start hook that answers (ctx.skip(...)) or rewrites the state wins.
max_len / head_max_len are not accepted here -- a window is sized by window or the
checkpoint budget, and overriding the single-window truncation is what predict_long is for.
Parameters:
-
window(Optional[int], default:None) –state tokens per window. Defaults to the routed checkpoint's budget (
max(64, max_len - head_max_len - 8); the 64 is a floor), capped at the room the questions leave for the state so no window is truncated again on the way in; a smaller window isolates a localized span. -
stride(Optional[int], default:None) –token step between windows; defaults to half the effective window (50% overlap). A stride past that window is a
ValueError, since the tokens between windows would reach no model. -
aggregate(str, default:'auto') –"auto" (the per-type rules above) is the only mode.
-
batch_size(Optional[int], default:None) –cap on windows per forward pass, to bound memory on very long states.
Returns:
-
Dict[str, Any]–The usual
predictpayload, withusage["windows"]counting the windows scored.
Raises:
-
TypeError–the routed agent has no
predict_long-- one attached by hand, since bothAgentandONNXAgentimplement it -- so there is nothing to scan with. Always raised, whateverhooks_raiseis, and so is every other error the scan itself raises -- none are caught: the scan is this method's own work, not a caller's hook, so the hook error policy does not decide whether it may be skipped. It previously ran as a start hook, wherehooks_raise=Falseswallowed this and returned one window scored bysystem_one-- a different question than the one asked. An agent whosepredict_longhas nolangparameter is scanned without one and warned about, not failed; that is a signature check, not a swallowed error.
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,
) -> Any
Answer state against a schema (JSON schema or pydantic model) and return typed values.
See laya.structured. Pass exactly one of schema or questions; extra keyword arguments
(for example model=, task=, hooks=) are forwarded to predict.
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]
Answer many states against one schema (JSON schema or pydantic model) in one batched call.
The throughput form of :meth:decide: the schema is planned once and its questions
run over every state through :meth:predict_batch (grouped forward passes, results in
input order), then each state's answers are projected as decide does. Extra keyword
arguments (batch_size=, model=, hooks=, ...) are forwarded to
predict_batch. See laya.structured.
route_batch ¶
route_batch(
requests: Sequence[Dict[str, Any]],
hooks_timeout: Optional[float] = None,
*,
hooks=None,
hooks_raise: Optional[bool] = None,
) -> List[RouteDecision]
Route a heterogeneous request batch without loading any checkpoints.
Each request is a mapping with state and questions plus the same optional
routing overrides accepted by :meth:route: model, task, lang and
lang_guess. The returned decisions preserve input order.
This is intentionally separate from inference so callers can inspect or aggregate routing decisions before paying model-load cost.
Parameters:
-
requests(Sequence[Dict[str, Any]]) –Sequence of request dictionaries, each requiring
stateandquestions. -
hooks_timeout(Optional[float], default:None) –Override the Router's
hooks_timeoutfor this call, applied to every request'son_routedispatch, as on :meth:route. -
hooks(HookArg, default:None) –Per-call hook or sequence of hooks for this call.
-
hooks_raise(Optional[bool], default:None) –Override the Router's
hooks_raisepolicy for this call.
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,
diagnostics: Optional[Dict[str, Any]] = None,
) -> List[Dict[str, Any]]
Route and execute a heterogeneous request batch with minimal model churn.
Requests are routed first and grouped by checkpoint. Within each checkpoint,
requests that share the same question schema are passed to
Agent.predict_batch so their states can share forward passes. Results are
then restored to the original request order.
Requests may independently specify model, task, lang,
lang_guess, max_len or head_max_len and may use different question schemas.
max_len / head_max_len are the per-request form of the token-budget override
predict takes as call arguments: they set the checkpoint's state and question-head
budgets for that one request, so a wide question can be asked without shrinking the
batch's other requests to the same window. Requests that ask for different budgets are
split into separate forward passes, since one Agent.predict_batch call carries one
budget for all its states. A start hook may still replace either value on ctx.
Router-level predict hooks run per request, as predict runs them: each request
gets its own PredictContext, so on_predict_start can replace that request's
state, questions or token budget, or ctx.skip(...) it, and on_predict_end
sees and may replace its result. Requests are grouped for the forward pass after
their start hooks have run, and a checkpoint group's requests end in reverse of the
order they started. If a checkpoint group fails, every request of it whose start hook
ran fails with the exception, a cache hit included: each gets on_error and then
on_predict_end before the exception propagates.
Parameters:
-
requests(Sequence[Dict[str, Any]]) –Sequence of request dictionaries. Every item requires
stateandquestionsand may includemodel,task,langorlang_guessrouting overrides andmax_len/head_max_lentoken-budget overrides. -
batch_size(Optional[int], default:None) –Optional maximum number of states per Agent forward-pass batch.
-
hooks_timeout(Optional[float], default:None) –Override the Router's
hooks_timeoutfor this call. -
min_confidence(Optional[float], default:None) –Optional float or per-bucket mapping for confidence gating.
-
sort_by_length(bool, default:False) –Forwarded to every
Agent.predict_batchcall, so each question group pads to a shorter maximum; seeAgent.predict_batch. Results retain the input order either way. Silently dropped for an attached agent whosepredict_batchpredates the knob (#294). -
hooks(HookArg, default:None) –Per-call hook or sequence of hooks for this call.
-
on_predict_start(PredictHookArg, default:None) –Plain callable or sequence of callables for start events.
-
on_predict_end(PredictHookArg, default:None) –Plain callable or sequence of callables for end events.
-
hooks_raise(Optional[bool], default:None) –Override the Router's
hooks_raisepolicy for this call. -
diagnostics(Optional[Dict[str, Any]], default:None) –Optional dict that receives the per-group sort outcome under the key
"sort_by_length"once the batch has run: how many question groups actually reached an agent, how many of those could reorder, and the smallest / largest group size. The reorder switches per group inside this call, so a caller that wants to say whether the knob did anything (#1111) reads this instead of guessing from the request count beforehand. Untouched whensort_by_lengthis false or no group reached an agent.
Returns:
-
List[Dict[str, Any]]–One normal Router prediction result per request, in the same order as the input.
RouteDecision ¶
Bases: dict
The routing outcome: which model, why, and what was detected.
Behaves as a dict so it serialises straight into an API response.
DEFAULT_MODELS
module-attribute
¶
DEFAULT_MODELS = {
"english": (BUNDLE_REPO, None),
"multilingual": (BUNDLE_REPO, "multilingual"),
"typed-decisions": (BUNDLE_REPO, "typed-decisions"),
}