@liustack/modlens 3.17.0 → 3.17.2
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 +9 -0
- package/README.md +8 -1
- package/README.zh-CN.md +9 -2
- package/dist/main.js +63 -11
- package/docs/harness-setup.md +2 -2
- package/docs/harness-setup.zh-CN.md +2 -2
- package/docs/troubleshooting.md +2 -2
- package/docs/troubleshooting.zh-CN.md +2 -2
- package/dsh/index.js +54 -1
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.17.2 - 2026-08-16
|
|
4
|
+
|
|
5
|
+
- **dsh: reasoning blocks come back on the `(modlens vision)` models ([#49](https://github.com/liustack/modlens/issues/49)).** Sessions routed through a wrapper model lost the model's reasoning blocks and wrote the chain of thought straight into the answer text, measured by the reporter at roughly 57% of turns against a 1% baseline on the identical upstream route, and fixed the instant the session switched back. Nothing was wrong with the messages, which is why passing them through unchanged looked correct: dsh removes an assistant message's adapter-private replay state whenever the provider recorded on that message belongs to a different adapter instance than the one about to run, and a wrapper is a different instance by construction. That state is what carries reasoning continuity, so every turn the wrapper had produced arrived upstream without it, and the longer the conversation the more turns were affected. The copy sent over the wire now names upstream as the source of the turns upstream actually produced. The durable session log keeps the wrapper id, so the UI and the model selector still show the route you picked. This applies only where the wrapper id proves which upstream made those turns, which is what auto-discovery's `modlens-<provider>` naming does; a hand-configured upstream under some other id could be repointed between runs, and relabelling there would hand one adapter another adapter's private state, so it is left alone. Thanks to @mmyhj, who measured it across three sessions before and after the switch rather than reporting an impression.
|
|
6
|
+
|
|
7
|
+
## 3.17.1 - 2026-08-16
|
|
8
|
+
|
|
9
|
+
- **A model that closes its JSON early no longer fails the whole read ([#45](https://github.com/liustack/modlens/issues/45)).** On the `openai` route, qwen3-vl intermittently ends the object after `semantics`, keeps writing the fields it still owed, and adds a stray quote. Extraction sliced from the first `{` to the last `}`, so it swallowed the fragment and failed, and the read died as `non-JSON output`, which reads exactly like a truncation. Extraction now walks the braces with string literals and escapes accounted for and keeps the largest top-level balanced span that parses, so the recoverable object comes back and the schema check names the fields that are genuinely missing. The old first-brace-to-last-brace slice still runs first, so nothing it already read correctly changes. When nothing parses, the error stops guessing why: `finish_reason=length` points at `max_tokens`, a `stop` points at `openai.structuredOutput`, and anything else (`content_filter`, a gateway's own reason) is reported as the gateway's own early ending rather than described as normal. Errors about how output ended now show the end of it rather than the opening. Thanks to @abyss-stars, who captured the raw response through a proxy and came back to verify which half was still broken.
|
|
10
|
+
- **A private endpoint can no longer reach your terminal through a gateway error.** Every error quoting model or gateway output is now redacted against both the configured key and the configured `baseUrl`, and redacted before it is clipped rather than after: a clip landing inside a hostname left nothing for an exact match to find. This covers the HTTP error body, the model content, the schema mismatch, and a `finish_reason` the gateway invented. The body previously masked only the key, so a private gateway hostname reached terminals, CI logs, and failover warnings.
|
|
11
|
+
|
|
3
12
|
## 3.17.0 - 2026-08-16
|
|
4
13
|
|
|
5
14
|
- **Breaking: a provider takes its settings from one place, whole ([#42](https://github.com/liustack/modlens/issues/42)).** `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL` used to override `~/.modlens/config.json` field by field. The reported failure was a configured Kimi endpoint answering 401, and the reason is worth stating precisely: a baseUrl and an apiKey are one credential, so taking the endpoint from the file and the key from an ambient variable built a pairing that existed in neither place, and nothing in the error named the environment as the source. Merging the other way round keeps that shape, which is why this is not a change of precedence: name a provider in the config file and the file is its source, name nothing and the environment is. A container or CI job that only exports variables keeps working exactly as before, both halves still matching. What changes is the mixture. If the file configures a provider and you were completing it from the environment, put the rest in the file: a key variable becomes `modlens config set <provider>.apiKey` at the hidden prompt, an endpoint variable becomes `modlens config set <provider>.baseUrl "$THE_VARIABLE"` (`$env:THE_VARIABLE` in PowerShell), and until you do, a run that names such a provider refuses with that exact command rather than quietly using the vendor's own endpoint. Naming a provider is about the key existing, not what it holds: an entry emptied down to `{}` still takes that provider off its variables, since the alternative is a file you edited quietly handing the provider back to the environment. `modlens config show` and `modlens doctor` label each provider with the source actually in effect. `MODLENS_MODEL`, `MODLENS_HARNESS` and the `HTTPS_PROXY` family are untouched: none is a credential, and none can be split from a matching half. This also defuses the documented `ANTHROPIC_BASE_URL` trap, where a shell that routed Claude Code through a text-only gateway silently sent vision requests there too. Thanks to @StarChen-Cycler for a report that arrived with the mechanism, a deterministic repro in both directions, and a patch.
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Issues are welcome any time: [open one](https://github.com/liustack/modlens/issu
|
|
|
34
34
|
|
|
35
35
|
## Highlights
|
|
36
36
|
|
|
37
|
-
**🥇 The first vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.
|
|
37
|
+
**🥇 The first vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.2`, and the text-only DeepSeek model behind dsh reads images through a native `modlens_read_image` tool. Updating is the same command again. The version is named rather than `@latest` on purpose: pnpm 11 holds back releases published in the last 24 hours and resolves the tag against what survives, so `@latest` would install whatever shipped a day ago ([details](docs/harness-setup.md#keeping-it-up-to-date)).
|
|
38
38
|
|
|
39
39
|
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 — and 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 text-only DeepSeek or GLM models and adds a wrapped entry per route (a stock install gets **`DeepSeek-V4-Flash (modlens vision)`** and **`DeepSeek-V4-Pro (modlens vision)`**; extra routes like opencode-go or zai get their own); the two families' own vision models 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)).
|
|
40
40
|
|
|
@@ -165,6 +165,13 @@ npx -y skills add liustack/vibemaster -g
|
|
|
165
165
|
|
|
166
166
|
⭐ If it helps, star [ModLens](https://github.com/liustack/modlens) and [VibeMaster](https://github.com/liustack/vibemaster). Stars are how the next developer finds them.
|
|
167
167
|
|
|
168
|
+
## Key ecosystem partners
|
|
169
|
+
|
|
170
|
+
The projects worth recommending in the DeepSeek Harness ecosystem.
|
|
171
|
+
|
|
172
|
+
- 🛒 **[dsh-market](https://github.com/dsh-market/dsh-market)** — The plugin market inside DeepSeek Harness. Browse 800+ community plugins with category filters and screenshot previews, one-click install and update, and live theme switching. Most need no restart.
|
|
173
|
+
DeepSeek Harness 的可视化插件市场。设置页里直接逛社区全部 800+ 插件:分类筛选、截图预览、一键安装与更新、主题即点即换,装完多数免重启。
|
|
174
|
+
|
|
168
175
|
## Star History
|
|
169
176
|
|
|
170
177
|
<a href="https://www.star-history.com/?repos=liustack%2Fmodlens&type=date&legend=top-left">
|
package/README.zh-CN.md
CHANGED
|
@@ -34,7 +34,7 @@ DeepSeek 和 GLM 的主力对话模型是纯文本的,无法进行图片识别
|
|
|
34
34
|
|
|
35
35
|
## 亮点
|
|
36
36
|
|
|
37
|
-
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.
|
|
37
|
+
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.2`,dsh 背后的纯文本 DeepSeek 模型即可通过原生 `modlens_read_image` 工具读图。更新就是再跑一遍同一条命令。这里点名版本号而不用 `@latest` 是有意的:pnpm 11 会扣住最近 24 小时内发布的版本,dist-tag 只在剩下的里面解析,用 `@latest` 装到的会是一天前发布的那个([细节](docs/harness-setup.zh-CN.md#保持更新))。
|
|
38
38
|
|
|
39
39
|
DeepSeek Harness 粘贴识图有两种玩法。
|
|
40
40
|
|
|
@@ -69,7 +69,7 @@ agy # 浏览器完成
|
|
|
69
69
|
**DeepSeek Harness(dsh)用户不走 skill 流程**,本包就是原生 dsh 插件:
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.
|
|
72
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.2
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
|
|
@@ -177,6 +177,13 @@ Codex 桌面 App 中识别一张推文截图。作者、配文、照片内容(
|
|
|
177
177
|
|
|
178
178
|
⭐ 如果它对你有用,请给 [ModLens](https://github.com/liustack/modlens) 一个 star,这是其他开发者找到它的方式。
|
|
179
179
|
|
|
180
|
+
## 重要生态伙伴
|
|
181
|
+
|
|
182
|
+
DeepSeek Harness 生态里最值得推荐的项目。
|
|
183
|
+
|
|
184
|
+
- 🛒 **[dsh-market](https://github.com/dsh-market/dsh-market)** — DeepSeek Harness 的可视化插件市场。设置页里直接逛社区全部 800+ 插件:分类筛选、截图预览、一键安装与更新、主题即点即换,装完多数免重启。
|
|
185
|
+
The plugin market inside DeepSeek Harness. Browse 800+ community plugins with category filters and screenshot previews, one-click install and update, and live theme switching. Most need no restart.
|
|
186
|
+
|
|
180
187
|
## Star History
|
|
181
188
|
|
|
182
189
|
<a href="https://www.star-history.com/?repos=liustack%2Fmodlens&type=date&legend=top-left">
|
package/dist/main.js
CHANGED
|
@@ -773,13 +773,62 @@ function parseBraceSlice(trimmed) {
|
|
|
773
773
|
const first = trimmed.indexOf("{");
|
|
774
774
|
const last = trimmed.lastIndexOf("}");
|
|
775
775
|
if (first >= 0 && last > first) {
|
|
776
|
-
|
|
776
|
+
const whole = tryParseJson(trimmed.slice(first, last + 1));
|
|
777
|
+
if (whole !== null) {
|
|
778
|
+
return whole;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return parseLongestBalancedObject(trimmed);
|
|
782
|
+
}
|
|
783
|
+
function parseLongestBalancedObject(text) {
|
|
784
|
+
const spans = [];
|
|
785
|
+
let depth = 0;
|
|
786
|
+
let start = -1;
|
|
787
|
+
let inString = false;
|
|
788
|
+
let escaped = false;
|
|
789
|
+
for (let i = 0; i < text.length; i++) {
|
|
790
|
+
const char = text[i];
|
|
791
|
+
if (inString) {
|
|
792
|
+
if (escaped) {
|
|
793
|
+
escaped = false;
|
|
794
|
+
} else if (char === "\\") {
|
|
795
|
+
escaped = true;
|
|
796
|
+
} else if (char === '"') {
|
|
797
|
+
inString = false;
|
|
798
|
+
}
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (char === '"') {
|
|
802
|
+
inString = true;
|
|
803
|
+
} else if (char === "{") {
|
|
804
|
+
if (depth === 0) {
|
|
805
|
+
start = i;
|
|
806
|
+
}
|
|
807
|
+
depth++;
|
|
808
|
+
} else if (char === "}") {
|
|
809
|
+
if (depth > 0) {
|
|
810
|
+
depth--;
|
|
811
|
+
if (depth === 0 && start >= 0) {
|
|
812
|
+
spans.push([start, i + 1]);
|
|
813
|
+
start = -1;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
for (const [from, to] of spans.sort((a, b) => b[1] - b[0] - (a[1] - a[0]))) {
|
|
819
|
+
const parsed = tryParseJson(text.slice(from, to));
|
|
820
|
+
if (parsed !== null) {
|
|
821
|
+
return parsed;
|
|
822
|
+
}
|
|
777
823
|
}
|
|
778
824
|
return null;
|
|
779
825
|
}
|
|
780
826
|
function truncate(text, max = 300) {
|
|
781
827
|
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
782
828
|
}
|
|
829
|
+
function tail(text, max = 300) {
|
|
830
|
+
return text.length > max ? `...${text.slice(-max)}` : text;
|
|
831
|
+
}
|
|
783
832
|
function parseExtraBody(raw, origin) {
|
|
784
833
|
const parsed = parseJsonOrExplain(raw, origin);
|
|
785
834
|
if (!isPlainObject(parsed)) {
|
|
@@ -1429,11 +1478,10 @@ ${JSON_TEMPLATE_INSTRUCTION}`;
|
|
|
1429
1478
|
},
|
|
1430
1479
|
options.settings?.proxy
|
|
1431
1480
|
);
|
|
1481
|
+
const quote = (shown, clip = truncate) => clip(redactSecrets(shown, [apiKey, baseUrl]));
|
|
1432
1482
|
if (!response.ok) {
|
|
1433
1483
|
const body = await response.text();
|
|
1434
|
-
throw new Error(
|
|
1435
|
-
`OpenAI-compatible API error ${response.status}: ${truncate(redactSecrets(body, [apiKey]))}`
|
|
1436
|
-
);
|
|
1484
|
+
throw new Error(`OpenAI-compatible API error ${response.status}: ${quote(body)}`);
|
|
1437
1485
|
}
|
|
1438
1486
|
const payload = await response.json();
|
|
1439
1487
|
const text = payload.choices?.[0]?.message?.content;
|
|
@@ -1442,13 +1490,17 @@ ${JSON_TEMPLATE_INSTRUCTION}`;
|
|
|
1442
1490
|
}
|
|
1443
1491
|
const rawResult = extractJson(text);
|
|
1444
1492
|
if (rawResult === null) {
|
|
1445
|
-
|
|
1493
|
+
const finishReason = payload.choices?.[0]?.finish_reason;
|
|
1494
|
+
const advice = finishReason === "length" ? `The answer was cut off (finish_reason=length). Raise the limit, e.g. modlens config set openai.extraBody '{"max_tokens":4096}'.` : finishReason === void 0 || finishReason === "stop" ? "The answer ended normally but no complete JSON object could be read from it. Ask the gateway to enforce the shape: modlens config set openai.structuredOutput true." : `The gateway ended the answer with finish_reason=${quote(finishReason, (t) => truncate(t, 80))}, so it may be incomplete for a reason of its own. Check what that reason means for this endpoint before changing the request.`;
|
|
1495
|
+
throw new Error(
|
|
1496
|
+
`OpenAI-compatible API returned non-JSON output. ${advice} Output ended with: ${quote(text, tail)}`
|
|
1497
|
+
);
|
|
1446
1498
|
}
|
|
1447
1499
|
const result = normalizeVisionResult(rawResult);
|
|
1448
1500
|
const missing = missingSchemaFields(result);
|
|
1449
1501
|
if (missing.length > 0) {
|
|
1450
1502
|
throw new Error(
|
|
1451
|
-
`OpenAI-compatible API returned JSON that does not match the vision schema (wrong or missing: ${missing.join(", ")}). Retry, or switch to gemini-api / anthropic for enforced schemas. Got: ${
|
|
1503
|
+
`OpenAI-compatible API returned JSON that does not match the vision schema (wrong or missing: ${missing.join(", ")}). Retry, or switch to gemini-api / anthropic for enforced schemas. Got: ${quote(text)}`
|
|
1452
1504
|
);
|
|
1453
1505
|
}
|
|
1454
1506
|
return {
|
|
@@ -3565,11 +3617,11 @@ function readWindowedLines(file, maxBytes = WINDOW_BYTES) {
|
|
|
3565
3617
|
}
|
|
3566
3618
|
const head = Buffer.alloc(maxBytes);
|
|
3567
3619
|
fs.readSync(fd, head, 0, maxBytes, 0);
|
|
3568
|
-
const
|
|
3569
|
-
fs.readSync(fd,
|
|
3620
|
+
const tail2 = Buffer.alloc(maxBytes);
|
|
3621
|
+
fs.readSync(fd, tail2, 0, maxBytes, size - maxBytes);
|
|
3570
3622
|
const headLines = head.toString("utf-8").split("\n");
|
|
3571
3623
|
headLines.pop();
|
|
3572
|
-
const tailLines =
|
|
3624
|
+
const tailLines = tail2.toString("utf-8").split("\n");
|
|
3573
3625
|
tailLines.shift();
|
|
3574
3626
|
return [...headLines, ...tailLines];
|
|
3575
3627
|
} catch {
|
|
@@ -4366,7 +4418,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
4366
4418
|
}
|
|
4367
4419
|
return Number.parseInt(raw, 10);
|
|
4368
4420
|
}
|
|
4369
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.17.
|
|
4421
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.17.2");
|
|
4370
4422
|
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(
|
|
4371
4423
|
"--extra-body <json>",
|
|
4372
4424
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
@@ -4476,7 +4528,7 @@ program.command("doctor").description(
|
|
|
4476
4528
|
configPath: CONFIG_PATH,
|
|
4477
4529
|
// Lets doctor name an installed skill copy that is older than
|
|
4478
4530
|
// the CLI reporting on it (issue #33).
|
|
4479
|
-
version: "3.17.
|
|
4531
|
+
version: "3.17.2"
|
|
4480
4532
|
});
|
|
4481
4533
|
const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
|
|
4482
4534
|
process.stdout.write(`${output}
|
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.17.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.2
|
|
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.
|
|
@@ -81,7 +81,7 @@ modlens ships often, and both install shapes freeze at whatever version they
|
|
|
81
81
|
got. On dsh, re-run the install with the version named:
|
|
82
82
|
|
|
83
83
|
```sh
|
|
84
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.
|
|
84
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.2
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
`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.17.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.17.2
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
@@ -71,7 +71,7 @@ dsh 的网页用户面前没有终端,所以引擎设置有一张卡片,在*
|
|
|
71
71
|
modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.
|
|
74
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.2
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
`npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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.17.
|
|
166
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.2
|
|
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.17.
|
|
181
|
+
- '@liustack/modlens@3.17.2'
|
|
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.17.
|
|
147
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.17.2
|
|
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.17.0
|
|
|
153
153
|
|
|
154
154
|
```yaml
|
|
155
155
|
minimumReleaseAgeExclude:
|
|
156
|
-
- '@liustack/modlens@3.17.
|
|
156
|
+
- '@liustack/modlens@3.17.2'
|
|
157
157
|
```
|
|
158
158
|
|
|
159
159
|
或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
|
package/dsh/index.js
CHANGED
|
@@ -452,6 +452,58 @@ function registerPasteRoute(ctx, host, ownProviders) {
|
|
|
452
452
|
* deepseek-official wrap keeps its historical `deepseek-modlens` id, so a
|
|
453
453
|
* selector remembering that provider survives the upgrade.
|
|
454
454
|
*/
|
|
455
|
+
/**
|
|
456
|
+
* Whether this wrapper id proves, by itself, which upstream produced the
|
|
457
|
+
* turns recorded under it.
|
|
458
|
+
*
|
|
459
|
+
* Auto-discovery mints `modlens-<upstream>` (and `deepseek-modlens` for
|
|
460
|
+
* `deepseek-official`), so the id carries its own provenance and cannot drift.
|
|
461
|
+
* A hand-configured `upstream` under some other id can be repointed between
|
|
462
|
+
* runs, and then history recorded under that id was produced by a provider
|
|
463
|
+
* that is no longer the one behind it. Relabelling there would hand one
|
|
464
|
+
* adapter another adapter's private replay state, which at best fails the
|
|
465
|
+
* request. Unprovable means no relabelling, so those setups keep exactly the
|
|
466
|
+
* behaviour they have today rather than gaining a worse one.
|
|
467
|
+
*/
|
|
468
|
+
function wrapperIdEncodes(wrapperId, upstream) {
|
|
469
|
+
return wrapperId === `modlens-${upstream}` || (wrapperId === 'deepseek-modlens' && upstream === 'deepseek-official')
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Re-label our own turns as the upstream provider's before delegating.
|
|
474
|
+
*
|
|
475
|
+
* dsh drops an assistant message's adapter-private replayState whenever the
|
|
476
|
+
* provider recorded on that message belongs to a different adapter instance
|
|
477
|
+
* than the one about to run (LlmService.forAdapter, an identity comparison).
|
|
478
|
+
* This wrapper is a different instance by construction, so every turn it
|
|
479
|
+
* produced reached upstream stripped of the state that carries reasoning
|
|
480
|
+
* continuity, and the model answered without engaging thinking mode:
|
|
481
|
+
* reasoning blocks went missing and the chain of thought landed inline in the
|
|
482
|
+
* text (issue #49). Nothing about the message content differed, which is why
|
|
483
|
+
* passing messages through unchanged looked correct.
|
|
484
|
+
*
|
|
485
|
+
* Renaming is the truth rather than a trick, but only where the id proves it
|
|
486
|
+
* (see wrapperIdEncodes): these turns are upstream's own work, produced by
|
|
487
|
+
* upstream's adapter, and the replayState in them is upstream's to read. Only
|
|
488
|
+
* the copy going over the wire is relabelled. The durable session log keeps
|
|
489
|
+
* the wrapper id, so the UI and the model selector still show the chosen route.
|
|
490
|
+
*/
|
|
491
|
+
function restoreUpstreamSource(messages, wrapperId, upstream) {
|
|
492
|
+
if (!wrapperIdEncodes(wrapperId, upstream)) {
|
|
493
|
+
return messages
|
|
494
|
+
}
|
|
495
|
+
let changed = false
|
|
496
|
+
const out = messages.map((message) => {
|
|
497
|
+
const source = message?.source
|
|
498
|
+
if (message?.role !== 'assistant' || source?.kind !== 'model' || source.provider !== wrapperId) {
|
|
499
|
+
return message
|
|
500
|
+
}
|
|
501
|
+
changed = true
|
|
502
|
+
return { ...message, source: { ...source, provider: upstream } }
|
|
503
|
+
})
|
|
504
|
+
return changed ? out : messages
|
|
505
|
+
}
|
|
506
|
+
|
|
455
507
|
function registerVisionProvider(ctx, config, ownProviders) {
|
|
456
508
|
// Wrap only the text-only members of these families. Their own vision
|
|
457
509
|
// models (present or future: deepseek-vl/ocr/janus, glm-4.5v, glm-5v-...)
|
|
@@ -512,7 +564,8 @@ function registerVisionProvider(ctx, config, ownProviders) {
|
|
|
512
564
|
// Cached per attachment, since the same history rides every step.
|
|
513
565
|
const self = this
|
|
514
566
|
return (async function* () {
|
|
515
|
-
const
|
|
567
|
+
const converted = await convertImagesToEvidence(ctx, options.messages, options.signal, self)
|
|
568
|
+
const messages = restoreUpstreamSource(converted, providerId, upstream)
|
|
516
569
|
yield* ctx.llm.stream({ ...options, provider: upstream, messages })
|
|
517
570
|
})()
|
|
518
571
|
},
|
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.17.
|
|
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.17.2):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.17.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.17.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.17.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.17.2: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.17.2 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.17.2 <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.
|
|
@@ -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.17.
|
|
27
|
+
$Pinned = '3.17.2'
|
|
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.17.
|
|
25
|
+
PINNED="3.17.2"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|