8.5 KiB
Laya Router Hook
A local Laya multilingual service that scores each coding prompt before Codex or Claude Code sees it. The hook adds a System-1 hint: task level, and whether the agent should plan, read the repo, look up docs, or reason carefully.
It does not switch models. Codex and Claude Code keep the model already configured for that session.
Prompt
-> UserPromptSubmit hook (stdlib only, fail-open)
-> http://127.0.0.1:8765
-> Laya multilingual
-> additionalContext
-> Codex / Claude Code
A browser dashboard at http://127.0.0.1:8765/ shows each decision, grouped by session, and updates over server-sent events.
The left rail lists sessions with their latest prompt, client, infer count, and the System-1 level tag. The main panel expands one session across calls; each call shows the question that was asked, the per-axis probabilities, and the model's plain-language verdict. SSE pushes new rows without a refresh.
Cursor loads Claude Code hooks, but its prompt event cannot inject context. The hook sees CURSOR_VERSION and returns immediately.
Install
macOS, uv, and Python 3.10+.
./install.sh
The script:
- Creates
~/.laya-router/.venvand installs this repo into it - Downloads the
multilingualweights ofconvaiinnovations/laya - Registers a launchd agent,
com.laya-router, that starts at login and restarts if it exits - Writes a hook when Codex or Claude Code is already installed
The install is a normal copy, not an editable install. launchd blocks forever if it has to import a checkout under ~/Documents. laya-router restart copies the repo into the venv again and restarts the service.
One client only:
./install.sh --only codex
./install.sh --only claude
The first Codex session must trust the hook. Start codex and run /hooks. Editing the Python files does not change the command string, so Codex does not ask for review again.
Commands
After install, the binaries live in ~/.laya-router/.venv/bin.
laya-router status
laya-router logs
laya-router logs -f
laya-router route "Rename the variables in this function"
laya-router restart
laya-router stop
laya-router start
Remove the hooks and the launchd agent, and keep the runtime directory:
./uninstall.sh
Also delete config, logs, and the virtualenv:
./uninstall.sh --purge
Configuration
~/.laya-router/config.toml is written on first install and is not overwritten later.
| Key | Meaning |
|---|---|
host |
Bind address. Keep 127.0.0.1. |
port |
Default 8765. |
device |
auto, mps, cuda, or cpu. auto falls through if warmup fails. |
log_prompts |
Write the raw prompt into data/routes.jsonl. |
request_timeout |
Seconds the prompt hook waits for the service. Default 2.5. |
event_timeout |
Seconds a tool hook waits. Default 1.0. |
max_prompt_chars |
Max characters sent to the model. Longer prompts keep the head and tail. |
trajectory |
Inject a short hint after tool calls, and before an edit of unread code. |
gate |
Tighten risky Bash and edit calls. Never returns allow. |
stop_check |
Ask the agent to verify after edits before it stops. |
skills |
Suggest at most one installed skill on each user prompt. |
skill_inject |
suggest names the skill. content attaches SKILL.md once per session. |
Restart after config or server changes: laya-router restart.
How a decision is made
One forward pass asks five questions:
task_level:simple,normal, orcomplexneeds_plan,needs_repo,needs_web,needs_deep_reasoning: probabilities from 0 to 1
laya-multilingual ships without fitted temperatures, so a displayed 0.90 is not a calibrated 90%. The injected text says the numbers are hints. Ordinal score is not used; Laya's own benchmarks show it is the weakest primitive.
Every call is appended to ~/.laya-router/data/routes.jsonl (timestamp, client, working directory, session, prompt unless disabled, scores, latency, question version). That file is the material for a later coding-specific fine-tune.
If the service is down, the hook prints {} and exits 0. Codex and Claude Code continue. The error is appended to ~/.laya-router/logs/hook.log.
During a turn
The same hook command also listens for PreToolUse, PostToolUse, and Stop. The resident service keeps the session trace in memory.
PreToolUseon Bash or an edit: a high risk is denied; a medium risk asks on Claude Code and warns on Codex, because Codex rejectspermissionDecision: ask. An edit of a file that was not read gets a short hint.PostToolUse: every Bash or edit checkpoint gets a short hint. The same hint is not repeated. Two failures in a row, or a highis_stuck, tells the agent to stop and replan.Read,Grep, andGlobare recorded and stay quiet.Stop: if code changed and nothing verified it, the hook blocks once so the agent keeps going.stop_hook_activeis always allowed through.
On UserPromptSubmit, a second pair of questions can name one installed skill. The roster is every SKILL.md under ~/.claude/skills, ~/.codex/skills, ~/.agents/skills, the project skill directories, and Claude plugin install paths. A skill with disable-model-invocation: true is skipped. The first pass ranks the roster and gates on whether a skill is warranted. The second pass re-reads the top three. Either threshold below 0.30 suggests nothing.
laya-mcp exposes laya_judge over stdio. Allow mcp__laya in Claude Code if you want the agent to call it. Install registers it with claude mcp add and codex mcp add when those CLIs are on PATH.
The first Codex session must trust the new hook entries. Run /hooks again after upgrading.
Develop
PYTHONPATH=src uv run --no-project --with pytest python -m pytest
Unit tests do not load the model. After the service is up, laya-router route runs a real inference.
| File | Role | Picked up |
|---|---|---|
src/laya_router/questions.py |
Question text. Bump QUESTIONS_VERSION when the wording changes. |
laya-router restart |
src/laya_router/render.py |
Text injected into the agent | laya-router restart, then the next prompt |
src/laya_router/hook.py |
Prompt and tool events, timeout, fail-open | laya-router restart, then the next prompt |
src/laya_router/server.py |
Model load, /route, dashboard |
laya-router restart |
src/laya_router/dashboard.html |
Browser UI | laya-router restart |
Do not run the server with --reload. A reloader starts extra processes and loads the model more than once.
Security
This repository does not contain API keys, account tokens, or model weights. The installer never uploads ~/.claude/settings.json or ~/.codex.
The service has no authentication. It listens on 127.0.0.1 only. /route accepts a prompt, and the dashboard shows stored prompts. Binding any other address makes both reachable by anyone who can open that port. The process prints a warning if host is not a loopback address. Do not put this port on a public interface, and do not reverse-proxy it without adding auth yourself.
routes.jsonl stores the raw prompt by default, including anything you paste into Codex or Claude Code. Set log_prompts = false to keep only a SHA-256. The runtime directory is ~/.laya-router/, outside this repo. Do not commit it.
The installer rewrites ~/.codex/hooks.json and merges UserPromptSubmit, PreToolUse, PostToolUse, and Stop entries into ~/.claude/settings.json. That Claude file often holds API credentials. The tool preserves existing keys, writes a timestamped *.bak-* copy before changing a file, and removes only its own hook on uninstall. Read the diff before you publish a dotfile.
A web page on another origin cannot read these responses: the server does not send Access-Control-Allow-Origin. Keep the default bind address anyway.
The hook adds a hint. It does not copy the user prompt back into the injected instructions.
Limits
The multilingual base checkpoint is a fast model to specialize, not a zero-shot coding router. Public typed-decision numbers for the base checkpoints sit near chance until a domain fine-tune. Expect some complex requests to be labeled simple. Use the log, then fine-tune, before trusting the scores to choose a model.
License
Apache License 2.0. The Laya model and library are also Apache-2.0.
