@liustack/modlens 3.25.1 → 3.25.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.25.3 - 2026-08-30
4
+
5
+ - **API provider requests survive Node 26.0's built-in fetch (empty headers, still-gzipped body) ([#91](https://github.com/liustack/modlens/issues/91)).** Importing npm undici — which modlens resolves from node_modules since #23 — claims the process-wide `undici.globalDispatcher.2` slot at load time. Node 26.0.0's built-in fetch is undici 8.0.2 and reads that same slot, so on the no-proxy path (which used the host fetch) it drove npm undici 8.10.0's Agent. The two disagree about HTTP/2 response headers: 8.0.2's fetch walks `rawHeaders` as a flat array while 8.10.0's h2 client hands over an object, so the walk runs zero times and every header is dropped — `content-encoding` included. The gzip body was never decompressed and `response.json()` threw `Unexpected token '\x1f'` on the raw bytes. Gemini's endpoint negotiates HTTP/2, which is why `gemini-api` surfaced it; HTTP/1.1 headers are already an array and are immune, as are Node 24 (built-in undici 7.x on the `.1` slot, with h2 forced off by the compat wrapper) and Node 26.8+ (built-in 8.10.0 agrees with the npm copy). `apiFetch` now never touches the host fetch: with or without a proxy it uses npm undici's own fetch with a same-sourced dispatcher, verified by an integration test over the real undici stack and against a local HTTP/2 gzip server on Node 26.0.0. A body that fails mid-read after the response arrived keeps its status and headers readable and rejects `text()`/`json()` with the original error, so 401 classification and API key rotation are unchanged, and connect failures still raise the connectivity hint. Thanks to @Sol1l0quy for a report that had already isolated the failing layer with a fetch probe and byte-for-byte replays.
6
+
7
+ ## 3.25.2 - 2026-08-28
8
+
9
+ - **GLM-5.3-Flash is recognized as native vision.** Z.ai released `glm-5.3-flash` on 2026-08-26 as the GLM-5 line's first natively multimodal model, and its name carries neither `v` nor `vision`. The reuse vision table matches the complete slug and delimited suffixes (`:free`, `-air`), so the bare slug, OpenRouter's `z-ai/glm-5.3-flash`, HuggingFace's `zai-org/GLM-5.3-Flash`, and `glm-5.3-flash:free` are judged image-capable, while a run-on name like `glm-5.3-flashlight` is not. The dsh wrapper's name gate uses the same boundary, so a catalog that copies the id without modalities does not mint a `(modlens vision)` twin and strip native sight. GLM-5.3 itself stays text-only. The guard example in `configure.md` drops the broad `glm-5.*` allow in favour of the known text spellings on both bare and namespaced forms (`glm-5.2*`, `*/glm-5.2*`, `glm-5.3`, `*/glm-5.3`). Vision denials are also namespaced: `glm-*v*` and `*/glm-*v*` carve `z-ai/glm-5.2v` and `z-ai/glm-5.2-vision` out of `*/glm-5.2*`, and flash denials use delimited patterns rather than a trailing `*`. Docs also note that GLM-5.3 and GLM-5.3-Flash cannot disable thinking.
10
+
3
11
  ## 3.25.1 - 2026-08-27
4
12
 
