From c4582ba810e31773e6f21ef2e1d495096b32d9ef Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 24 Sep 2026 10:08:09 +0800 Subject: [PATCH] Document the local router for open source and warn when it is not bound to loopback. Co-authored-by: Cursor --- README.md | 116 ++++++++++++++++++++++------------ README.zh-CN.md | 140 +++++++++++++++++++++++++++++++++++++++++ src/laya_router/cli.py | 6 ++ 3 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 README.zh-CN.md diff --git a/README.md b/README.md index c4df10b..9da3e80 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,140 @@ # 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 Prompt - -> UserPromptSubmit hook(标准库,失败则放行) + -> UserPromptSubmit hook (stdlib only, fail-open) -> http://127.0.0.1:8765 -> Laya multilingual -> additionalContext -> 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 ./install.sh ``` -脚本会: +The script: -1. 在 `~/.laya-router/.venv` 创建虚拟环境,并把本仓库安装进去 -2. 下载 `convaiinnovations/laya` 的 `multilingual` 权重 -3. 注册 launchd 用户服务 `com.laya-router`(登录自启,进程退出后拉起) -4. 检测到 Codex / Claude Code 时写入 hook +1. Creates `~/.laya-router/.venv` and installs this repo into it +2. Downloads the `multilingual` weights of `convaiinnovations/laya` +3. Registers a launchd agent, `com.laya-router`, that starts at login and restarts if it exits +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 ./install.sh --only codex ./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`,或直接用绝对路径。 - -浏览器打开 [http://127.0.0.1:8765/](http://127.0.0.1:8765/) 可以按会话看每次判断。页面通过服务端事件实时追加,左边是会话,右边是这一路 prompt 的判定过程。 +After install, the binaries live in `~/.laya-router/.venv/bin`. ```bash laya-router status laya-router logs laya-router logs -f -laya-router route "帮我把这个函数的变量名改掉" +laya-router route "Rename the variables in this function" laya-router restart laya-router stop laya-router start ``` -卸载 hook 和服务,但保留运行目录: +Remove the hooks and the launchd agent, and keep the runtime directory: ```bash ./uninstall.sh ``` -连同配置、日志、虚拟环境一起删除: +Also delete config, logs, and the virtualenv: ```bash ./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` 预热失败会回退 | -| `log_prompts` | 是否把原文写入 `data/routes.jsonl` | -| `request_timeout` | Hook 等待服务的秒数,默认 2.5 | -| `max_prompt_chars` | 送入模型的最大字符数,超长保留头尾 | +| `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 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: -| 文件 | 作用 | 生效方式 | -| --- | --- | --- | -| `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` | +- `task_level`: `simple`, `normal`, or `complex` +- `needs_plan`, `needs_repo`, `needs_web`, `needs_deep_reasoning`: probabilities from 0 to 1 -不要给服务加 `--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 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. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..400baf3 --- /dev/null +++ b/README.zh-CN.md @@ -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;本项目的许可证需要单独选择。 diff --git a/src/laya_router/cli.py b/src/laya_router/cli.py index 6be055b..52fd4d1 100644 --- a/src/laya_router/cli.py +++ b/src/laya_router/cli.py @@ -81,6 +81,12 @@ def cmd_serve(_: argparse.Namespace) -> int: print(f"laya-router {__version__} on {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( "laya_router.server:app", host=settings.host,