Document the local router for open source and warn when it is not bound to loopback.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-24 10:08:09 +08:00
parent eae4df7d51
commit c4582ba810
3 changed files with 221 additions and 41 deletions

116
README.md
View File

@@ -1,106 +1,140 @@
# Laya Router Hook # Laya Router Hook
本地常驻 [Laya](https://github.com/NandhaKishorM/laya) multilingual 模型,在 Codex 和 Claude Code 每次提交 prompt 时注入一条 System-1 路由提示。它不切换底层模型,只补充任务复杂度、是否需要计划、读仓库、查文档和深度推理的判断。 [中文说明](README.zh-CN.md)
A local [Laya](https://github.com/NandhaKishorM/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.
```text ```text
Prompt Prompt
-> UserPromptSubmit hook(标准库,失败则放行) -> UserPromptSubmit hook (stdlib only, fail-open)
-> http://127.0.0.1:8765 -> http://127.0.0.1:8765
-> Laya multilingual -> Laya multilingual
-> additionalContext -> additionalContext
-> Codex / Claude Code -> Codex / Claude Code
``` ```
Cursor 会加载 Claude Code 的 hook 配置,但当前事件无法把上下文注入对话。Hook 发现 `CURSOR_VERSION` 时直接返回,不增加延迟。 A browser dashboard at [http://127.0.0.1:8765/](http://127.0.0.1:8765/) shows each decision, grouped by session, and updates over server-sent events.
## 安装 Cursor loads Claude Code hooks, but its prompt event cannot inject context. The hook sees `CURSOR_VERSION` and returns immediately.
需要 macOS、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和 Python 3.10+。 ## Install
macOS, [uv](https://docs.astral.sh/uv/getting-started/installation/), and Python 3.10+.
```bash ```bash
./install.sh ./install.sh
``` ```
脚本会: The script:
1. 在 `~/.laya-router/.venv` 创建虚拟环境,并把本仓库安装进去 1. Creates `~/.laya-router/.venv` and installs this repo into it
2. 下载 `convaiinnovations/laya` 的 `multilingual` 权重 2. Downloads the `multilingual` weights of `convaiinnovations/laya`
3. 注册 launchd 用户服务 `com.laya-router`(登录自启,进程退出后拉起) 3. Registers a launchd agent, `com.laya-router`, that starts at login and restarts if it exits
4. 检测到 Codex / Claude Code 时写入 hook 4. Writes a hook when Codex or Claude Code is already installed
安装的是一份普通副本,不是指向仓库的可编辑安装。仓库如果在 `~/Documents` 下,launchd 直接 import 那个目录会卡在文件打开上。`laya-router restart` 会先把当前仓库同步进虚拟环境再重启。 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:
```bash ```bash
./install.sh --only codex ./install.sh --only codex
./install.sh --only claude ./install.sh --only claude
``` ```
Codex 第一次会要求信任 hook。启动 `codex` 后运行 `/hooks` 确认。命令行本身不变时,修改 Python 不会再次触发信任审查。 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
命令来自 `~/.laya-router/.venv`。`./install.sh` 之后可以把这个 `bin` 加到 `PATH`,或直接用绝对路径。 After install, the binaries live in `~/.laya-router/.venv/bin`.
浏览器打开 [http://127.0.0.1:8765/](http://127.0.0.1:8765/) 可以按会话看每次判断。页面通过服务端事件实时追加,左边是会话,右边是这一路 prompt 的判定过程。
```bash ```bash
laya-router status laya-router status
laya-router logs laya-router logs
laya-router logs -f laya-router logs -f
laya-router route "帮我把这个函数的变量名改掉" laya-router route "Rename the variables in this function"
laya-router restart laya-router restart
laya-router stop laya-router stop
laya-router start laya-router start
``` ```
卸载 hook 和服务,但保留运行目录: Remove the hooks and the launchd agent, and keep the runtime directory:
```bash ```bash
./uninstall.sh ./uninstall.sh
``` ```
连同配置、日志、虚拟环境一起删除: Also delete config, logs, and the virtualenv:
```bash ```bash
./uninstall.sh --purge ./uninstall.sh --purge
``` ```
## 配置 ## Configuration
`~/.laya-router/config.toml` 在首次安装时生成,之后不会被覆盖。 `~/.laya-router/config.toml` is written on first install and is not overwritten later.
| 键 | 含义 | | Key | Meaning |
| --- | --- | | --- | --- |
| `device` | `auto`、`mps`、`cuda` 或 `cpu`。`auto` 预热失败会回退 | | `host` | Bind address. Keep `127.0.0.1`. |
| `log_prompts` | 是否把原文写入 `data/routes.jsonl` | | `port` | Default `8765`. |
| `request_timeout` | Hook 等待服务的秒数,默认 2.5 | | `device` | `auto`, `mps`, `cuda`, or `cpu`. `auto` falls through if warmup fails. |
| `max_prompt_chars` | 送入模型的最大字符数,超长保留头尾 | | `log_prompts` | Write the raw prompt into `data/routes.jsonl`. |
| `request_timeout` | Seconds the hook waits for the service. Default `2.5`. |
| `max_prompt_chars` | Max characters sent to the model. Longer prompts keep the head and tail. |
改完配置或服务端代码后执行 `laya-router restart`。 Restart after config or server changes: `laya-router restart`.
## 迭代 ## How a decision is made
改完保存后执行 `laya-router restart`。它会把仓库同步进 `~/.laya-router` 再拉起服务。Hook 是每次 prompt 新起的进程,同步完成后下一条 prompt 就会用到新的 `hook.py` 和 `render.py`。 One forward pass asks five questions:
| 文件 | 作用 | 生效方式 | - `task_level`: `simple`, `normal`, or `complex`
| --- | --- | --- | - `needs_plan`, `needs_repo`, `needs_web`, `needs_deep_reasoning`: probabilities from 0 to 1
| `src/laya_router/questions.py` | 路由问题。改措辞时把 `QUESTIONS_VERSION` 加一 | `laya-router restart` |
| `src/laya_router/render.py` | 注入给 Agent 的提示文本 | `laya-router restart`,然后下一条 prompt |
| `src/laya_router/hook.py` | 超时、跳过斜杠命令、失败放行 | `laya-router restart`,然后下一条 prompt |
| `src/laya_router/server.py` | 模型加载与 `/route` | `laya-router restart` |
不要给服务加 `--reload`。开发模式会再起进程,把模型加载多次。 `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.
路由记录在 `~/.laya-router/data/routes.jsonl`,包含判断、耗时和问题版本,用来以后对照真实任务做微调。`laya-multilingual` 没有自带温度校准,注入文本里的概率只是提示。 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.
服务挂掉时 hook 输出 `{}` 并退出 0,Codex 和 Claude Code 照常工作。原因写在 `~/.laya-router/logs/hook.log`。 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`.
## 开发 ## Develop
```bash ```bash
PYTHONPATH=src uv run --no-project --with pytest python -m pytest PYTHONPATH=src uv run --no-project --with pytest python -m pytest
``` ```
单元测试不加载模型。装好服务后用 `laya-router route` 做真实推理检查。 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` | Timeout, slash-command skip, 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 a `UserPromptSubmit` entry 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
No license file is in this repository yet. Add one before you publish it. The Laya model and library are Apache-2.0; this project's license is a separate choice.

140
README.zh-CN.md Normal file
View File

@@ -0,0 +1,140 @@
# Laya Router Hook
[English](README.md)
本地常驻 [Laya](https://github.com/NandhaKishorM/laya) multilingual 模型。Codex 或 Claude Code 收到 prompt 之前,hook 会补上一条 System-1 提示:任务复杂度,以及要不要做计划、读仓库、查文档、仔细推理。
它不切换模型。当前会话用哪一个模型,仍由 Codex / Claude Code 自己的配置决定。
```text
Prompt
-> UserPromptSubmit hook(只依赖标准库,失败则放行)
-> http://127.0.0.1:8765
-> Laya multilingual
-> additionalContext
-> Codex / Claude Code
```
浏览器打开 [http://127.0.0.1:8765/](http://127.0.0.1:8765/) 可以按会话看每次判断。页面用服务端事件实时追加。
Cursor 会加载 Claude Code 的 hook,但当前事件无法把上下文注入对话。Hook 发现 `CURSOR_VERSION` 时直接返回。
## 安装
需要 macOS、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和 Python 3.10+。
```bash
./install.sh
```
脚本会:
1. 在 `~/.laya-router/.venv` 创建虚拟环境,并把本仓库安装进去
2. 下载 `convaiinnovations/laya` 的 `multilingual` 权重
3. 注册 launchd 用户服务 `com.laya-router`(登录自启,进程退出后拉起)
4. 检测到 Codex 或 Claude Code 时写入 hook
安装的是普通副本,不是可编辑安装。仓库如果在 `~/Documents` 下,launchd 直接 import 那个目录会卡在文件打开上。`laya-router restart` 会把当前仓库同步进虚拟环境再重启。
只注册一边:
```bash
./install.sh --only codex
./install.sh --only claude
```
Codex 第一次会要求信任 hook。启动 `codex` 后运行 `/hooks`。命令字符串不变时,修改 Python 不会再次要求审查。
## 日常命令
命令在 `~/.laya-router/.venv/bin`。
```bash
laya-router status
laya-router logs
laya-router logs -f
laya-router route "帮我把这个函数的变量名改掉"
laya-router restart
laya-router stop
laya-router start
```
卸掉 hook 和服务,保留运行目录:
```bash
./uninstall.sh
```
连同配置、日志、虚拟环境一起删除:
```bash
./uninstall.sh --purge
```
## 配置
`~/.laya-router/config.toml` 在首次安装时生成,之后不会被覆盖。
| 键 | 含义 |
| --- | --- |
| `host` | 监听地址。保持 `127.0.0.1`。 |
| `port` | 默认 `8765`。 |
| `device` | `auto`、`mps`、`cuda` 或 `cpu`。`auto` 预热失败会回退。 |
| `log_prompts` | 是否把原文写入 `data/routes.jsonl`。 |
| `request_timeout` | Hook 等待服务的秒数,默认 `2.5`。 |
| `max_prompt_chars` | 送进模型的最大字符数。超长时保留头尾。 |
改完配置或服务端代码后执行 `laya-router restart`。
## 一次判断里有什么
一次前向同时问五个问题:
- `task_level`:`simple`、`normal`、`complex`
- `needs_plan`、`needs_repo`、`needs_web`、`needs_deep_reasoning`:0 到 1 的概率
`laya-multilingual` 没有自带温度校准,界面上的 0.90 不是校准后的 90%。注入给 Agent 的文本也写明了这只是提示。没有使用 ordinal `score`,Laya 自己的基准里它是最弱的一种输出。
每次调用追加到 `~/.laya-router/data/routes.jsonl`(时间、客户端、目录、会话、prompt、分数、耗时、问题版本)。关掉原文记录后只留 SHA-256。这份日志是以后按自己的编程任务做微调的材料。
服务挂了时,hook 输出 `{}` 并退出 0,Codex 和 Claude Code 照常工作。原因写在 `~/.laya-router/logs/hook.log`。
## 开发
```bash
PYTHONPATH=src uv run --no-project --with pytest python -m pytest
```
单元测试不加载模型。服务起来之后,用 `laya-router route` 做真实推理。
| 文件 | 作用 | 生效 |
| --- | --- | --- |
| `src/laya_router/questions.py` | 问题措辞。改措辞时把 `QUESTIONS_VERSION` 加一。 | `laya-router restart` |
| `src/laya_router/render.py` | 注入给 Agent 的文本 | `laya-router restart`,然后下一条 prompt |
| `src/laya_router/hook.py` | 超时、跳过斜杠命令、失败放行 | `laya-router restart`,然后下一条 prompt |
| `src/laya_router/server.py` | 加载模型、`/route`、看板 | `laya-router restart` |
| `src/laya_router/dashboard.html` | 浏览器页面 | `laya-router restart` |
不要给服务加 `--reload`。开发模式会再起进程,把模型加载多次。
## 安全
这个仓库里没有 API Key、账号 token 或模型权重。安装程序也不会上传 `~/.claude/settings.json` 或 `~/.codex`。
服务没有鉴权,默认只听 `127.0.0.1`。`/route` 接受任意 prompt,看板会显示已经记下的原文。把 `host` 改成其他地址后,谁能访问这个端口,谁就能看到 prompt 并调用模型。如果绑定地址不是回环地址,进程会在标准错误里警告。不要把这个端口暴露到公网,也不要在没有自行加上鉴权之前做反向代理。
`routes.jsonl` 默认保存完整 prompt,包括你贴进 Codex 或 Claude Code 的内容。`log_prompts = false` 时只保存 SHA-256。运行目录是仓库外面的 `~/.laya-router/`,不要把它提交进 git。
安装程序会改写 `~/.codex/hooks.json`,并往 `~/.claude/settings.json` 合并一条 `UserPromptSubmit`。Claude 的这个文件里常常有 API 凭证。程序会保留原有字段,改文件前写一份带时间戳的 `*.bak-*`,卸载时只删自己的 hook。公开任何本机配置之前先看 diff。
其他网站的页面读不到这些接口:服务没有发送 `Access-Control-Allow-Origin`。监听地址仍然应该保持默认。
Hook 只附加分数和建议,不会把用户 prompt 再抄进注入指令里。
## 限度
multilingual 基础模型适合按领域微调,不是开箱即用的编程路由器。公开的 typed-decision 数字里,基础 checkpoint 在微调之前接近随机。复杂请求被标成 `simple` 是预期中的情况。先积累日志再微调,然后再考虑用它来选模型。
## 许可证
仓库里还没有许可证文件。公开发布前需要补上。Laya 模型和库是 Apache-2.0;本项目的许可证需要单独选择。

View File

@@ -81,6 +81,12 @@ def cmd_serve(_: argparse.Namespace) -> int:
print(f"laya-router {__version__} on {settings.base_url}", flush=True) print(f"laya-router {__version__} on {settings.base_url}", flush=True)
print(f"看板 {settings.base_url}/", flush=True) print(f"看板 {settings.base_url}/", flush=True)
if settings.host not in {"127.0.0.1", "localhost", "::1"}:
print(
f"警告: 服务绑定在 {settings.host},没有鉴权。看板和 /route 会暴露 prompt。请改回 127.0.0.1。",
file=sys.stderr,
flush=True,
)
uvicorn.run( uvicorn.run(
"laya_router.server:app", "laya_router.server:app",
host=settings.host, host=settings.host,