@liustack/modlens 3.23.1 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +13 -4
- package/README.zh-CN.md +12 -3
- package/dist/main.js +626 -94
- package/docs/cli.md +3 -2
- package/docs/cli.zh-CN.md +3 -2
- package/docs/harness-setup.md +24 -2
- package/docs/harness-setup.zh-CN.md +8 -2
- package/docs/security.md +4 -0
- package/docs/security.zh-CN.md +4 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/troubleshooting.zh-CN.md +3 -3
- package/dsh/client.js +19 -0
- package/dsh/index.js +14 -3
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/configure.md +5 -3
- package/skills/modlens/references/configure.zh-CN.md +5 -3
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
package/docs/cli.md
CHANGED
|
@@ -98,6 +98,7 @@ Six providers: `antigravity-cli` (no key), `gemini-api` (fastest free route), `o
|
|
|
98
98
|
Other subcommands:
|
|
99
99
|
|
|
100
100
|
- `modlens guard [--model <id>]`: should the engine run for the active model at all? Exit 0 allow, 1 deny, verdict as JSON.
|
|
101
|
-
- `modlens config <init|set|show>`: `set` with the value omitted on an `apiKey` field prompts for it with the echo hidden, so the key never enters argv, shell history, or the conversation with an agent driving the terminal. It also accepts one piped line (`pbpaste | modlens config set openai.apiKey`), which keeps the key out of argv, though whatever produced that pipe is still your own command to keep out of history. Keys are `provider`, `proxy` (HTTP/HTTPS proxy for the API providers, `HTTPS_PROXY`/`HTTP_PROXY` also honored), `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`, plus `openai.structuredOutput` (that route only).
|
|
102
|
-
- `modlens
|
|
101
|
+
- `modlens config <init|set|show>`: `set` with the value omitted on an `apiKey` field prompts for it with the echo hidden, so the key never enters argv, shell history, or the conversation with an agent driving the terminal. It also accepts one piped line (`pbpaste | modlens config set openai.apiKey`), which keeps the key out of argv, though whatever produced that pipe is still your own command to keep out of history. Keys are `provider`, `proxy` (HTTP/HTTPS proxy for the API providers, `HTTPS_PROXY`/`HTTP_PROXY` also honored), `cooldown` (`on` or `off`, on by default), `reuse.<claude|codex|opencode|pi|grok>`, `guards.<denyModels|allowModels|denyWhenUnknown>`, and `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`, plus `openai.structuredOutput` (that route only). `apiKey` accepts a comma-separated list and rotates after authentication, rate-limit, or quota failures.
|
|
102
|
+
- `modlens state clear`: forget every provider cooldown in `~/.modlens/state.json`, so all providers are tried at full priority again.
|
|
103
|
+
- `modlens doctor`: Node and node:sqlite, provider readiness (including API key counts), the failover chains for this machine, the cooldown switch and any cooling keys, the detected harness, the guard's rules with a live verdict, and the Reuse section with per-harness grant decisions and discovered vision. Spends no quota; `--json` for a machine-readable report.
|
|
103
104
|
|
package/docs/cli.zh-CN.md
CHANGED
|
@@ -95,5 +95,6 @@ modlens recover-paste # pull a pasted image into a fil
|
|
|
95
95
|
其他子命令:
|
|
96
96
|
|
|
97
97
|
- `modlens guard [--model <id>]`:判断当前激活的模型到底该不该运行引擎。退出码 0 表示放行,1 表示拒绝,判定结果以 JSON 输出。
|
|
98
|
-
- `modlens config <init|set|show>`:`set` 在 `apiKey` 字段上省略值会进入不回显的输入提示,密钥不进 argv、不进 shell 历史,也不进你与终端里那个 agent 的对话。它也接受从管道读一行(`pbpaste | modlens config set openai.apiKey`),这样密钥不进 argv,但产生这条管道的命令本身是否留在历史里,由你自己决定。可用的键有 `provider`、`proxy`(API provider 的 HTTP/HTTPS 代理,也认 `HTTPS_PROXY`/`HTTP_PROXY`)、`reuse.<claude|codex|opencode|pi|grok>`、`guards.<denyModels|allowModels|denyWhenUnknown>`,以及 `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`,另有 `openai.structuredOutput
|
|
99
|
-
- `modlens
|
|
98
|
+
- `modlens config <init|set|show>`:`set` 在 `apiKey` 字段上省略值会进入不回显的输入提示,密钥不进 argv、不进 shell 历史,也不进你与终端里那个 agent 的对话。它也接受从管道读一行(`pbpaste | modlens config set openai.apiKey`),这样密钥不进 argv,但产生这条管道的命令本身是否留在历史里,由你自己决定。可用的键有 `provider`、`proxy`(API provider 的 HTTP/HTTPS 代理,也认 `HTTPS_PROXY`/`HTTP_PROXY`)、`cooldown`(`on` 或 `off`,默认打开)、`reuse.<claude|codex|opencode|pi|grok>`、`guards.<denyModels|allowModels|denyWhenUnknown>`,以及 `<provider>.<apiKey|baseUrl|model|proxy|extraBody>`,另有 `openai.structuredOutput`(仅这条路线用得上)。`apiKey` 接受英文逗号分隔的列表,鉴权、限流或配额失败后会轮换。
|
|
99
|
+
- `modlens state clear`:忘掉 `~/.modlens/state.json` 里每一条 provider 冷却,让所有 provider 重新按完整优先级尝试。
|
|
100
|
+
- `modlens doctor`:报告 Node 与 node:sqlite、各 provider 的就绪状态(含密钥数量)、本机的故障转移链、冷却开关和正在冷却的密钥、检测到的 harness、guard 规则和一次现场判定,以及 Reuse 一节里按 harness 的授权决定与发现的视觉能力。不花任何额度,`--json` 输出机器可读报告。
|
package/docs/harness-setup.md
CHANGED
|
@@ -55,7 +55,7 @@ OpenCode with DeepSeek: `opencode auth login`, pick DeepSeek and paste the key (
|
|
|
55
55
|
dsh is different from the other harnesses: modlens plugs in as a native tool, not a prompt-triggered skill. The package itself is a dsh bundle, so one command installs it into a profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.24.0
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
This registers a `modlens_read_image` tool whose schema reaches the model on every request (no trigger heuristics), runs the modlens CLI shipped inside the same package, and returns the structured evidence as the tool's canonical JSON output. Engines, reuse grants, and guard rules stay in `~/.modlens/config.json`, shared with every other harness. dsh is in developer preview and its plugin surface may change; the plugin keeps its touch small (raw tool registration, the llm adapter surface for the vision variants, the attachment reader, and one agent pre-step hook) and degrades loudly if any of them moves.
|
|
@@ -87,7 +87,7 @@ modlens ships often, and both install shapes freeze at whatever version they
|
|
|
87
87
|
got. On dsh, re-run the install with the version named:
|
|
88
88
|
|
|
89
89
|
```sh
|
|
90
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.
|
|
90
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.24.0
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
`npm view @liustack/modlens version` prints the current one, and this page is
|
|
@@ -137,6 +137,28 @@ becomes a file path and the session never holds an attachment, so nothing locks
|
|
|
137
137
|
the model selector. It only comes up after a paste on a variant or on a vision
|
|
138
138
|
model, where an attachment is the point.
|
|
139
139
|
|
|
140
|
+
### For other plugin authors: injecting images on a `(modlens vision)` route
|
|
141
|
+
|
|
142
|
+
A wrapper route's declared `inputModalities: ['text', 'image']` is a promise,
|
|
143
|
+
not decoration ([#74](https://github.com/liustack/modlens/issues/74)). Every
|
|
144
|
+
`image` block in the request — pasted by the user or injected by another
|
|
145
|
+
plugin, including blocks nested in tool results — is converted to structured
|
|
146
|
+
evidence text at request time, before the wire reaches the text-only upstream.
|
|
147
|
+
Nothing is silently dropped, so the branch "declared image → inject a native
|
|
148
|
+
image block, otherwise inject a file path" works unchanged whether the
|
|
149
|
+
declaring route is a real vision model or a modlens wrapper.
|
|
150
|
+
|
|
151
|
+
One requirement: the block must carry a host attachment reference, the shape
|
|
152
|
+
`ctx.attachments.saveImage` returns and a Web UI paste produces — the plugin
|
|
153
|
+
reads bytes through `ctx.attachments.readImage(block.attachment)`. A
|
|
154
|
+
hand-built block holding only a path or base64 payload degrades to a constant
|
|
155
|
+
read-failure placeholder. And do not add a file-path text next to an image
|
|
156
|
+
block on an image-declaring route: the block already becomes complete
|
|
157
|
+
evidence there, and the extra path invites a second read of the same image
|
|
158
|
+
through the tool — double quota, and a second wording of the same content,
|
|
159
|
+
which is exactly the prefix-cache churn
|
|
160
|
+
[#68](https://github.com/liustack/modlens/issues/68) removed.
|
|
161
|
+
|
|
140
162
|
### Paste-to-path (web profile)
|
|
141
163
|
|
|
142
164
|
Pasting an image into the dsh Web UI under a **text-only model** used to die at
|
|
@@ -55,7 +55,7 @@ OpenCode 接 DeepSeek:执行 `opencode auth login`,选择 DeepSeek 并粘贴
|
|
|
55
55
|
dsh 与其他 harness 不同:modlens 以原生工具的形式接入,而不是靠提示词触发的 skill。本包自身就是一个 dsh bundle,一条命令即可装进某个 profile:
|
|
56
56
|
|
|
57
57
|
```sh
|
|
58
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.24.0
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
@@ -73,7 +73,7 @@ dsh 的网页用户面前没有终端,所以引擎设置有一张卡片,在*
|
|
|
73
73
|
modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
|
|
74
74
|
|
|
75
75
|
```sh
|
|
76
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.
|
|
76
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.24.0
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
`npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
|
|
@@ -96,6 +96,12 @@ skill 类 harness 上,skill 是一个拷贝出来的文件夹,拷贝会保
|
|
|
96
96
|
|
|
97
97
|
走上面第一条粘贴路线就不会遇到:图片变成文件路径,会话里从不存在附件,模型选择器也就不会被锁住。只有在变体或视觉模型上粘贴时才会产生附件,而那种场景下附件本来就是目的。
|
|
98
98
|
|
|
99
|
+
### 给其他插件作者:向 `(modlens vision)` 路由注入图片
|
|
100
|
+
|
|
101
|
+
包装路由声明的 `inputModalities: ['text', 'image']` 是承诺,不是装饰([#74](https://github.com/liustack/modlens/issues/74))。请求里的每一个 `image` 块,无论来自用户粘贴还是其他插件注入(包括嵌在 tool result 里的),都会在请求时转成结构化证据文本,再发往 text-only 上游。没有任何内容被静默丢弃,所以「声明 image 就注入原生图片块,否则注入文件路径」这条分支不用区分对面是真视觉模型还是 modlens 包装。
|
|
102
|
+
|
|
103
|
+
唯一前提:块必须带宿主附件引用,即 `ctx.attachments.saveImage` 返回、Web UI 粘贴产生的那种形状,插件靠 `ctx.attachments.readImage(block.attachment)` 取字节。手搓的只含路径或 base64 的块会落进固定的读取失败占位。另外不要在声明 image 的路由上给图片块旁边再附路径文本:块在这里已经变成完整证据,多出的路径会诱导模型用工具把同一张图再读一遍,双倍配额,还产生同一内容的第二种措辞,正是 [#68](https://github.com/liustack/modlens/issues/68) 清除过的前缀缓存抖动。
|
|
104
|
+
|
|
99
105
|
### 粘贴转路径(paste-to-path,web profile)
|
|
100
106
|
|
|
101
107
|
过去在 dsh Web UI 里,**纯文本模型**下粘贴图片会死在图片准入检查这一步。插件现在带了一个浏览器端半边(由 dsh 的客户端插件系统自动加载),恰好在这种情况下接管粘贴:图片字节发到插件在 dsh web 服务器上的 `/modlens/paste` 路由(仅回环地址,校验 magic byte,上限 25 MB),落成一个私有临时文件,输入框收到的则是纯文本的文件路径。这与 Pi、OpenCode、Claude Code 递给模型的形态一致,也正是 modlens skill 和 `modlens_read_image` 工具的首要触发条件。消息里不带图片附件,准入检查根本不会触发。
|
package/docs/security.md
CHANGED
|
@@ -38,6 +38,10 @@ So the private-address guards, the magic-byte check, and the size cap protect ex
|
|
|
38
38
|
|
|
39
39
|
Text inside an image is untrusted, the same as a web page. A screenshot can contain instructions aimed at whatever reads it. The prompt says so explicitly, but that is mitigation, not a guarantee: analyze images you are willing to open, and prefer a sandboxed working directory when they came from elsewhere.
|
|
40
40
|
|
|
41
|
+
## Credentials in errors
|
|
42
|
+
|
|
43
|
+
Gateway bodies, subprocess stderr, per-attempt records, warnings, and persisted cooldown reasons all go through redaction before they travel. When a provider has several API keys, every sibling key is registered as a secret, not the joined `'k1,k2'` string as one. Truncation happens after that pass, so a cut cannot hide a key from the exact-match scrub.
|
|
44
|
+
|
|
41
45
|
## Evidence, not invention
|
|
42
46
|
|
|
43
47
|
What the engine cannot read goes into `uncertainty` rather than being filled in. v2 dropped pixel coordinates and confidence scores entirely, because those are the two fields models fabricate most convincingly.
|
package/docs/security.zh-CN.md
CHANGED
|
@@ -38,6 +38,10 @@ ModLens 调用 `agy` 时带上 `--dangerously-skip-permissions`,因为某些
|
|
|
38
38
|
|
|
39
39
|
图片里的文字是不可信的,和网页一样。一张截图可能带着写给任何读取者的指令。prompt 里明确说了这一点,但那是缓解措施,不是保证:只分析你愿意打开的图片,图片来自别处时优先用沙箱化的工作目录。
|
|
40
40
|
|
|
41
|
+
## 报错里的凭据
|
|
42
|
+
|
|
43
|
+
网关响应体、子进程 stderr、每次尝试记录、警告,以及写入冷却状态的原因,都会先脱敏再往外走。一个 provider 配了多把密钥时,每一把都会登记为秘密,而不是把 `'k1,k2'` 整串当成一把。截断发生在脱敏之后,避免切到密钥中间让精确匹配找不到。
|
|
44
|
+
|
|
41
45
|
## 证据,而非编造
|
|
42
46
|
|
|
43
47
|
引擎读不出来的内容会进入 `uncertainty`,而不是被脑补出来。v2 彻底去掉了像素坐标和置信度分数,因为这两个字段正是模型编造得最像真的。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -10,7 +10,7 @@ read_when:
|
|
|
10
10
|
|
|
11
11
|
English | [中文](troubleshooting.zh-CN.md)
|
|
12
12
|
|
|
13
|
-
Start with `modlens doctor`: it checks your Node version, which providers are ready, which one will be selected and why, and the detected harness, all without spending quota or making a network request. It catches most setup problems before you read any further.
|
|
13
|
+
Start with `modlens doctor`: it checks your Node version, which providers are ready (including how many API keys each one has), which one will be selected and why, the cooldown switch and any cooling keys, and the detected harness, all without spending quota or making a network request. It catches most setup problems before you read any further. A spent key rotates to the next one, then cools, so the next run tries a healthy key first.
|
|
14
14
|
|
|
15
15
|
Every message below is one modlens actually prints. Search this file for the words you saw.
|
|
16
16
|
|
|
@@ -163,7 +163,7 @@ simply lands on an older one. Name the exact version instead, which pnpm treats
|
|
|
163
163
|
as a deliberate request rather than a resolution:
|
|
164
164
|
|
|
165
165
|
```sh
|
|
166
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.
|
|
166
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.24.0
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
`npm view @liustack/modlens version` prints the current one. pnpm 11 installs a named
|
|
@@ -178,7 +178,7 @@ file:
|
|
|
178
178
|
|
|
179
179
|
```yaml
|
|
180
180
|
minimumReleaseAgeExclude:
|
|
181
|
-
- '@liustack/modlens@3.
|
|
181
|
+
- '@liustack/modlens@3.24.0'
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
Or lift the gate for a single command, which lifts it for everything that
|
|
@@ -10,7 +10,7 @@ read_when:
|
|
|
10
10
|
|
|
11
11
|
[English](troubleshooting.md) | 中文
|
|
12
12
|
|
|
13
|
-
先跑 `modlens doctor`:它会检查你的 Node 版本、哪些 provider
|
|
13
|
+
先跑 `modlens doctor`:它会检查你的 Node 版本、哪些 provider 已就绪(含各家有几把密钥)、将选中哪一个及其原因、冷却开关和正在冷却的密钥,以及检测到的 harness,全程不消耗额度,也不发网络请求。大多数配置问题在你继续往下读之前就能被它查出来。一把密钥用尽会先轮换到下一把,再进入冷却,下次运行会优先试还健康的密钥。
|
|
14
14
|
|
|
15
15
|
下面每条消息都是 modlens 实际会打印的。拿你看到的字眼在本文里搜索即可。
|
|
16
16
|
|
|
@@ -144,7 +144,7 @@ dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存
|
|
|
144
144
|
`@latest` 绕不开这一层,本页早先的说法是错的。冷静期先把候选版本过滤掉,dist-tag 才在剩下的里面解析,于是它直接落到了更旧的那个上。改成写死精确版本号,pnpm 会把它当作一次明确的指定,而不是一次解析:
|
|
145
145
|
|
|
146
146
|
```sh
|
|
147
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.
|
|
147
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.24.0
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
`npm view @liustack/modlens version` 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余所有包和 modlens 以后的版本仍然留在窗口后面。
|
|
@@ -153,7 +153,7 @@ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.23.1
|
|
|
153
153
|
|
|
154
154
|
```yaml
|
|
155
155
|
minimumReleaseAgeExclude:
|
|
156
|
-
- '@liustack/modlens@3.
|
|
156
|
+
- '@liustack/modlens@3.24.0'
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
|
package/dsh/client.js
CHANGED
|
@@ -192,6 +192,8 @@ window.__ModuleLoader__.load({
|
|
|
192
192
|
pickToConfigure: 'Pick an engine above to configure its key and endpoint.',
|
|
193
193
|
engine: 'Engine',
|
|
194
194
|
apiKey: 'API key',
|
|
195
|
+
apiKeyHint:
|
|
196
|
+
'Separate multiple keys with commas. ModLens rotates to the next key after authentication, rate-limit, or quota failures.',
|
|
195
197
|
baseUrl: 'Base URL',
|
|
196
198
|
model: 'Model',
|
|
197
199
|
stored: 'stored, leave empty to keep it',
|
|
@@ -219,6 +221,7 @@ window.__ModuleLoader__.load({
|
|
|
219
221
|
pickToConfigure: '在上面选一个引擎,才能配置它的密钥和地址。',
|
|
220
222
|
engine: '引擎',
|
|
221
223
|
apiKey: 'API 密钥',
|
|
224
|
+
apiKeyHint: '多个密钥用英文逗号分隔。鉴权、限流或配额失败时会自动轮换到下一个密钥。',
|
|
222
225
|
baseUrl: '接口地址',
|
|
223
226
|
model: '模型',
|
|
224
227
|
stored: '已保存,留空即不改动',
|
|
@@ -644,6 +647,22 @@ window.__ModuleLoader__.load({
|
|
|
644
647
|
'clinote',
|
|
645
648
|
)
|
|
646
649
|
: secretField(t.apiKey, 'apiKey', current.hasKey ? t.stored : t.unset),
|
|
650
|
+
draft.provider === '' || keyless
|
|
651
|
+
? null
|
|
652
|
+
: fieldRow(
|
|
653
|
+
'',
|
|
654
|
+
h(
|
|
655
|
+
'div',
|
|
656
|
+
{
|
|
657
|
+
style: {
|
|
658
|
+
fontSize: '13px',
|
|
659
|
+
color: 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))',
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
t.apiKeyHint,
|
|
663
|
+
),
|
|
664
|
+
'api-key-rotation',
|
|
665
|
+
),
|
|
647
666
|
draft.provider === '' || keyless ? null : textField(t.baseUrl, 'baseUrl', 'text', t.fallback),
|
|
648
667
|
draft.provider === '' ? null : textField(t.model, 'model', 'text', t.fallback),
|
|
649
668
|
// Where these values are coming from, said once, because the
|
package/dsh/index.js
CHANGED
|
@@ -1397,16 +1397,27 @@ const REUSE_HARNESSES = ['claude', 'codex', 'opencode', 'pi', 'grok']
|
|
|
1397
1397
|
// one source whole, so the card has to read the same two places a read does or
|
|
1398
1398
|
// it shows an empty form for an engine that works.
|
|
1399
1399
|
const ENGINE_ENV_BINDINGS = {
|
|
1400
|
-
'gemini-api': { apiKey: 'GEMINI_API_KEY' },
|
|
1400
|
+
'gemini-api': { apiKey: 'GEMINI_API_KEY', baseUrl: 'GEMINI_BASE_URL' },
|
|
1401
1401
|
openai: { apiKey: 'OPENAI_API_KEY', baseUrl: 'OPENAI_BASE_URL' },
|
|
1402
1402
|
anthropic: { apiKey: 'ANTHROPIC_API_KEY', baseUrl: 'ANTHROPIC_BASE_URL' },
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
|
+
/** Whether a comma-separated API-key value contains at least one real key. */
|
|
1406
|
+
function hasApiKeys(value) {
|
|
1407
|
+
return (
|
|
1408
|
+
typeof value === 'string' &&
|
|
1409
|
+
value
|
|
1410
|
+
.split(',')
|
|
1411
|
+
.map((key) => key.trim())
|
|
1412
|
+
.some((key) => key !== '')
|
|
1413
|
+
)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1405
1416
|
function engineEnvSettings(engine, env = process.env) {
|
|
1406
1417
|
const settings = {}
|
|
1407
1418
|
for (const [field, variable] of Object.entries(ENGINE_ENV_BINDINGS[engine] ?? {})) {
|
|
1408
1419
|
const value = typeof env[variable] === 'string' ? env[variable].trim() : ''
|
|
1409
|
-
if (value !== '') settings[field] = value
|
|
1420
|
+
if (value !== '' && (field !== 'apiKey' || hasApiKeys(value))) settings[field] = value
|
|
1410
1421
|
}
|
|
1411
1422
|
return settings
|
|
1412
1423
|
}
|
|
@@ -1472,7 +1483,7 @@ function engineSummary(config = readModlensConfig()) {
|
|
|
1472
1483
|
engines[name] = {
|
|
1473
1484
|
baseUrl: typeof settings.baseUrl === 'string' ? settings.baseUrl : '',
|
|
1474
1485
|
model: typeof settings.model === 'string' ? settings.model : '',
|
|
1475
|
-
hasKey:
|
|
1486
|
+
hasKey: hasApiKeys(settings.apiKey),
|
|
1476
1487
|
// '' means neither source holds anything, which is not the same as the
|
|
1477
1488
|
// file holding an empty entry: that one is already off its variables.
|
|
1478
1489
|
source: inFile ? 'file' : Object.keys(settings).length > 0 ? 'env' : '',
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -20,11 +20,11 @@ powershell -ExecutionPolicy Bypass -File <skill-dir>\scripts\run.ps1 <args>
|
|
|
20
20
|
|
|
21
21
|
It resolves a working runtime (PATH `modlens`, then `npx`, then `bunx`) and forwards your arguments unchanged. Exit 78 means no runtime: relay the `nextSteps` from its stderr JSON instead of retrying.
|
|
22
22
|
|
|
23
|
-
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.
|
|
23
|
+
If your harness forbids running scripts, reason through the same order by hand and run the first line that works (the pinned version is 3.24.0):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.24.0: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.24.0 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.24.0 <args>`.
|
|
28
28
|
4. Otherwise tell the user no JavaScript runtime was found and that installing Node 22.19+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
29
29
|
|
|
30
30
|
`references/runtime.md` documents the pin and the diagnostic fields.
|
|
@@ -19,11 +19,12 @@ modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model
|
|
|
19
19
|
|
|
20
20
|
## The file's exact shape
|
|
21
21
|
|
|
22
|
-
Everything lives under
|
|
22
|
+
Everything lives under seven top-level keys, all optional. This example shows every supported key and field at once (a real file only needs what you use). A missing file means all defaults. Provider settings sit under `providers.<name>`, not at the top level, which is the mistake hand-editors make most.
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
26
26
|
"provider": "gemini-api",
|
|
27
|
+
"cooldown": "on",
|
|
27
28
|
"proxy": "http://127.0.0.1:7890",
|
|
28
29
|
"reuse": { "claude": true, "codex": true, "opencode": false, "pi": true, "grok": true },
|
|
29
30
|
"saved": {
|
|
@@ -64,7 +65,8 @@ Everything lives under six top-level keys, all optional. This example shows ever
|
|
|
64
65
|
Field semantics:
|
|
65
66
|
|
|
66
67
|
- `provider`: which provider runs when `-p` is not given. Canonical names or aliases both work (`agy`/`antigravity` for `antigravity-cli`, `gemini` for `gemini-api`, `openai-compat` for `openai`, `claude` for `anthropic`, `kimi`/`kimi-code` for `kimi-cli`, `claude-code` for `claude-cli`). Empty or absent pins nothing: the failover chain decides, trying configured API providers before the agent CLIs.
|
|
67
|
-
- `
|
|
68
|
+
- `cooldown`: `'on'` (default) or `'off'`. On, a quota-spent key is remembered in `~/.modlens/state.json` and tried last until it recovers (45 minutes by default, 24 hours for monthly HTTP 432/433, or the engine-reported `Resets in` clause). Off, that file is neither read nor written. `modlens state clear` forgets every cooldown.
|
|
69
|
+
- `providers.<name>.<field>`: six fields exist, `apiKey`, `baseUrl`, `model`, `proxy`, `extraBody`, and `structuredOutput` (the openai route only). Every provider entry is optional, and every field inside it is optional. Alias keys are read too (settings saved under `gemini` are found when `gemini-api` resolves), with the canonical key winning on conflict. `apiKey` accepts a comma-separated list. Requests use the configured order and rotate only after authentication, rate-limit, or quota failures. Other failures skip remaining keys and keep provider failover.
|
|
68
70
|
- `providers.<name>.extraBody`: a JSON object merged into the request body of the API providers (`gemini-api`, `openai`, `anthropic`), for whatever knobs that vendor has and modlens has no flag for. Turning thinking off is the usual reason, see the section below. Nested objects merge key by key, so adding one knob leaves the rest of that block alone. The fields carrying the image, the prompt, and each route's own enforcement machinery are refused with an error naming the field. `response_format` on the `openai` route is not one of them: setting it there deliberately replaces the schema modlens would otherwise send. The three CLI providers take no request body, so a run on `antigravity-cli`, `claude-cli` or `kimi-cli` ignores it and says so in `meta.warnings`.
|
|
69
71
|
- `providers.openai.structuredOutput`: `true` asks an OpenAI-compatible gateway to enforce the vision contract itself, as `response_format: json_schema` in the strict form those endpoints require. Off by default, since a gateway without structured-output support answers 400 for the field. A `response_format` you set in `extraBody` wins over it.
|
|
70
72
|
- `saved.openai.<label>`: named saved copies of the openai slot, written only by `modlens config save openai <label>` and swapped in whole by `modlens config use openai <label>`. Switching gateways used to mean overwriting `providers.openai` and losing the previous key; a saved copy is where it survives. `use` refuses to overwrite an active slot that no label holds (pass `--discard` to drop it deliberately), and nothing in resolution, guards, or the env bindings reads this section: the active slot stays the only openai route in any run.
|
|
@@ -73,7 +75,7 @@ Field semantics:
|
|
|
73
75
|
- `allowModels` non-empty (allowlist mode): only the listed models run the engine, every other identified model is denied. Right for the actual 2026 landscape, where text-only models are the short list. A deny pattern still wins over an allow match, so a broad allow can have its vision variants carved out, as in the example above: `glm-5.*` allows the text line while `glm-*v*` catches `glm-5v-turbo`. Anchor allow patterns tightly (`deepseek-v4-*`, not `deepseek*`) so a vendor's next multimodal generation falls off the list and steps aside until you have checked it.
|
|
74
76
|
- List a model by what actually reaches it, not by what it could see: a multimodal model behind a gateway that strips images still needs modlens, and your session transcript records the model name the gateway reports. `modlens doctor`'s Guard section shows the rules and a live verdict for checking the result.
|
|
75
77
|
- `denyWhenUnknown` (default `false`) decides what happens when no signal identifies the active model, in either mode: `false` proceeds, `true` denies. The active model is detected from, strongest first: the `MODLENS_MODEL` env var (`none` means "treat as unknown"), the harness's session storage, the `--model` self-report.
|
|
76
|
-
- `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL` configure a provider this file says nothing about, and are ignored entirely for one it does. They used to merge field by field, which built pairings that existed nowhere: a baseUrl and an apiKey are one credential. modlens still reads `MODLENS_HARNESS` (paste-recovery and guard scope), `MODLENS_MODEL` (guard override, see `guards`), and the fingerprints harnesses inject themselves, which pin the guard's storage lookup to the current session: `CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, plus the presence markers harness detection relies on (`CLAUDECODE`, `PI_CODING_AGENT`, `CODEX_SANDBOX`).
|
|
78
|
+
- `GEMINI_API_KEY`, `GEMINI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL` configure a provider this file says nothing about, and are ignored entirely for one it does. They used to merge field by field, which built pairings that existed nowhere: a baseUrl and an apiKey are one credential. The key variables accept a comma-separated list the same way the file field does. modlens still reads `MODLENS_HARNESS` (paste-recovery and guard scope), `MODLENS_MODEL` (guard override, see `guards`), and the fingerprints harnesses inject themselves, which pin the guard's storage lookup to the current session: `CLAUDE_CODE_SESSION_ID`, `CODEX_THREAD_ID`, plus the presence markers harness detection relies on (`CLAUDECODE`, `PI_CODING_AGENT`, `CODEX_SANDBOX`).
|
|
77
79
|
- `reuse.<claude|codex|opencode|pi|grok>`: per-harness grants for spending other local logins, written by the onboarding conversation (`references/onboard.md`). `true` lets reads reuse that harness (pi credentials join the inline region with every guard intact; a signed-in Codex, an OpenCode vision model, or pi driven directly join the agent region before `claude-cli`), `false` records a refusal so the user is never re-asked, absent means never asked and nothing runs. `claude` absent counts as granted: `claude-cli` predates this model as a built-in provider, and `reuse.claude false` removes it from the chain (`-p claude-cli` still pins). Reused engines get no priority over the user's own: regions order by speed class only. Every reused answer adds a `meta.warnings` line naming whose quota it spent, and `modlens doctor`'s Reuse section shows each harness's decision plus what discovery found (probe results cache for 6 hours in `~/.modlens/auto-cache.json`; doctor always re-probes). Set with `modlens config set reuse.codex true` (empty clears back to never-asked).
|
|
78
80
|
- Unknown top-level keys and unknown provider names are ignored rather than rejected, so a typo fails quiet: run `modlens doctor` after hand-editing, it shows which file and env values are actually in effect.
|
|
79
81
|
|
|
@@ -19,11 +19,12 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
|
|
|
19
19
|
|
|
20
20
|
## 配置文件的完整形状
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
所有内容都在七个顶层键之下,全部可选。下面的示例一次性展示了所有支持的键和字段(真实文件只需要写你用到的部分)。文件不存在就全用默认值。provider 的设置放在 `providers.<name>` 下面,不在顶层,手工编辑最常犯的就是这个错。
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
26
26
|
"provider": "gemini-api",
|
|
27
|
+
"cooldown": "on",
|
|
27
28
|
"proxy": "http://127.0.0.1:7890",
|
|
28
29
|
"reuse": { "claude": true, "codex": true, "opencode": false, "pi": true, "grok": true },
|
|
29
30
|
"saved": {
|
|
@@ -64,7 +65,8 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
|
|
|
64
65
|
字段含义:
|
|
65
66
|
|
|
66
67
|
- `provider`:不传 `-p` 时由哪个 provider 执行。标准名和别名都行(`agy`/`antigravity` 对应 `antigravity-cli`,`gemini` 对应 `gemini-api`,`openai-compat` 对应 `openai`,`claude` 对应 `anthropic`,`kimi`/`kimi-code` 对应 `kimi-cli`,`claude-code` 对应 `claude-cli`)。留空或缺失表示不钉任何一个:由失败切换链决定,已配置的 API provider 先于 agent CLI 被尝试。
|
|
67
|
-
- `
|
|
68
|
+
- `cooldown`:`'on'`(默认)或 `'off'`。打开时,配额耗尽的密钥会记入 `~/.modlens/state.json`,恢复前放到队尾再试(默认 45 分钟,月度 HTTP 432/433 为 24 小时,引擎回报的 `Resets in` 子句优先)。关闭时不读也不写那个文件。`modlens state clear` 会忘掉全部冷却。
|
|
69
|
+
- `providers.<name>.<field>`:共六个字段,`apiKey`、`baseUrl`、`model`、`proxy`、`extraBody`、`structuredOutput`(仅 openai 路线)。每个 provider 条目都可选,条目里的每个字段也都可选。别名键同样会被读取(存在 `gemini` 下的设置在解析到 `gemini-api` 时也能找到),冲突时标准键胜出。`apiKey` 接受英文逗号分隔的列表。请求按配置顺序使用,只在鉴权、限流或配额失败后轮换。其他失败会跳过剩余密钥,并继续走现有的 provider 故障转移。
|
|
68
70
|
- `providers.<name>.extraBody`:一个 JSON 对象,合并进 API provider(`gemini-api`、`openai`、`anthropic`)的请求体,用来传厂商有而 modlens 没有对应参数的开关。最常见的用途是关掉思考,见下文小节。嵌套对象逐键合并,所以加一个开关不会动到该块里的其他内容。承载图片、提示词和各路线自身强制机制的字段会被拒绝,报错会点名该字段。`openai` 路线上的 `response_format` 不在此列:在那里设置它就是有意替换掉 modlens 本来会发的那份 schema。三个 CLI provider 不发请求体,所以在 `antigravity-cli`、`claude-cli` 或 `kimi-cli` 上运行时它会被忽略,并在 `meta.warnings` 里说明。
|
|
69
71
|
- `providers.openai.structuredOutput`:设为 `true` 时,让 OpenAI 兼容网关自己强制执行视觉契约,以 `response_format: json_schema` 的严格形式发出。默认关闭,因为不支持结构化输出的网关会对这个字段返回 400。你在 `extraBody` 里设的 `response_format` 优先级更高。
|
|
70
72
|
- `saved.openai.<标签>`:openai 槽的命名存档,只有 `modlens config save openai <标签>` 写入、`modlens config use openai <标签>` 整包换入。切换网关不再丢上一个端点的 key:`use` 拒绝覆盖没有任何标签保存过的活跃槽(`--discard` 表示明确放弃)。解析、guard、failover、环境变量规则都不读这个区,活跃槽始终是唯一生效的 openai 路由。
|
|
@@ -73,7 +75,7 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
|
|
|
73
75
|
- `allowModels` 非空(白名单模式):只有列出的模型运行引擎,其他所有已识别的模型一律拒绝。适合 2026 年的实际格局,纯文本模型才是那份短名单。deny 模式仍然优先于 allow 匹配,所以宽泛的 allow 可以把视觉变体剔出去,正如上面的示例:`glm-5.*` 放行文本系列,`glm-*v*` 抓住 `glm-5v-turbo`。allow 模式要锚定得紧一些(写 `deepseek-v4-*` 而不是 `deepseek*`),这样厂商下一代多模态型号会自动掉出名单,等你检查过再上场。
|
|
74
76
|
- 按真正抵达模型的内容来列名单,而不是按它本来能看到什么:多模态模型如果躲在一个剥离图片的网关后面,照样需要 modlens,而你的会话记录里存的是网关上报的模型名。`modlens doctor` 的 Guard 一节会显示规则和一条实时判定,方便核对结果。
|
|
75
77
|
- `denyWhenUnknown`(默认 `false`)决定在两种模式下,当没有任何信号能识别当前模型时怎么办:`false` 放行,`true` 拒绝。当前模型的检测来源从强到弱依次是:`MODLENS_MODEL` 环境变量(`none` 表示「按未知处理」)、harness 的会话存储、`--model` 自报。
|
|
76
|
-
- `GEMINI_API_KEY`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL` 用来配置本文件只字未提的 provider
|
|
78
|
+
- `GEMINI_API_KEY`、`GEMINI_BASE_URL`、`OPENAI_API_KEY`、`OPENAI_BASE_URL`、`ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL` 用来配置本文件只字未提的 provider。本文件提到过的,它们完全不生效。过去它们逐字段覆盖,拼出的组合在哪儿都不存在:地址和密钥本是一副凭据。密钥变量和文件字段一样接受英文逗号分隔的列表。modlens 仍然读取 `MODLENS_HARNESS`(粘贴恢复和 guard 的作用范围)、`MODLENS_MODEL`(guard 覆盖,见 `guards`),以及各 harness 自己注入的指纹,它们把 guard 的存储查询钉在当前 session 上:`CLAUDE_CODE_SESSION_ID`、`CODEX_THREAD_ID`,加上 harness 检测依赖的存在性标记(`CLAUDECODE`、`PI_CODING_AGENT`、`CODEX_SANDBOX`)。
|
|
77
79
|
- `reuse.<claude|codex|opencode|pi|grok>`:按 harness 记录的授权,决定能否花费本机其他登录态,由引导对话(`references/onboard.md`)写入。`true` 允许读图时复用该 harness(pi 的凭据加入 inline 区且所有 guard 照常生效,已登录的 Codex、OpenCode 的视觉模型或直接驱动的 pi 加入 agent 区,排在 `claude-cli` 之前),`false` 记下一次拒绝,用户不会被再次询问,缺失表示从未问过,什么都不会运行。`claude` 缺失视为已授权:`claude-cli` 作为内置 provider 早于这套模型存在,`reuse.claude false` 会把它移出链条(`-p claude-cli` 仍可钉死)。复用来的引擎不比用户自己的优先:分区只按速度档次排序。每个复用得来的答案都会在 `meta.warnings` 里加一行,说明花的是谁的额度,`modlens doctor` 的 Reuse 一节会显示每个 harness 的决定和探测发现的结果(探测结果在 `~/.modlens/auto-cache.json` 里缓存 6 小时,doctor 每次都重新探测)。用 `modlens config set reuse.codex true` 设置(传空恢复为从未问过)。
|
|
78
80
|
- 未知的顶层键和未知的 provider 名会被忽略而不是报错,所以敲错字会无声失败:手工编辑后跑一下 `modlens doctor`,它会显示哪些文件值和环境变量真正生效。
|
|
79
81
|
|
|
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
24
24
|
# package.json version, and the release script rewrites it on every bump.
|
|
25
25
|
$Package = '@liustack/modlens'
|
|
26
26
|
$Bin = 'modlens'
|
|
27
|
-
$Pinned = '3.
|
|
27
|
+
$Pinned = '3.24.0'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -22,7 +22,7 @@ set -eu
|
|
|
22
22
|
# package.json version, and the release script rewrites it on every bump.
|
|
23
23
|
PKG="@liustack/modlens"
|
|
24
24
|
BIN="modlens"
|
|
25
|
-
PINNED="3.
|
|
25
|
+
PINNED="3.24.0"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|