@liustack/modlens 3.16.5 → 3.16.6
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 +5 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/dist/main.js +101 -11
- package/docs/cli.md +1 -1
- package/docs/cli.zh-CN.md +1 -1
- package/docs/harness-setup.md +2 -2
- package/docs/harness-setup.zh-CN.md +2 -2
- package/docs/output-schema.md +2 -0
- package/docs/output-schema.zh-CN.md +2 -0
- package/docs/troubleshooting.md +30 -4
- package/docs/troubleshooting.zh-CN.md +20 -4
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +4 -4
- package/skills/modlens/references/configure.md +17 -6
- package/skills/modlens/references/configure.zh-CN.md +17 -6
- 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,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.16.6 - 2026-08-15
|
|
4
|
+
|
|
5
|
+
- **A `null` where the contract asks for nothing no longer fails the read ([#37](https://github.com/liustack/modlens/issues/37)).** The reported failure named `visual.notes`, which the validator only reports when the field is present in a shape it does not accept: leaving it out was always fine. `null` is the shape a model reaches for when it has nothing to say, and it is what the reporter's own workaround had to legalize, so that is what this fixes. An optional field holding `null` is now dropped before the check rather than passing through it, which keeps the read alive and keeps `null` out of the fields this contract declares: each one is absent or holds its declared type, as the schema always promised. A key a gateway adds on its own is dropped the same way when it is null. On a required field `null` is still a violation. The error also stopped calling everything missing, since reading `missing: visual.notes` about a field that was right there sends you looking in the wrong place.
|
|
6
|
+
- **The openai provider can ask the gateway to enforce the contract ([#37](https://github.com/liustack/modlens/issues/37)).** `modlens config set openai.structuredOutput true` sends it as `response_format: json_schema` in the strict form those endpoints require: every property required, `additionalProperties: false`, and the ones this contract leaves optional made nullable. It is derived from the same schema the runtime checks against, so there is no second copy to keep in step, which is the part that made the reporter's own workaround expensive: they had to hand-write the whole thing to get thinking-disabled qwen through. Off by default, since a gateway without structured-output support answers 400 for the field, and a `response_format` set in `extraBody` still wins.
|
|
7
|
+
|
|
3
8
|
## 3.16.5 - 2026-08-15
|
|
4
9
|
|
|
5
10
|
- **dsh: pasting into a plain text-only model works again ([#36](https://github.com/liustack/modlens/issues/36)).** The paste takeover shipped in 3.14.0 has been dead in every default install since 3.16.0 moved the verdict server-side. The verdict refuses when any model matching the selector label declares image input, which is right for a real vision model and wrong for the one case it could not see: this plugin's own `(modlens vision)` wrapper reuses the upstream model id verbatim and declares image input, because that declaration is exactly how the wrapper unlocks admission. So selecting plain `DeepSeek-V4-Pro` matched the real text-only model and the plugin's synthetic twin of it, the twin vetoed, and the paste fell through to dsh's own gate and its `MODEL_DOES_NOT_SUPPORT_IMAGES`. The verdict now skips a twin two ways, both requiring proof rather than a name: the provider ids this instance actually registered, tracked as each wrapper lands, and a model that carries the `(modlens vision)` marker on a provider id minted by the rule this plugin uses, which is how a sibling instance in the same process is recognized. A real vision provider still vetoes even if it borrows the marker, and an id someone else already holds is never trusted as ours. The verdict had no test at all, which is how a regression this total shipped and stayed for six releases; it now has an integration suite driving the real route against a registry shaped like a live install. Thanks to @Taz-dingo for a report that arrived with the conflicting rules already quoted side by side.
|
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.16.
|
|
37
|
+
**🥇 The first vision plugin for DeepSeek Harness (dsh):** one command, `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6`, 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
|
|
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.16.
|
|
37
|
+
**🥇 全网第一个支持 DeepSeek Harness(dsh)的外挂视觉识别插件:**一条命令 `npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6`,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.16.
|
|
72
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
装完即有 `modlens_read_image` 工具,选「(modlens vision)」模型变体即可直接粘贴识图。引擎配置同样在 `~/.modlens`,详见[宿主接入](docs/harness-setup.zh-CN.md)。
|
package/dist/main.js
CHANGED
|
@@ -619,12 +619,71 @@ const VISION_RESULT_SCHEMA = {
|
|
|
619
619
|
},
|
|
620
620
|
required: ["summary", "ocr", "layout", "semantics", "visual", "uncertainty"]
|
|
621
621
|
};
|
|
622
|
+
function strictSchema(node) {
|
|
623
|
+
if (node.type === "object") {
|
|
624
|
+
const properties = {};
|
|
625
|
+
const required = node.required ?? [];
|
|
626
|
+
for (const [key, child] of Object.entries(node.properties ?? {})) {
|
|
627
|
+
const strict = strictSchema(child);
|
|
628
|
+
properties[key] = required.includes(key) ? strict : (
|
|
629
|
+
// Strict mode has no optional properties, only nullable ones.
|
|
630
|
+
{ anyOf: [strict, { type: "null" }] }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
type: "object",
|
|
635
|
+
properties,
|
|
636
|
+
required: Object.keys(properties),
|
|
637
|
+
additionalProperties: false
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if (node.type === "array" && node.items) {
|
|
641
|
+
return { ...node, items: strictSchema(node.items) };
|
|
642
|
+
}
|
|
643
|
+
return node;
|
|
644
|
+
}
|
|
645
|
+
function visionResponseFormat() {
|
|
646
|
+
return {
|
|
647
|
+
type: "json_schema",
|
|
648
|
+
json_schema: {
|
|
649
|
+
name: "vision_result",
|
|
650
|
+
strict: true,
|
|
651
|
+
schema: strictSchema(VISION_RESULT_SCHEMA)
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
622
655
|
function visionResultSchemaJson() {
|
|
623
656
|
return JSON.stringify(VISION_RESULT_SCHEMA);
|
|
624
657
|
}
|
|
625
658
|
function missingSchemaFields(result) {
|
|
626
659
|
return schemaViolations(VISION_RESULT_SCHEMA, result, "");
|
|
627
660
|
}
|
|
661
|
+
function withoutEmptyOptionals(value, schema) {
|
|
662
|
+
if (schema.type === "object") {
|
|
663
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
const record = value;
|
|
667
|
+
const cleaned = {};
|
|
668
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
669
|
+
const childSchema = schema.properties?.[key];
|
|
670
|
+
const isRequired = schema.required?.includes(key) ?? false;
|
|
671
|
+
if (entry === null && !isRequired) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
cleaned[key] = childSchema ? withoutEmptyOptionals(entry, childSchema) : entry;
|
|
675
|
+
}
|
|
676
|
+
return cleaned;
|
|
677
|
+
}
|
|
678
|
+
if (schema.type === "array" && schema.items && Array.isArray(value)) {
|
|
679
|
+
const itemSchema = schema.items;
|
|
680
|
+
return value.map((item) => withoutEmptyOptionals(item, itemSchema));
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
function normalizeVisionResult(result) {
|
|
685
|
+
return withoutEmptyOptionals(result, VISION_RESULT_SCHEMA);
|
|
686
|
+
}
|
|
628
687
|
function schemaViolations(schema, value, path2) {
|
|
629
688
|
const label = path2 || "(root)";
|
|
630
689
|
if (schema.type === "object") {
|
|
@@ -1256,6 +1315,13 @@ ${JSON_TEMPLATE_INSTRUCTION}`;
|
|
|
1256
1315
|
mergeExtraBody(
|
|
1257
1316
|
{
|
|
1258
1317
|
model,
|
|
1318
|
+
// Asked for, never assumed: a gateway without
|
|
1319
|
+
// structured-output support answers 400 for a field
|
|
1320
|
+
// it does not know (issue #37). A response_format the
|
|
1321
|
+
// caller supplied wins outright rather than being
|
|
1322
|
+
// merged into ours, since the two describe the same
|
|
1323
|
+
// thing and a blend of them describes neither.
|
|
1324
|
+
...options.settings?.structuredOutput && options.settings?.extraBody?.response_format === void 0 ? { response_format: visionResponseFormat() } : {},
|
|
1259
1325
|
messages: [
|
|
1260
1326
|
{
|
|
1261
1327
|
role: "user",
|
|
@@ -1286,14 +1352,15 @@ ${JSON_TEMPLATE_INSTRUCTION}`;
|
|
|
1286
1352
|
if (!text) {
|
|
1287
1353
|
throw new Error("OpenAI-compatible API returned no message content.");
|
|
1288
1354
|
}
|
|
1289
|
-
const
|
|
1290
|
-
if (
|
|
1355
|
+
const rawResult = extractJson(text);
|
|
1356
|
+
if (rawResult === null) {
|
|
1291
1357
|
throw new Error(`OpenAI-compatible API returned non-JSON output: ${truncate(text)}`);
|
|
1292
1358
|
}
|
|
1359
|
+
const result = normalizeVisionResult(rawResult);
|
|
1293
1360
|
const missing = missingSchemaFields(result);
|
|
1294
1361
|
if (missing.length > 0) {
|
|
1295
1362
|
throw new Error(
|
|
1296
|
-
`OpenAI-compatible API returned JSON that does not match the vision schema (missing: ${missing.join(", ")}). Retry, or switch to gemini-api / anthropic for enforced schemas. Got: ${truncate(text)}`
|
|
1363
|
+
`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: ${truncate(text)}`
|
|
1297
1364
|
);
|
|
1298
1365
|
}
|
|
1299
1366
|
return {
|
|
@@ -1432,12 +1499,31 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
1432
1499
|
const dot = dottedKey.indexOf(".");
|
|
1433
1500
|
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
1434
1501
|
throw new Error(
|
|
1435
|
-
`Invalid config key: ${dottedKey}. Use "provider", "reuse.<claude|codex|opencode|pi|grok>", "guards.<denyModels|allowModels|denyWhenUnknown>", or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
1502
|
+
`Invalid config key: ${dottedKey}. Use "provider", "proxy", "reuse.<claude|codex|opencode|pi|grok>", "guards.<denyModels|allowModels|denyWhenUnknown>", or "<provider>.<apiKey|baseUrl|model|proxy|extraBody|structuredOutput>".`
|
|
1436
1503
|
);
|
|
1437
1504
|
}
|
|
1438
1505
|
const providerName = dottedKey.slice(0, dot);
|
|
1439
1506
|
const field = dottedKey.slice(dot + 1);
|
|
1440
|
-
if (field === "
|
|
1507
|
+
if (field === "structuredOutput") {
|
|
1508
|
+
if ((providerAliases()[providerName] ?? providerName) !== "openai") {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`structuredOutput applies to the openai provider only, not ${providerName}.`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
const normalized = value.trim().toLowerCase();
|
|
1514
|
+
if (normalized !== "" && normalized !== "true" && normalized !== "false") {
|
|
1515
|
+
throw new Error(
|
|
1516
|
+
`${providerName}.structuredOutput must be true or false (empty clears).`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
config2.providers ??= {};
|
|
1520
|
+
config2.providers[providerName] ??= {};
|
|
1521
|
+
if (normalized === "") {
|
|
1522
|
+
delete config2.providers[providerName].structuredOutput;
|
|
1523
|
+
} else {
|
|
1524
|
+
config2.providers[providerName].structuredOutput = normalized === "true";
|
|
1525
|
+
}
|
|
1526
|
+
} else if (field === "extraBody") {
|
|
1441
1527
|
config2.providers ??= {};
|
|
1442
1528
|
config2.providers[providerName] ??= {};
|
|
1443
1529
|
if (value.trim() === "") {
|
|
@@ -1450,7 +1536,7 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
1450
1536
|
}
|
|
1451
1537
|
} else if (!STRING_FIELDS.includes(field)) {
|
|
1452
1538
|
throw new Error(
|
|
1453
|
-
`Unknown config field: ${field}. Use apiKey, baseUrl, model, proxy, or
|
|
1539
|
+
`Unknown config field: ${field}. Use apiKey, baseUrl, model, proxy, extraBody, or structuredOutput.`
|
|
1454
1540
|
);
|
|
1455
1541
|
} else {
|
|
1456
1542
|
config2.providers ??= {};
|
|
@@ -1539,6 +1625,9 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
1539
1625
|
fields[field] = `${shown} (${source})`;
|
|
1540
1626
|
}
|
|
1541
1627
|
}
|
|
1628
|
+
if (fileSettings.structuredOutput !== void 0) {
|
|
1629
|
+
fields.structuredOutput = `${fileSettings.structuredOutput} (file)`;
|
|
1630
|
+
}
|
|
1542
1631
|
if (fileSettings.extraBody !== void 0) {
|
|
1543
1632
|
fields.extraBody = `${JSON.stringify(fileSettings.extraBody)} (file)`;
|
|
1544
1633
|
}
|
|
@@ -2770,10 +2859,11 @@ async function runProvider(provider, model, options, resolvedInput, timeoutMs, c
|
|
|
2770
2859
|
`Provider ${provider.name} implements neither execute nor buildInvocation.`
|
|
2771
2860
|
);
|
|
2772
2861
|
}
|
|
2862
|
+
parsed.result = normalizeVisionResult(parsed.result);
|
|
2773
2863
|
const missing = missingSchemaFields(parsed.result);
|
|
2774
2864
|
if (missing.length > 0) {
|
|
2775
2865
|
throw new Error(
|
|
2776
|
-
`${provider.name} returned a result that does not match the vision schema (missing: ${missing.join(", ")}).`
|
|
2866
|
+
`${provider.name} returned a result that does not match the vision schema (wrong or missing: ${missing.join(", ")}).`
|
|
2777
2867
|
);
|
|
2778
2868
|
}
|
|
2779
2869
|
return parsed;
|
|
@@ -4069,7 +4159,7 @@ function parsePositiveInt(raw, flag) {
|
|
|
4069
4159
|
}
|
|
4070
4160
|
return Number.parseInt(raw, 10);
|
|
4071
4161
|
}
|
|
4072
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.
|
|
4162
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.16.6");
|
|
4073
4163
|
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(
|
|
4074
4164
|
"--extra-body <json>",
|
|
4075
4165
|
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
@@ -4179,7 +4269,7 @@ program.command("doctor").description(
|
|
|
4179
4269
|
configPath: CONFIG_PATH,
|
|
4180
4270
|
// Lets doctor name an installed skill copy that is older than
|
|
4181
4271
|
// the CLI reporting on it (issue #33).
|
|
4182
|
-
version: "3.16.
|
|
4272
|
+
version: "3.16.6"
|
|
4183
4273
|
});
|
|
4184
4274
|
const output = options.json ? JSON.stringify(report, null, 2) : renderDoctorReport(report);
|
|
4185
4275
|
process.stdout.write(`${output}
|
|
@@ -4199,9 +4289,9 @@ config.command("init").description(`Create a starter config at ${CONFIG_PATH}`).
|
|
|
4199
4289
|
process.stdout.write(
|
|
4200
4290
|
[
|
|
4201
4291
|
`Created ${CONFIG_PATH}`,
|
|
4202
|
-
"Everything is optional.
|
|
4292
|
+
"Everything is optional. The usual ones:",
|
|
4203
4293
|
" modlens config set provider <name> which provider analyzes images",
|
|
4204
|
-
" modlens config set <provider>.<apiKey|baseUrl|model> <value> provider
|
|
4294
|
+
" modlens config set <provider>.<apiKey|baseUrl|model> <value> provider settings",
|
|
4205
4295
|
` modlens config set <provider>.extraBody '{"thinking":{"type":"disabled"}}' vendor request fields`,
|
|
4206
4296
|
""
|
|
4207
4297
|
].join("\n")
|
package/docs/cli.md
CHANGED
|
@@ -97,6 +97,6 @@ Five providers: `antigravity-cli` (no key), `gemini-api` (fastest free route), `
|
|
|
97
97
|
Other subcommands:
|
|
98
98
|
|
|
99
99
|
- `modlens guard [--model <id>]`: should the engine run for the active model at all? Exit 0 allow, 1 deny, verdict as JSON.
|
|
100
|
-
- `modlens config <init|set|show>`: 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
|
|
100
|
+
- `modlens config <init|set|show>`: 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).
|
|
101
101
|
- `modlens doctor`: Node and node:sqlite, provider readiness, the failover chains for this machine, 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.
|
|
102
102
|
|
package/docs/cli.zh-CN.md
CHANGED
|
@@ -94,5 +94,5 @@ modlens recover-paste # pull a pasted image into a fil
|
|
|
94
94
|
其他子命令:
|
|
95
95
|
|
|
96
96
|
- `modlens guard [--model <id>]`:判断当前激活的模型到底该不该运行引擎。退出码 0 表示放行,1 表示拒绝,判定结果以 JSON 输出。
|
|
97
|
-
- `modlens config <init|set|show>`:可用的键有 `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
|
|
97
|
+
- `modlens config <init|set|show>`:可用的键有 `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`(仅这条路线用得上)。
|
|
98
98
|
- `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.16.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6
|
|
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.
|
|
@@ -66,7 +66,7 @@ modlens ships often, and both install shapes freeze at whatever version they
|
|
|
66
66
|
got. On dsh, re-run the install with the version named:
|
|
67
67
|
|
|
68
68
|
```sh
|
|
69
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.
|
|
69
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.6
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
`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.16.
|
|
58
|
+
npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.6
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
这会注册一个 `modlens_read_image` 工具,它的 schema 随每次请求抵达模型(不靠触发启发式),运行同一个包里自带的 modlens CLI,并把结构化证据作为工具的标准 JSON 输出返回。引擎、复用授权和 guard 规则仍在 `~/.modlens/config.json` 里,与其他所有 harness 共享。dsh 还在开发者预览阶段,插件接口可能变化。这个插件刻意保持很小的接触面(原生工具注册、视觉变体所用的 llm 适配层、附件读取器,以及一个 agent 执行前钩子),其中任何一处变动,它都会大声报错而不是无声退化。
|
|
@@ -65,7 +65,7 @@ npx -y @deepseek-ai/dsh plugin --profile web add @liustack/modlens@3.16.5
|
|
|
65
65
|
modlens 发布很频繁,而两种安装形态都会冻结在装进来的那个版本上。dsh 上重跑一遍安装即可,版本号要点名:
|
|
66
66
|
|
|
67
67
|
```sh
|
|
68
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.
|
|
68
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.6
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
`npm view @liustack/modlens version` 可以查到当前版本号,本页的版本号则由发布流程自动写入。
|
package/docs/output-schema.md
CHANGED
|
@@ -71,6 +71,8 @@ The CLI prints one JSON object to stdout:
|
|
|
71
71
|
|
|
72
72
|
Required fields: `summary`, `ocr`, `layout`, `semantics`, `visual`, `uncertainty` — every top-level field, `visual` included. (Earlier docs called `visual` optional; the enforced schema has always required it, so build to the schema.)
|
|
73
73
|
|
|
74
|
+
Optional fields: `ocr.lines[].language`, `semantics.intent`, `semantics.entities[].evidence`, `semantics.relations`, `visual.dominant_colors`, `visual.style`, `visual.notes`. Each is either absent or holds its declared type. Never `null`: a model with nothing to say there often writes one, and modlens drops the key before the result reaches you, so reading an optional field means checking whether it is there, not whether it is null.
|
|
75
|
+
|
|
74
76
|
`layout.regions[].type` is a free string, not a closed list. Region kinds are an open set: a fixed enum rejected `link` on any web screenshot and `search` on a portal, and a rejected result fails the whole read over a descriptive label. The field's schema `description` names the common vocabulary as guidance, which reaches every provider that enforces this schema server-side, so an unlisted kind costs nothing.
|
|
75
77
|
|
|
76
78
|
Changes from v1: pixel `bbox` coordinates and numeric `confidence` scores were removed. Vision models fabricate both, so v2 stops pretending to provide them.
|
|
@@ -71,6 +71,8 @@ CLI 向 stdout 打印一个 JSON 对象:
|
|
|
71
71
|
|
|
72
72
|
必填字段:`summary`、`ocr`、`layout`、`semantics`、`visual`、`uncertainty`,也就是每一个顶层字段,`visual` 也不例外。(早期文档把 `visual` 写成可选,但强制执行的 schema 一直要求它,请以 schema 为准。)
|
|
73
73
|
|
|
74
|
+
可选字段:`ocr.lines[].language`、`semantics.intent`、`semantics.entities[].evidence`、`semantics.relations`、`visual.dominant_colors`、`visual.style`、`visual.notes`。每一个要么不存在,要么就是它声明的类型,绝不会是 `null`:模型在这些位置没话可说时经常写 `null`,modlens 会在结果交到你手上之前把这个键删掉,所以读可选字段只需判断它在不在,不用判断是不是 null。
|
|
75
|
+
|
|
74
76
|
`layout.regions[].type` 是自由字符串,不是封闭列表。区域类型本质是开放集合:固定枚举会让任何网页截图里的 `link`、门户页里的 `search` 直接落选,而一次落选就为了一个描述性标签废掉整次识别。常用词表写在该字段的 schema `description` 里作为指引,凡是在服务端强制执行这份 schema 的 provider 都会收到,没列到的类型不会有任何代价。
|
|
75
77
|
|
|
76
78
|
相对 v1 的变化:删掉了像素级 `bbox` 坐标和数值型 `confidence` 分数。视觉模型会凭空编造这两样,v2 不再假装提供。
|
package/docs/troubleshooting.md
CHANGED
|
@@ -98,10 +98,36 @@ Working as intended. Codex writes pasted images to disk and puts the path in the
|
|
|
98
98
|
|
|
99
99
|
```
|
|
100
100
|
OpenAI-compatible API returned JSON that does not match the vision schema
|
|
101
|
-
(missing:
|
|
101
|
+
(wrong or missing: visual.notes, ...)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
That endpoint returned
|
|
104
|
+
That endpoint returned something the contract does not accept. Note the
|
|
105
|
+
wording: a field named here can be absent, or present with the wrong shape.
|
|
106
|
+
For an optional field like `visual.notes`, only the second is possible, since
|
|
107
|
+
leaving it out is accepted. A `null` there is dropped rather than refused, so
|
|
108
|
+
what remains is a genuinely wrong type.
|
|
109
|
+
|
|
110
|
+
Most OpenAI-compatible gateways enforce nothing server-side, so the contract
|
|
111
|
+
travels as a filled-in JSON template in the prompt and a weaker model can
|
|
112
|
+
answer with half of it, especially with thinking turned off. Ask the gateway
|
|
113
|
+
to enforce it instead:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
modlens config set openai.structuredOutput true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
That sends the contract as `response_format: json_schema` in strict form,
|
|
120
|
+
derived from the same schema modlens checks against, so there is nothing to
|
|
121
|
+
keep in sync by hand. It is off by default because a gateway that does not
|
|
122
|
+
support the field answers 400 for it. If that happens:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
modlens config set openai.structuredOutput false
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A `response_format` you set yourself in `extraBody` wins over the derived one.
|
|
129
|
+
|
|
130
|
+
Failing that, retry once, then switch:
|
|
105
131
|
|
|
106
132
|
```bash
|
|
107
133
|
modlens -i <image> -p gemini-api
|
|
@@ -137,7 +163,7 @@ simply lands on an older one. Name the exact version instead, which pnpm treats
|
|
|
137
163
|
as a deliberate request rather than a resolution:
|
|
138
164
|
|
|
139
165
|
```sh
|
|
140
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.
|
|
166
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.6
|
|
141
167
|
```
|
|
142
168
|
|
|
143
169
|
`npm view @liustack/modlens version` prints the current one. pnpm 11 installs a named
|
|
@@ -152,7 +178,7 @@ file:
|
|
|
152
178
|
|
|
153
179
|
```yaml
|
|
154
180
|
minimumReleaseAgeExclude:
|
|
155
|
-
- '@liustack/modlens@3.16.
|
|
181
|
+
- '@liustack/modlens@3.16.6'
|
|
156
182
|
```
|
|
157
183
|
|
|
158
184
|
Or lift the gate for a single command, which lifts it for everything that
|
|
@@ -98,10 +98,26 @@ tag in the message carries its path.
|
|
|
98
98
|
|
|
99
99
|
```
|
|
100
100
|
OpenAI-compatible API returned JSON that does not match the vision schema
|
|
101
|
-
(missing:
|
|
101
|
+
(wrong or missing: visual.notes, ...)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
那个端点返回了不符合契约的内容。注意措辞:被点名的字段可能是缺失,也可能是存在但形状不对。像 `visual.notes` 这样的可选字段只可能是后者,因为它缺失是被接受的。写成 `null` 也会被丢弃而不是拒绝,所以剩下的就是真正的类型错误。
|
|
105
|
+
|
|
106
|
+
大多数 OpenAI 兼容网关在服务端什么都不强制,契约是以填好的 JSON 模板形式随提示词发过去的,能力弱一些的模型可能只答出一半,关掉思考时尤其明显。可以改成让网关自己强制执行:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
modlens config set openai.structuredOutput true
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
这会把契约以 `response_format: json_schema` 的严格形式发过去,schema 由 modlens 校验用的那份推导而来,没有需要手工同步的副本。默认关闭,因为不支持这个字段的网关会直接 400。真遇到就关回去:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
modlens config set openai.structuredOutput false
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
你自己在 `extraBody` 里设的 `response_format` 优先级更高。
|
|
119
|
+
|
|
120
|
+
还是不行就重试一次,然后换 provider:
|
|
105
121
|
|
|
106
122
|
```bash
|
|
107
123
|
modlens -i <image> -p gemini-api
|
|
@@ -128,7 +144,7 @@ dsh profile 装到的是旧版 modlens。`dsh.bundle` 声明从 3.9.0 起才存
|
|
|
128
144
|
`@latest` 绕不开这一层,本页早先的说法是错的。冷静期先把候选版本过滤掉,dist-tag 才在剩下的里面解析,于是它直接落到了更旧的那个上。改成写死精确版本号,pnpm 会把它当作一次明确的指定,而不是一次解析:
|
|
129
145
|
|
|
130
146
|
```sh
|
|
131
|
-
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.
|
|
147
|
+
npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.6
|
|
132
148
|
```
|
|
133
149
|
|
|
134
150
|
`npm view @liustack/modlens version` 可以查到当前版本号。pnpm 11 会装上被点名的版本,11.1.3 起还会把它作为一条已批准的例外写进该 profile 的 `pnpm-workspace.yaml`,其余所有包和 modlens 以后的版本仍然留在窗口后面。
|
|
@@ -137,7 +153,7 @@ npx -y @deepseek-ai/dsh plugin --profile <name> add @liustack/modlens@3.16.5
|
|
|
137
153
|
|
|
138
154
|
```yaml
|
|
139
155
|
minimumReleaseAgeExclude:
|
|
140
|
-
- '@liustack/modlens@3.16.
|
|
156
|
+
- '@liustack/modlens@3.16.6'
|
|
141
157
|
```
|
|
142
158
|
|
|
143
159
|
或者只为这一条命令解除冷静期,注意它解除的是这条命令解析到的所有包,不只 modlens:
|
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.16.
|
|
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.16.6):
|
|
24
24
|
|
|
25
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.
|
|
26
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.
|
|
27
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.
|
|
25
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.16.6: `modlens <args>`.
|
|
26
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.16.6 modlens <args>`.
|
|
27
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.16.6 <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.
|
|
@@ -12,14 +12,14 @@ Read this when the user asks how to set up, configure, or switch ModLens provide
|
|
|
12
12
|
modlens config init # write a starter config (refuses to overwrite; --force to redo)
|
|
13
13
|
modlens config show # effective file, API keys masked
|
|
14
14
|
modlens config set provider <name> # change the default provider
|
|
15
|
-
modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model, extraBody
|
|
15
|
+
modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model, proxy, extraBody, structuredOutput
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
`config set` writes the file with 0600 permissions.
|
|
19
19
|
|
|
20
20
|
## The file's exact shape
|
|
21
21
|
|
|
22
|
-
Everything lives under
|
|
22
|
+
Everything lives under five 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
|
{
|
|
@@ -42,7 +42,9 @@ Everything lives under four top-level keys, all optional. This example shows eve
|
|
|
42
42
|
"apiKey": "sk-...",
|
|
43
43
|
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
44
44
|
"model": "qwen3.6-27b",
|
|
45
|
-
"
|
|
45
|
+
"proxy": "http://127.0.0.1:7890",
|
|
46
|
+
"extraBody": { "thinking": { "type": "disabled" } },
|
|
47
|
+
"structuredOutput": true
|
|
46
48
|
},
|
|
47
49
|
"anthropic": {
|
|
48
50
|
"apiKey": "sk-ant-...",
|
|
@@ -57,8 +59,9 @@ Everything lives under four top-level keys, all optional. This example shows eve
|
|
|
57
59
|
Field semantics:
|
|
58
60
|
|
|
59
61
|
- `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`, `claude-code` for `claude-cli`). Empty or absent pins nothing: the failover chain decides, trying configured API providers before the agent CLIs.
|
|
60
|
-
- `providers.<name>.<field>`:
|
|
61
|
-
- `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
|
|
62
|
+
- `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.
|
|
63
|
+
- `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 two CLI providers take no request body, so a run on `antigravity-cli` or `claude-cli` ignores it and says so in `meta.warnings`.
|
|
64
|
+
- `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.
|
|
62
65
|
- `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:
|
|
63
66
|
- `denyModels` alone: everything runs the engine except the listed vision models. Right when text-only models are the majority of what you plug in.
|
|
64
67
|
- `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.
|
|
@@ -105,7 +108,15 @@ modlens config set openai.apiKey <sk-key>
|
|
|
105
108
|
modlens config set openai.model qwen3.6-27b
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
For official OpenAI: baseUrl `https://api.openai.com/v1`, a vision-capable model. Environment equivalents: `OPENAI_BASE_URL`, `OPENAI_API_KEY`. The model must be multimodal; text-only models will fail or hallucinate.
|
|
111
|
+
For official OpenAI: baseUrl `https://api.openai.com/v1`, a vision-capable model. Environment equivalents: `OPENAI_BASE_URL`, `OPENAI_API_KEY`. The model must be multimodal; text-only models will fail or hallucinate.
|
|
112
|
+
|
|
113
|
+
This route enforces nothing server-side by default, so a weaker model can answer with half the contract and the run fails with an explicit error. If that happens, ask the gateway to enforce it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
modlens config set openai.structuredOutput true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The contract goes out as `response_format: json_schema` in strict form, derived from the schema modlens checks against. Off by default because a gateway without structured-output support answers 400 for the field, so turn it back off if the endpoint refuses it. Turning thinking off (below) makes the shape failures more likely, so the two often go together.
|
|
109
120
|
|
|
110
121
|
### anthropic (Claude API key)
|
|
111
122
|
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
modlens config init # 写入一份起步配置(已存在则拒绝,--force 重写)
|
|
13
13
|
modlens config show # 生效的配置文件,API key 打码显示
|
|
14
14
|
modlens config set provider <name> # 更改默认 provider
|
|
15
|
-
modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、model、extraBody
|
|
15
|
+
modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、model、proxy、extraBody、structuredOutput
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
`config set` 写文件时权限为 0600。
|
|
19
19
|
|
|
20
20
|
## 配置文件的完整形状
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
所有内容都在五个顶层键之下,全部可选。下面的示例一次性展示了所有支持的键和字段(真实文件只需要写你用到的部分)。文件不存在就全用默认值。provider 的设置放在 `providers.<name>` 下面,不在顶层,手工编辑最常犯的就是这个错。
|
|
23
23
|
|
|
24
24
|
```json
|
|
25
25
|
{
|
|
@@ -42,7 +42,9 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
|
|
|
42
42
|
"apiKey": "sk-...",
|
|
43
43
|
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
44
44
|
"model": "qwen3.6-27b",
|
|
45
|
-
"
|
|
45
|
+
"proxy": "http://127.0.0.1:7890",
|
|
46
|
+
"extraBody": { "thinking": { "type": "disabled" } },
|
|
47
|
+
"structuredOutput": true
|
|
46
48
|
},
|
|
47
49
|
"anthropic": {
|
|
48
50
|
"apiKey": "sk-ant-...",
|
|
@@ -57,8 +59,9 @@ modlens config set <provider>.<field> <value> # 字段:apiKey、baseUrl、mo
|
|
|
57
59
|
字段含义:
|
|
58
60
|
|
|
59
61
|
- `provider`:不传 `-p` 时由哪个 provider 执行。标准名和别名都行(`agy`/`antigravity` 对应 `antigravity-cli`,`gemini` 对应 `gemini-api`,`openai-compat` 对应 `openai`,`claude` 对应 `anthropic`,`claude-code` 对应 `claude-cli`)。留空或缺失表示不钉任何一个:由失败切换链决定,已配置的 API provider 先于 agent CLI 被尝试。
|
|
60
|
-
- `providers.<name>.<field
|
|
61
|
-
- `providers.<name>.extraBody`:一个 JSON 对象,合并进 API provider(`gemini-api`、`openai`、`anthropic`)的请求体,用来传厂商有而 modlens
|
|
62
|
+
- `providers.<name>.<field>`:共六个字段,`apiKey`、`baseUrl`、`model`、`proxy`、`extraBody`、`structuredOutput`(仅 openai 路线)。每个 provider 条目都可选,条目里的每个字段也都可选。别名键同样会被读取(存在 `gemini` 下的设置在解析到 `gemini-api` 时也能找到),冲突时标准键胜出。
|
|
63
|
+
- `providers.<name>.extraBody`:一个 JSON 对象,合并进 API provider(`gemini-api`、`openai`、`anthropic`)的请求体,用来传厂商有而 modlens 没有对应参数的开关。最常见的用途是关掉思考,见下文小节。嵌套对象逐键合并,所以加一个开关不会动到该块里的其他内容。承载图片、提示词和各路线自身强制机制的字段会被拒绝,报错会点名该字段。`openai` 路线上的 `response_format` 不在此列:在那里设置它就是有意替换掉 modlens 本来会发的那份 schema。两个 CLI provider 不发请求体,所以在 `antigravity-cli` 或 `claude-cli` 上运行时它会被忽略,并在 `meta.warnings` 里说明。
|
|
64
|
+
- `providers.openai.structuredOutput`:设为 `true` 时,让 OpenAI 兼容网关自己强制执行视觉契约,以 `response_format: json_schema` 的严格形式发出。默认关闭,因为不支持结构化输出的网关会对这个字段返回 400。你在 `extraBody` 里设的 `response_format` 优先级更高。
|
|
62
65
|
- `guards`:调用 guard,给在同一个客户端里既跑纯文本模型又跑视觉模型的人用。两个列表都放 glob 模式(支持 `*` 和 `?`,不区分大小写,同时匹配模型名和 `provider/model`),用 `modlens config set guards.denyModels '["gemini-3*"]'` 或 `guards.allowModels` 设置(JSON 数组或逗号分隔的列表都行,传空则清除)。两种写法表达同一个意图,选列表更短的那种:
|
|
63
66
|
- 只用 `denyModels`:除了列出的视觉模型,其余全部运行引擎。适合你接入的模型大多是纯文本的情况。
|
|
64
67
|
- `allowModels` 非空(白名单模式):只有列出的模型运行引擎,其他所有已识别的模型一律拒绝。适合 2026 年的实际格局,纯文本模型才是那份短名单。deny 模式仍然优先于 allow 匹配,所以宽泛的 allow 可以把视觉变体剔出去,正如上面的示例:`glm-5.*` 放行文本系列,`glm-*v*` 抓住 `glm-5v-turbo`。allow 模式要锚定得紧一些(写 `deepseek-v4-*` 而不是 `deepseek*`),这样厂商下一代多模态型号会自动掉出名单,等你检查过再上场。
|
|
@@ -105,7 +108,15 @@ modlens config set openai.apiKey <sk-key>
|
|
|
105
108
|
modlens config set openai.model qwen3.6-27b
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
官方 OpenAI 的写法:baseUrl 用 `https://api.openai.com/v1`,配一个具备视觉能力的模型。对应的环境变量:`OPENAI_BASE_URL`、`OPENAI_API_KEY
|
|
111
|
+
官方 OpenAI 的写法:baseUrl 用 `https://api.openai.com/v1`,配一个具备视觉能力的模型。对应的环境变量:`OPENAI_BASE_URL`、`OPENAI_API_KEY`。模型必须是多模态的,纯文本模型会失败或产生幻觉。
|
|
112
|
+
|
|
113
|
+
这条路线默认在服务端不做任何约束,能力弱一些的模型可能只答出契约的一半,运行就会以明确报错失败。真遇到就让网关自己强制执行:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
modlens config set openai.structuredOutput true
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
契约会以 `response_format: json_schema` 的严格形式发出去,schema 由 modlens 校验用的那份推导而来。默认关闭,因为不支持结构化输出的网关会对这个字段返回 400,端点拒绝就关回去。关掉思考(见下)会让结构错误更容易出现,所以这两项常常一起用。
|
|
109
120
|
|
|
110
121
|
### anthropic(Claude API key)
|
|
111
122
|
|
|
@@ -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.16.
|
|
27
|
+
$Pinned = '3.16.6'
|
|
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.16.
|
|
25
|
+
PINNED="3.16.6"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|