5
13
  - **Aborted provider calls fail over again on Node 24 ([#85](https://github.com/liustack/modlens/issues/85)).** Node 24's `DOMException` — including the `AbortError` thrown when `AbortSignal.timeout` fires or a call is cancelled — exposes `message` as a read-only getter, and two places assigned to it directly: the failover loop in `analyze()` while contextualizing a provider failure, and `runCommand()` while redacting secrets from the explained error. The assignment threw `Cannot set property message of #<AbortError> which has only a getter`, replacing the abort with a hard crash, so a timed-out provider took the whole read down instead of falling through to the next provider in the chain. On dsh this surfaced as "the vision engine failed" for any paste slow enough to hit the timeout. Both sites now write through one helper that assigns when it can and redefines the property on the same object when the getter refuses, so the error keeps its identity (`instanceof`, quota classification) and failover proceeds. Regression tests cover a real `DOMException('AbortError')` and both call paths. Thanks to @Dialong for a report that had already located both assignments and the fix.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  <img src="https://img.shields.io/badge/users-unknown-lightgrey?style=flat-square" alt="Users unknown">
26
26
  </p>
27
27
 
28
- The flagship DeepSeek and GLM chat models are text-only and cannot read images. ModLens is a plug-in vision engine that gives a text-only model sight. **ModLens reads images pasted straight into the chat**, no saving to a file and passing a path first.
28
+ DeepSeek's flagship chat models, and GLM-5.3 itself, are text-only and cannot read images. GLM-5.3-Flash is native multimodal. ModLens is a plug-in vision engine that gives a text-only model sight. **ModLens reads images pasted straight into the chat**, no saving to a file and passing a path first.
29
29
 
30
30
  ## Talk to us
31
31
 
@@ -33,9 +33,9 @@ Issues are welcome any time: [open one](https://github.com/liustack/modlens/issu
33
33
 
34
34
  ## Highlights
35
35
 
36
- **🥇 The most capable vision plugin for DeepSeek Harness (dsh):** install it instantly with one command: `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.1`. See the [setup guide](docs/harness-setup.md) for installation and update details. If the command line is not your thing but you still want to try DSH, check out <a href="https://github.com/liustack/aimanager"><b>AIManager</b></a>, the lightest desktop wrapper for DeepSeek Harness. It gets you started with zero code or configuration and installs every dependency for you with one click.
36
+ **🥇 The most capable vision plugin for DeepSeek Harness (dsh):** install it instantly with one command: `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3`. See the [setup guide](docs/harness-setup.md) for installation and update details. If the command line is not your thing but you still want to try DSH, check out <a href="https://github.com/liustack/aimanager"><b>AIManager</b></a>, the lightest desktop wrapper for DeepSeek Harness. It gets you started with zero code or configuration and installs every dependency for you with one click.
37
37
 
38
- Pasting an image works two ways. **① Just paste.** On a text-only model the pasted image lands as a private temp file and its path enters the composer (the same interaction OpenCode and Pi ship), then the `modlens_read_image` tool takes it from there. **② Pick a `(modlens vision)` entry** in the model selector (it remembers your choice, so once is enough), then paste: the thumbnail stays visible in your message, closer to the Codex app feel, and the image is converted to structured evidence at request time, answered by the same underlying route. The plugin auto-discovers every provider route carrying eligible text-only DeepSeek, GLM, or MiMo Pro models and adds a wrapped entry per route. A stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**, while extra routes like opencode-go or zai get their own. Native vision models in those families are excluded automatically. Which paste route applies is the host's per-model call: only a model its metadata positively confirms text-only is taken over, anything unconfirmed is left alone, so vision models keep their native paste ([details](docs/harness-setup.md)).
38
+ Pasting an image works two ways. **① Just paste.** On a text-only model the pasted image lands as a private temp file and its path enters the composer (the same interaction OpenCode and Pi ship), then the `modlens_read_image` tool takes it from there. **② Pick a `(modlens vision)` entry** in the model selector (it remembers your choice, so once is enough), then paste: the thumbnail stays visible in your message, closer to the Codex app feel, and the image is converted to structured evidence at request time, answered by the same underlying route. The plugin auto-discovers every provider route carrying eligible text-only DeepSeek, GLM, or MiMo Pro models and adds a wrapped entry per route. A stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**, while extra routes like opencode-go or zai get their own. Native vision models in those families, including GLM-5.3-Flash, are excluded automatically. Which paste route applies is the host's per-model call: only a model its metadata positively confirms text-only is taken over, anything unconfirmed is left alone, so vision models keep their native paste ([details](docs/harness-setup.md)).
39
39
 
40
40
  **Paste images directly in every harness.** No saving to a file and passing a path first.
41
41
 
package/README.zh-CN.md CHANGED
@@ -25,7 +25,7 @@
25
25
  <img src="https://img.shields.io/badge/users-unknown-lightgrey?style=flat-square" alt="Users unknown">
26
26
  </p>
27
27
 
28
- DeepSeek 和 GLM 的主力对话模型是纯文本的,无法进行图片识别。ModLens 借助外挂视觉引擎,为纯文本模型补上视觉能力。**ModLens 支持直接粘贴图片识别**,无需先保存成文件再提供路径。
28
+ DeepSeek 的主力对话模型和 GLM-5.3 本体仍是纯文本,无法读图。GLM-5.3-Flash 已是原生多模态。ModLens 借助外挂视觉引擎,为纯文本模型补上视觉能力。**ModLens 支持直接粘贴图片识别**,无需先保存成文件再提供路径。
29
29
 
30
30
  ## 交流
31
31
 
@@ -33,13 +33,13 @@ DeepSeek 和 GLM 的主力对话模型是纯文本的,无法进行图片识别
33
33
 
34
34
  ## 亮点
35
35
 
36
- **🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令即刻安装 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.1`。更多安装与更新细节参考 [配置手册](docs/harness-setup.zh-CN.md) 。如果用不惯命令行,也想想玩玩 DSH,推荐食用全网最轻量级的 DeepSeek Harness 桌面版封装 <a href="https://github.com/liustack/aimanager"><b> AIManager</b></a>,零代码零配置起手,一键帮你安装所有依赖环境。
36
+ **🥇 全网最强的 DeepSeek Harness(dsh)外挂视觉识别插件:**一条命令即刻安装 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3`。更多安装与更新细节参考 [配置手册](docs/harness-setup.zh-CN.md) 。如果用不惯命令行,也想想玩玩 DSH,推荐食用全网最轻量级的 DeepSeek Harness 桌面版封装 <a href="https://github.com/liustack/aimanager"><b> AIManager</b></a>,零代码零配置起手,一键帮你安装所有依赖环境。
37
37
 
38
38
  DeepSeek Harness 粘贴识图有两种玩法。
39
39
 
40
40
  **① 直接粘贴** 贴进来的图片自主转换成文件路径进输入框(与 OpenCode、Pi 同款交互),`modlens_read_image` 工具接手读图。
41
41
 
42
- **② 切到带 `(modlens vision)` 后缀的模型变体**(选择器有记忆,选一次就行)再粘贴:缩略图直接可见、所见即所得,体验更接近 Codex App。变体由插件自动发现生成:每条承载纯文本 DeepSeek 或 GLM 模型的 provider 路由各得一组包装条目(默认安装下就是 **`DeepSeek-V4-Flash (modlens vision)`** 和 **`DeepSeek-V4-Pro (modlens vision)`**,装了 opencode-go、zai 等额外路由的机器会各自多出一组),两家自己的视觉型号自动排除。走哪条通路由 host 依据真实模型元数据逐个裁决:只有被元数据确认纯文本的模型才会被接管,确认不了的一律不动,视觉模型因此保留原生贴图([细节](docs/harness-setup.zh-CN.md))。
42
+ **② 切到带 `(modlens vision)` 后缀的模型变体**(选择器有记忆,选一次就行)再粘贴:缩略图直接可见、所见即所得,体验更接近 Codex App。变体由插件自动发现生成:每条承载纯文本 DeepSeek 或 GLM 模型的 provider 路由各得一组包装条目(默认安装下就是 **`DeepSeek-V4-Flash (modlens vision)`** 和 **`DeepSeek-V4-Pro (modlens vision)`**,装了 opencode-go、zai 等额外路由的机器会各自多出一组),两家自己的视觉型号(含 GLM-5.3-Flash)自动排除。走哪条通路由 host 依据真实模型元数据逐个裁决:只有被元数据确认纯文本的模型才会被接管,确认不了的一律不动,视觉模型因此保留原生贴图([细节](docs/harness-setup.zh-CN.md))。
43
43
 
44
44
  **所有 Harness 直接粘贴图片识别** 无需先保存成文件再提供路径。
45
45
 
@@ -71,7 +71,7 @@ agy # 浏览器完成
71
71
  **DeepSeek Harness(dsh)用户不走 skill 流程**,本包就是原生 dsh 插件:
72
72
 
73
73
  ```sh
74
- npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.1
74
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
75
75
  ```
76
76
 
77
77
  装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as os from "os";
6
6
  import { fileURLToPath } from "url";
7
- import { fetch as fetch$1, Agent, ProxyAgent, EnvHttpProxyAgent } from "undici";
7
+ import { fetch, Agent, ProxyAgent, EnvHttpProxyAgent } from "undici";
8
8
  import * as dns from "dns/promises";
9
9
  import { isIP } from "net";
10
10
  import { execFileSync, spawn } from "child_process";
@@ -346,7 +346,7 @@ async function fetchRemoteImageBase64(url, timeoutMs) {
346
346
  const pinned = await assertSafeRemoteTarget(current);
347
347
  const dispatcher = pinnedDispatcher(pinned);
348
348
  dispatchers.push(dispatcher);
349
- const response = await fetch$1(current, {
349
+ const response = await fetch(current, {
350
350
  method: "GET",
351
351
  redirect: "manual",
352
352
  signal,
@@ -475,13 +475,13 @@ function connectFailureHint(error, url) {
475
475
  return `Could not connect to ${host} (${cause.code}). The request never reached the network. If this machine reaches the internet through a proxy, set HTTPS_PROXY/HTTP_PROXY, or run: modlens config set proxy <url>`;
476
476
  }
477
477
  async function apiFetch(url, init, proxy, env = process.env) {
478
- const dispatcher = apiProxyDispatcher(proxy, env);
478
+ const dispatcher = apiProxyDispatcher(proxy, env) ?? new Agent();
479
479
  try {
480
- if (dispatcher) {
481
- const response = await fetch$1(url, {
482
- ...init,
483
- dispatcher
484
- });
480
+ const response = await fetch(url, {
481
+ ...init,
482
+ dispatcher
483
+ });
484
+ try {
485
485
  const buffered = Buffer.from(await response.arrayBuffer());
486
486
  await dispatcher.close();
487
487
  return new Response(buffered, {
@@ -489,17 +489,33 @@ async function apiFetch(url, init, proxy, env = process.env) {
489
489
  statusText: response.statusText,
490
490
  headers: response.headers
491
491
  });
492
- }
493
- return await fetch(url, init);
494
- } catch (error) {
495
- if (dispatcher) {
492
+ } catch (bodyError) {
496
493
  await dispatcher.close().catch(() => {
497
494
  });
495
+ return bodyFailedResponse(response, bodyError);
498
496
  }
497
+ } catch (error) {
498
+ await dispatcher.close().catch(() => {
499
+ });
499
500
  const hint = connectFailureHint(error, url);
500
501
  throw hint ? new Error(hint, { cause: error }) : error;
501
502
  }
502
503
  }
504
+ function bodyFailedResponse(response, error) {
505
+ const cause = error instanceof Error ? error : new Error(String(error));
506
+ return new Response(
507
+ new ReadableStream({
508
+ start(controller) {
509
+ controller.error(cause);
510
+ }
511
+ }),
512
+ {
513
+ status: response.status,
514
+ statusText: response.statusText,
515
+ headers: response.headers
516
+ }
517
+ );
518
+ }
503
519
  const JSON_TEMPLATE_INSTRUCTION = `Respond with ONE JSON object only, no markdown fences, no commentary. Fill this exact structure with your findings from the image (do not repeat this template literally, replace every value):
504
520
  {"summary":"one paragraph describing the image","ocr":{"full_text":"all visible text","lines":[{"text":"one line","language":"en"}]},"layout":{"regions":[{"type":"a short kind, e.g. title, heading, paragraph, list, table, chart, form, code, image, icon, link, nav, button, search, or any other short label that fits better","reading_order":1,"text":"region text"}]},"semantics":{"scene":"what kind of scene","intent":"what the image is for","entities":[{"name":"entity","type":"kind","evidence":"where seen"}],"relations":[{"subject":"a","predicate":"relates to","object":"b"}]},"visual":{"dominant_colors":["color"],"style":"visual style","notes":["notable visual detail"]},"uncertainty":["anything unreadable or ambiguous"]}`;
505
521
  function buildVisionPrompt(options) {
@@ -2835,6 +2851,13 @@ const VISION_MODEL_PATTERNS = [
2835
2851
  "o4*",
2836
2852
  "gemini-*",
2837
2853
  "glm-*v*",
2854
+ // GLM-5.3-Flash (2026-08-26): first native multimodal in the GLM-5 line.
2855
+ // The name carries no v, so glm-*v* does not catch it. Match the complete
2856
+ // slug or a delimited suffix (:free, -air). A run-on name like
2857
+ // glm-5.3-flashlight is not the same model. GLM-5.3 itself stays text-only.
2858
+ "glm-5.3-flash",
2859
+ "glm-5.3-flash-*",
2860
+ "glm-5.3-flash:*",
2838
2861
  "qwen*-vl*",
2839
2862
  "qwen3.5-plus*",
2840
2863
  "qwen3.6-plus*",
@@ -5534,7 +5557,7 @@ function parsePositiveInt(raw, flag) {
5534
5557
  }
5535
5558
  return Number.parseInt(raw, 10);
5536
5559
  }
5537
- program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.25.1");
5560
+ program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.25.3");
5538
5561
  program.command("analyze", { isDefault: true }).description("Analyze an image into structured JSON evidence (default command)").requiredOption("-i, --input <path|url>", "Input image path or https URL").option("-o, --output <path>", "Write result JSON to a file").option("-m, --model <name>", "Provider model name").option("-p, --provider <name>", `Vision provider (${listProviders().join(", ")})`).option("--prompt <text>", "Extra focus for this image").option("--timeout <ms>", "Provider timeout in milliseconds", "180000").option("--provider-bin <path>", "Provider binary path (default: agy)").option("--workdir <path>", "Working directory for the provider").option(
5539
5562
  "--extra-body <json>",
5540
5563
  `JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
@@ -5645,7 +5668,7 @@ program.command("doctor").description(
5645
5668
  configPath: CONFIG_PATH,
5646
5669
  // Lets doctor name an installed skill copy that is older than
5647
5670
  // the CLI reporting on it (issue #33).
5648
- version: "3.25.1"
5671
+ version: "3.25.3"
5649
5672
  });
5650
5673
  const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
5651
5674
  process.stdout.write(`${output}
@@ -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.25.1
58
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
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.25.1
90
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
91
91
  ```
92
92
 
93
93
  `npm view @liustack/modlens version` prints the current one, and this page is
@@ -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.25.1
58
+ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.25.3
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.25.1
76
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
77
77
  ```
78
78
 
79
79
  `npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
@@ -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.25.1
166
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
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.25.1'
181
+ - '@liustack/modlens@3.25.3'
182
182
  ```
183
183
 
184
184
  Or lift the gate for a single command, which lifts it for everything that
@@ -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.25.1
147
+ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.25.3
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.25.1
153
153
 
154
154
  ```yaml
155
155
  minimumReleaseAgeExclude:
156
- - '@liustack/modlens@3.25.1'
156
+ - '@liustack/modlens@3.25.3'
157
157
  ```
158
158
 
159
159
  或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
package/dsh/index.js CHANGED
@@ -658,14 +658,17 @@ function restoreUpstreamSource(messages, wrapperId, upstream) {
658
658
  function registerVisionProvider(ctx, config, ownProviders, evidenceCache) {
659
659
  // Wrap only the text-only members of these families. Their own vision
660
660
  // models (present or future: deepseek-vl/ocr/janus, glm-4.5v, glm-5v-...,
661
- // deepseek-v4-flash-vision-exp) need no bridge and are excluded by name and
662
- // by declared modality. The name gate matters on its own: third-party
663
- // catalogs copy an id without its modalities, and a vision model handed a
664
- // wrapper twin loses its native sight. Family matching also strips a vendor
665
- // namespace (OpenRouter's z-ai/glm-5.2:free, ~-prefixed aliases), because
666
- // the text-only member is the same model wherever the id carries a prefix.
661
+ // glm-5.3-flash, deepseek-v4-flash-vision-exp) need no bridge and are
662
+ // excluded by name and by declared modality. The name gate matters on its
663
+ // own: third-party catalogs copy an id without its modalities, and a vision
664
+ // model handed a wrapper twin loses its native sight. Family matching also
665
+ // strips a vendor namespace (OpenRouter's z-ai/glm-5.2:free, ~-prefixed
666
+ // aliases), because the text-only member is the same model wherever the id
667
+ // carries a prefix. GLM-5.3-Flash (2026-08-26) is native multimodal without
668
+ // a v in the name, so the glm-*v* branch cannot catch it. GLM-5.3 itself
669
+ // stays wrappable.
667
670
  const families = config.families || ['deepseek', 'glm', 'mimo']
668
- const VISION_ID = /(deepseek-(vl|ocr)|janus|glm-[\d.]*v(\b|-)|\bvision\b)/i
671
+ const VISION_ID = /(deepseek-(vl|ocr)|janus|glm-[\d.]*v(\b|-)|glm-5\.3-flash(?:$|[-:])|\bvision\b)/i
669
672
  const shouldWrap = (info) => {
670
673
  const id = String(info?.id ?? '').toLowerCase()
671
674
  // The model's own name: alias marker and vendor namespace stripped. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liustack/modlens",
3
- "version": "3.25.1",
3
+ "version": "3.25.3",
4
4
  "description": "Plug-in vision for text-only LLMs, powered by the free Antigravity CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.25.1):
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.25.3):
24
24
 
25
- 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.25.1: `modlens <args>`.
26
- 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.25.1 modlens <args>`.
27
- 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.25.1 <args>`.
25
+ 1. A `modlens` on `PATH` whose major version is 3 and is at least 3.25.3: `modlens <args>`.
26
+ 2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.25.3 modlens <args>`.
27
+ 3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.25.3 <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.
@@ -33,8 +33,8 @@ Everything lives under seven top-level keys, all optional. This example shows ev
33
33
  }
34
34
  },
35
35
  "guards": {
36
- "allowModels": ["deepseek-v4-*", "glm-5.*", "minimax-m2.5*", "qwen3-coder*"],
37
- "denyModels": ["glm-*v*", "deepseek-vl*"],
36
+ "allowModels": ["deepseek-v4-*", "glm-5.2*", "*/glm-5.2*", "glm-5.3", "*/glm-5.3", "minimax-m2.5*", "qwen3-coder*"],
37
+ "denyModels": ["glm-*v*", "*/glm-*v*", "glm-5.3-flash", "glm-5.3-flash-*", "glm-5.3-flash:*", "*/glm-5.3-flash", "*/glm-5.3-flash-*", "*/glm-5.3-flash:*", "deepseek-vl*"],
38
38
  "denyWhenUnknown": false
39
39
  },
40
40
  "providers": {
@@ -72,7 +72,7 @@ Field semantics:
72
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
73
  - `guards`: the invocation guard, for people who run both text-only and vision-capable models through the same client. Both lists hold glob patterns (`*` and `?`, case-insensitive, matched against the model name and `provider/model`), set with `modlens config set guards.denyModels '["gemini-3*"]'` or `guards.allowModels` (a JSON array or a comma-separated list, empty clears). Two ways to express the same intent, pick the shorter list:
74
74
  - `denyModels` alone: everything runs the engine except the listed vision models. Right when text-only models are the majority of what you plug in.
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.
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.2*` and `*/glm-5.2*` cover the 5.2 line on bare and namespaced ids (`z-ai/glm-5.2:free`), `glm-5.3` and `*/glm-5.3` cover GLM-5.3 itself, while `glm-*v*` and `*/glm-*v*` catch `glm-5v-turbo`, `z-ai/glm-5.2v`, and `z-ai/glm-5.2-vision`, and the delimited `glm-5.3-flash` / `glm-5.3-flash-*` / `glm-5.3-flash:*` patterns (and the `*/` forms) catch `glm-5.3-flash`. The guard matches the id as stored, it does not strip a vendor prefix, so a namespaced text model needs its `*/` twin, and a namespaced vision variant needs its deny twin too. Do not write `glm-5.*` or `glm-5.3-flash*`: the first also matches `glm-5.3-flash`, the second also matches a run-on name like `glm-5.3-flashlight`. 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.
76
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.
77
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.
78
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`).
@@ -189,7 +189,7 @@ Known spellings, current as of August 2026:
189
189
  | :-- | :-- |
190
190
  | MiMo official API (`api.xiaomimimo.com/v1`) | `{"thinking":{"type":"disabled"}}` |
191
191
  | MiMo Responses-format route | `{"reasoning":{"effort":"none"}}` |
192
- | Qwen, GLM, MiMo and friends self-hosted on vLLM or SGLang | `{"chat_template_kwargs":{"enable_thinking":false}}` |
192
+ | Qwen, GLM, MiMo and friends self-hosted on vLLM or SGLang (GLM-5.3 and GLM-5.3-Flash cannot disable thinking) | `{"chat_template_kwargs":{"enable_thinking":false}}` |
193
193
  | OpenAI-style gateways that accept an effort level | `{"reasoning_effort":"low"}` |
194
194
  | `gemini-api`, Gemini 3 family | `{"generationConfig":{"thinkingConfig":{"thinkingLevel":"LOW"}}}` |
195
195
  | `gemini-api`, Gemini 2.5 Flash and Flash Lite | `{"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}` |
@@ -33,8 +33,8 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
33
33
  }
34
34
  },
35
35
  "guards": {
36
- "allowModels": ["deepseek-v4-*", "glm-5.*", "minimax-m2.5*", "qwen3-coder*"],
37
- "denyModels": ["glm-*v*", "deepseek-vl*"],
36
+ "allowModels": ["deepseek-v4-*", "glm-5.2*", "*/glm-5.2*", "glm-5.3", "*/glm-5.3", "minimax-m2.5*", "qwen3-coder*"],
37
+ "denyModels": ["glm-*v*", "*/glm-*v*", "glm-5.3-flash", "glm-5.3-flash-*", "glm-5.3-flash:*", "*/glm-5.3-flash", "*/glm-5.3-flash-*", "*/glm-5.3-flash:*", "deepseek-vl*"],
38
38
  "denyWhenUnknown": false
39
39
  },
40
40
  "providers": {
@@ -72,7 +72,7 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
72
72
  - `saved.openai.<标签>`:openai 槽的命名存档,只有 `modlens config save openai <标签>` 写入、`modlens config use openai <标签>` 整包换入。切换网关不再丢上一个端点的 key:`use` 拒绝覆盖没有任何标签保存过的活跃槽(`--discard` 表示明确放弃)。解析、guard、failover、环境变量规则都不读这个区,活跃槽始终是唯一生效的 openai 路由。
73
73
  - `guards`:调用 guard,给在同一个客户端里既跑纯文本模型又跑视觉模型的人用。两个列表都放 glob 模式(支持 `*` 和 `?`,不区分大小写,同时匹配模型名和 `provider/model`),用 `modlens config set guards.denyModels '["gemini-3*"]'` 或 `guards.allowModels` 设置(JSON 数组或逗号分隔的列表都行,传空则清除)。两种写法表达同一个意图,选列表更短的那种:
74
74
  - 只用 `denyModels`:除了列出的视觉模型,其余全部运行引擎。适合你接入的模型大多是纯文本的情况。
75
- - `allowModels` 非空(白名单模式):只有列出的模型运行引擎,其他所有已识别的模型一律拒绝。适合 2026 年的实际格局,纯文本模型才是那份短名单。deny 模式仍然优先于 allow 匹配,所以宽泛的 allow 可以把视觉变体剔出去,正如上面的示例:`glm-5.*` 放行文本系列,`glm-*v*` 抓住 `glm-5v-turbo`。allow 模式要锚定得紧一些(写 `deepseek-v4-*` 而不是 `deepseek*`),这样厂商下一代多模态型号会自动掉出名单,等你检查过再上场。
75
+ - `allowModels` 非空(白名单模式):只有列出的模型运行引擎,其他所有已识别的模型一律拒绝。适合 2026 年的实际格局,纯文本模型才是那份短名单。deny 模式仍然优先于 allow 匹配,所以宽泛的 allow 可以把视觉变体剔出去,正如上面的示例:`glm-5.2*` 和 `*/glm-5.2*` 覆盖裸名和带命名空间的 5.2 系列(`z-ai/glm-5.2:free`),`glm-5.3` 和 `*/glm-5.3` 覆盖 GLM-5.3 本体,`glm-*v*` 和 `*/glm-*v*` 抓住 `glm-5v-turbo`、`z-ai/glm-5.2v` 与 `z-ai/glm-5.2-vision`,带分隔符的 `glm-5.3-flash` / `glm-5.3-flash-*` / `glm-5.3-flash:*`(以及对应的 `*/` 形式)抓住 `glm-5.3-flash`。guard 按存下来的 id 匹配,不会剥掉厂商前缀,所以带命名空间的文本型号需要那条 `*/` 配对,带命名空间的视觉变体也需要对应的 deny 配对。不要写 `glm-5.*` 或 `glm-5.3-flash*`:前者也会匹配 `glm-5.3-flash`,后者也会匹配 `glm-5.3-flashlight` 这种连写。allow 模式要锚定得紧一些(写 `deepseek-v4-*` 而不是 `deepseek*`),这样厂商下一代多模态型号会自动掉出名单,等你检查过再上场。
76
76
  - 按真正抵达模型的内容来列名单,而不是按它本来能看到什么:多模态模型如果躲在一个剥离图片的网关后面,照样需要 modlens,而你的会话记录里存的是网关上报的模型名。`modlens doctor` 的 Guard 一节会显示规则和一条实时判定,方便核对结果。
77
77
  - `denyWhenUnknown`(默认 `false`)决定在两种模式下,当没有任何信号能识别当前模型时怎么办:`false` 放行,`true` 拒绝。当前模型的检测来源从强到弱依次是:`MODLENS_MODEL` 环境变量(`none` 表示「按未知处理」)、harness 的会话存储、`--model` 自报。
78
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`)。
@@ -180,7 +180,7 @@ modlens config set openai.extraBody '' # 清
180
180
  | :-- | :-- |
181
181
  | MiMo 官方 API(`api.xiaomimimo.com/v1`) | `{"thinking":{"type":"disabled"}}` |
182
182
  | MiMo Responses 格式路由 | `{"reasoning":{"effort":"none"}}` |
183
- | Qwen、GLM、MiMo 等自建在 vLLM 或 SGLang 上 | `{"chat_template_kwargs":{"enable_thinking":false}}` |
183
+ | Qwen、GLM、MiMo 等自建在 vLLM 或 SGLang 上(GLM-5.3 与 GLM-5.3-Flash 不支持关闭思考) | `{"chat_template_kwargs":{"enable_thinking":false}}` |
184
184
  | 接受 effort 档位的 OpenAI 风格网关 | `{"reasoning_effort":"low"}` |
185
185
  | `gemini-api`,Gemini 3 系列 | `{"generationConfig":{"thinkingConfig":{"thinkingLevel":"LOW"}}}` |
186
186
  | `gemini-api`,Gemini 2.5 Flash 与 Flash Lite | `{"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}` |
@@ -8,7 +8,7 @@ shell syntax.
8
8
 
9
9
  ## Pinned version
10
10
 
11
- - Pinned CLI version: 3.25.1
11
+ - Pinned CLI version: 3.25.3
12
12
  - npm package: `@liustack/modlens`
13
13
  - CLI binary name: `modlens`
14
14
 
@@ -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.25.1'
27
+ $Pinned = '3.25.3'
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.1"
25
+ PINNED="3.25.3"
26
26
  # -------------------------------------------------------------------------------
27
27
 
28
28
  NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"