@liustack/modlens 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +11 -0
- package/README.zh-CN.md +11 -0
- package/dist/main.js +218 -86
- package/docs/troubleshooting.md +17 -0
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +6 -4
- package/skills/modlens/references/cli.md +6 -0
- package/skills/modlens/references/configure.md +38 -3
- package/skills/modlens/references/runtime.md +1 -1
- package/skills/modlens/scripts/run.ps1 +1 -1
- package/skills/modlens/scripts/run.sh +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.4.0 - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- Vendor-specific request fields can now be passed through to the three API providers, which is how you turn thinking off (issue #12). `modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}'` stores it per provider, `--extra-body '<json>'` overrides it for one run, and an empty value clears it. Reasoning models spend their budget re-deriving a transcription task that needs none, so on a thinking-by-default model this is the difference between a slow read and a fast one. There is deliberately no `--no-thinking` flag: every gateway spells the knob differently (`thinking.type` on the MiMo API, `reasoning.effort` on its Responses route, `chat_template_kwargs.enable_thinking` on a self-hosted vLLM, `thinkingConfig` inside `generationConfig` on Gemini), some ignore what they do not know and others reject it with a 400, so guessing on the user's behalf would fail silently about as often as it worked. `configure.md` carries the per-vendor recipes.
|
|
6
|
+
- The passthrough deep-merges into the request body, so adding a knob to a nested block keeps what was already there (a Gemini `thinkingConfig` no longer wipes out the `responseJsonSchema` next to it). The fields that carry the image, the prompt, and the schema enforcement are reserved and rejected with a message naming the field. The two CLI providers take no request body: they warn in `meta.warnings` that the value was ignored rather than letting a run look configured when nothing was sent.
|
|
7
|
+
|
|
3
8
|
## 3.3.0 - 2026-08-07
|
|
4
9
|
|
|
5
10
|
- Automatic provider failover. A run now tries every provider that is set up on this machine, in order, and the first good result wins: a provider that errors, times out, or returns a schema-violating result hands over to the next. A local image tries `antigravity-cli`, then `gemini-api`, `openai`, `anthropic`, `claude-cli`; a remote URL tries the inline API providers first and the agent last (only the inline download path runs the private-address guards, the magic-byte check, and the size cap), and `claude-cli` never joins the remote chain since it reads local files only. The result's `meta.attempts` records every provider tried with timings and failure reasons, and `meta.warnings` carries failover notices. `doctor` prints both chains. Availability (binary on PATH, required keys present) is one shared source of truth between the doctor's readiness report and the chain. The 3.2.0 remote-URL reroute is absorbed by the remote chain order.
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
+
<a href="https://x.com/liustack"><img src="https://img.shields.io/badge/follow-%40liustack-black?style=flat-square&logo=x&logoColor=white" alt="Follow @liustack on X"></a>
|
|
19
20
|
<a href="https://www.npmjs.com/package/@liustack/modlens"><img src="https://img.shields.io/npm/v/@liustack/modlens?style=flat-square&label=npm&color=cb3837" alt="npm"></a>
|
|
20
21
|
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@liustack/modlens?style=flat-square" alt="Node.js"></a>
|
|
21
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License"></a>
|
|
@@ -101,6 +102,16 @@ npx -y skills add liustack/liustack -g
|
|
|
101
102
|
|
|
102
103
|
⭐ If it helps, star [ModLens](https://github.com/liustack/modlens) and [liustack](https://github.com/liustack/liustack). Stars are how the next developer finds them.
|
|
103
104
|
|
|
105
|
+
## Star History
|
|
106
|
+
|
|
107
|
+
<a href="https://www.star-history.com/?repos=liustack%2Fmodlens&type=date&legend=top-left">
|
|
108
|
+
<picture>
|
|
109
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=liustack/modlens&type=date&theme=dark&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
110
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=liustack/modlens&type=date&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
111
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=liustack/modlens&type=date&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
112
|
+
</picture>
|
|
113
|
+
</a>
|
|
114
|
+
|
|
104
115
|
## Disclaimer
|
|
105
116
|
|
|
106
117
|
Provided as-is under the MIT License below. The author makes no warranty and gives no endorsement for any particular use, commercial use included. Your use of upstream engines (Antigravity CLI, the Gemini, OpenAI, and Anthropic APIs, and any OpenAI-compatible endpoint) is governed by their own terms and quotas, which you are responsible for.
|
package/README.zh-CN.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
|
+
<a href="https://x.com/liustack"><img src="https://img.shields.io/badge/follow-%40liustack-black?style=flat-square&logo=x&logoColor=white" alt="Follow @liustack on X"></a>
|
|
19
20
|
<a href="https://www.npmjs.com/package/@liustack/modlens"><img src="https://img.shields.io/npm/v/@liustack/modlens?style=flat-square&label=npm&color=cb3837" alt="npm"></a>
|
|
20
21
|
<a href="https://nodejs.org"><img src="https://img.shields.io/node/v/@liustack/modlens?style=flat-square" alt="Node.js"></a>
|
|
21
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License"></a>
|
|
@@ -101,6 +102,16 @@ Codex 桌面 App 中识别一张推文截图。配文、互动数据(2.9K 回
|
|
|
101
102
|
|
|
102
103
|
⭐ 如果它对你有用,请给 [ModLens](https://github.com/liustack/modlens) 一个 star,这是其他开发者找到它的方式。
|
|
103
104
|
|
|
105
|
+
## Star History
|
|
106
|
+
|
|
107
|
+
<a href="https://www.star-history.com/?repos=liustack%2Fmodlens&type=date&legend=top-left">
|
|
108
|
+
<picture>
|
|
109
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=liustack/modlens&type=date&theme=dark&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
110
|
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=liustack/modlens&type=date&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
111
|
+
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=liustack/modlens&type=date&legend=top-left&sealed_token=oQQAwrPffo9WRUsM6P4RnEu4ZdRART3ChPwIkavGtAfrMycGmLYdjuM2uJ4gjnoIyaF_MDwhOBkJlzmS8pT_W9IRDlsCqLafe7gwvw7Vcnr5MRTkczOasg" />
|
|
112
|
+
</picture>
|
|
113
|
+
</a>
|
|
114
|
+
|
|
104
115
|
## 免责声明
|
|
105
116
|
|
|
106
117
|
本项目依下方 MIT 协议按现状提供。作者不对任何特定用途(含商业使用)提供保证或背书。上游引擎(Antigravity CLI,Gemini、OpenAI、Anthropic 的 API,以及任何 OpenAI 兼容端点)的使用受各自条款和额度约束,由使用者负责。
|
package/dist/main.js
CHANGED
|
@@ -7,7 +7,7 @@ import { spawn } from "child_process";
|
|
|
7
7
|
import * as os from "os";
|
|
8
8
|
import require$$0$1 from "node:assert";
|
|
9
9
|
import require$$1 from "node:net";
|
|
10
|
-
import require$$2 from "node:http";
|
|
10
|
+
import require$$2$1 from "node:http";
|
|
11
11
|
import require$$0$2 from "node:stream";
|
|
12
12
|
import require$$5 from "node:querystring";
|
|
13
13
|
import require$$0 from "node:events";
|
|
@@ -18,21 +18,44 @@ import require$$0$4 from "node:buffer";
|
|
|
18
18
|
import require$$0$5 from "node:zlib";
|
|
19
19
|
import require$$5$1 from "node:perf_hooks";
|
|
20
20
|
import require$$8 from "node:util/types";
|
|
21
|
-
import require$$2$
|
|
22
|
-
import require$$2$
|
|
23
|
-
import require$$1$1 from "node:sqlite";
|
|
21
|
+
import require$$2$2 from "node:worker_threads";
|
|
22
|
+
import require$$2$3 from "node:crypto";
|
|
24
23
|
import require$$11 from "node:stream/web";
|
|
25
24
|
import require$$0$6 from "node:url";
|
|
26
|
-
import require$$1$
|
|
27
|
-
import require$$1$
|
|
25
|
+
import require$$1$1 from "node:async_hooks";
|
|
26
|
+
import require$$1$2 from "node:console";
|
|
28
27
|
import require$$0$7 from "node:fs/promises";
|
|
29
|
-
import require$$1$
|
|
30
|
-
import require$$2$
|
|
31
|
-
import require$$1$
|
|
28
|
+
import require$$1$3 from "node:path";
|
|
29
|
+
import require$$2$4 from "node:timers";
|
|
30
|
+
import require$$1$4 from "node:dns";
|
|
32
31
|
import * as dns$1 from "dns/promises";
|
|
33
32
|
import { isIP } from "net";
|
|
34
33
|
import { createRequire } from "module";
|
|
35
34
|
import * as crypto from "crypto";
|
|
35
|
+
function getAugmentedNamespace(n) {
|
|
36
|
+
if (Object.prototype.hasOwnProperty.call(n, "__esModule")) return n;
|
|
37
|
+
var f = n.default;
|
|
38
|
+
if (typeof f == "function") {
|
|
39
|
+
var a = function a2() {
|
|
40
|
+
if (this instanceof a2) {
|
|
41
|
+
return Reflect.construct(f, arguments, this.constructor);
|
|
42
|
+
}
|
|
43
|
+
return f.apply(this, arguments);
|
|
44
|
+
};
|
|
45
|
+
a.prototype = f.prototype;
|
|
46
|
+
} else a = {};
|
|
47
|
+
Object.defineProperty(a, "__esModule", { value: true });
|
|
48
|
+
Object.keys(n).forEach(function(k) {
|
|
49
|
+
var d = Object.getOwnPropertyDescriptor(n, k);
|
|
50
|
+
Object.defineProperty(a, k, d.get ? d : {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
get: function() {
|
|
53
|
+
return n[k];
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
return a;
|
|
58
|
+
}
|
|
36
59
|
var undici = { exports: {} };
|
|
37
60
|
var symbols;
|
|
38
61
|
var hasRequiredSymbols;
|
|
@@ -1038,7 +1061,7 @@ function requireUtil$5() {
|
|
|
1038
1061
|
hasRequiredUtil$5 = 1;
|
|
1039
1062
|
const assert = require$$0$1;
|
|
1040
1063
|
const { kDestroyed, kBodyUsed, kListeners, kBody } = requireSymbols();
|
|
1041
|
-
const { IncomingMessage } = require$$2;
|
|
1064
|
+
const { IncomingMessage } = require$$2$1;
|
|
1042
1065
|
const stream = require$$0$2;
|
|
1043
1066
|
const net = require$$1;
|
|
1044
1067
|
const { stringify } = require$$5;
|
|
@@ -4286,7 +4309,7 @@ function requireWebidl() {
|
|
|
4286
4309
|
hasRequiredWebidl = 1;
|
|
4287
4310
|
const assert = require$$0$1;
|
|
4288
4311
|
const { types, inspect } = require$$3;
|
|
4289
|
-
const { markAsUncloneable } = require$$2$
|
|
4312
|
+
const { markAsUncloneable } = require$$2$2;
|
|
4290
4313
|
const UNDEFINED = 1;
|
|
4291
4314
|
const BOOLEAN = 2;
|
|
4292
4315
|
const STRING = 3;
|
|
@@ -5647,14 +5670,20 @@ function requireUtil$4() {
|
|
|
5647
5670
|
return util$4;
|
|
5648
5671
|
}
|
|
5649
5672
|
var runtimeFeatures = {};
|
|
5673
|
+
const __viteBrowserExternal = {};
|
|
5674
|
+
const __viteBrowserExternal$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
5675
|
+
__proto__: null,
|
|
5676
|
+
default: __viteBrowserExternal
|
|
5677
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
5678
|
+
const require$$2 = /* @__PURE__ */ getAugmentedNamespace(__viteBrowserExternal$1);
|
|
5650
5679
|
var hasRequiredRuntimeFeatures;
|
|
5651
5680
|
function requireRuntimeFeatures() {
|
|
5652
5681
|
if (hasRequiredRuntimeFeatures) return runtimeFeatures;
|
|
5653
5682
|
hasRequiredRuntimeFeatures = 1;
|
|
5654
5683
|
const lazyLoaders = {
|
|
5655
5684
|
__proto__: null,
|
|
5656
|
-
"node:crypto": () => require$$2$
|
|
5657
|
-
"node:sqlite": () => require$$
|
|
5685
|
+
"node:crypto": () => require$$2$3,
|
|
5686
|
+
"node:sqlite": () => require$$2
|
|
5658
5687
|
};
|
|
5659
5688
|
function detectRuntimeFeatureByNodeModule(moduleName) {
|
|
5660
5689
|
try {
|
|
@@ -5731,7 +5760,7 @@ function requireFormdata() {
|
|
|
5731
5760
|
const { webidl } = requireWebidl();
|
|
5732
5761
|
const nodeUtil = require$$3;
|
|
5733
5762
|
const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
|
|
5734
|
-
const random = runtimeFeatures2.has("crypto") ? require$$2$
|
|
5763
|
+
const random = runtimeFeatures2.has("crypto") ? require$$2$3.randomInt : (max) => Math.floor(Math.random() * max);
|
|
5735
5764
|
class FormData {
|
|
5736
5765
|
#state = [];
|
|
5737
5766
|
#boundary = null;
|
|
@@ -9083,7 +9112,7 @@ function requireClient() {
|
|
|
9083
9112
|
hasRequiredClient = 1;
|
|
9084
9113
|
const assert = require$$0$1;
|
|
9085
9114
|
const net = require$$1;
|
|
9086
|
-
const http = require$$2;
|
|
9115
|
+
const http = require$$2$1;
|
|
9087
9116
|
const util2 = requireUtil$5();
|
|
9088
9117
|
const { ClientStats } = requireStats();
|
|
9089
9118
|
const { channels } = requireDiagnostics();
|
|
@@ -12557,7 +12586,7 @@ function requireApiRequest() {
|
|
|
12557
12586
|
if (hasRequiredApiRequest) return apiRequest.exports;
|
|
12558
12587
|
hasRequiredApiRequest = 1;
|
|
12559
12588
|
const assert = require$$0$1;
|
|
12560
|
-
const { AsyncResource } = require$$1$
|
|
12589
|
+
const { AsyncResource } = require$$1$1;
|
|
12561
12590
|
const { Readable } = requireReadable();
|
|
12562
12591
|
const { InvalidArgumentError, RequestAbortedError } = requireErrors();
|
|
12563
12592
|
const util2 = requireUtil$5();
|
|
@@ -12810,7 +12839,7 @@ function requireApiStream() {
|
|
|
12810
12839
|
if (hasRequiredApiStream) return apiStream;
|
|
12811
12840
|
hasRequiredApiStream = 1;
|
|
12812
12841
|
const assert = require$$0$1;
|
|
12813
|
-
const { AsyncResource } = require$$1$
|
|
12842
|
+
const { AsyncResource } = require$$1$1;
|
|
12814
12843
|
const { InvalidArgumentError, InvalidReturnValueError } = requireErrors();
|
|
12815
12844
|
const util2 = requireUtil$5();
|
|
12816
12845
|
const { addSignal, removeSignal } = requireAbortSignal();
|
|
@@ -13025,7 +13054,7 @@ function requireApiPipeline() {
|
|
|
13025
13054
|
PassThrough
|
|
13026
13055
|
} = require$$0$2;
|
|
13027
13056
|
const assert = require$$0$1;
|
|
13028
|
-
const { AsyncResource } = require$$1$
|
|
13057
|
+
const { AsyncResource } = require$$1$1;
|
|
13029
13058
|
const {
|
|
13030
13059
|
InvalidArgumentError,
|
|
13031
13060
|
InvalidReturnValueError,
|
|
@@ -13227,7 +13256,7 @@ function requireApiUpgrade() {
|
|
|
13227
13256
|
if (hasRequiredApiUpgrade) return apiUpgrade;
|
|
13228
13257
|
hasRequiredApiUpgrade = 1;
|
|
13229
13258
|
const { InvalidArgumentError, SocketError } = requireErrors();
|
|
13230
|
-
const { AsyncResource } = require$$1$
|
|
13259
|
+
const { AsyncResource } = require$$1$1;
|
|
13231
13260
|
const assert = require$$0$1;
|
|
13232
13261
|
const util2 = requireUtil$5();
|
|
13233
13262
|
const { kHTTP2Stream } = requireSymbols();
|
|
@@ -13327,7 +13356,7 @@ function requireApiConnect() {
|
|
|
13327
13356
|
if (hasRequiredApiConnect) return apiConnect;
|
|
13328
13357
|
hasRequiredApiConnect = 1;
|
|
13329
13358
|
const assert = require$$0$1;
|
|
13330
|
-
const { AsyncResource } = require$$1$
|
|
13359
|
+
const { AsyncResource } = require$$1$1;
|
|
13331
13360
|
const { InvalidArgumentError, SocketError } = requireErrors();
|
|
13332
13361
|
const util2 = requireUtil$5();
|
|
13333
13362
|
const { addSignal, removeSignal } = requireAbortSignal();
|
|
@@ -13502,7 +13531,7 @@ function requireMockUtils() {
|
|
|
13502
13531
|
kTotalDispatchCount
|
|
13503
13532
|
} = requireMockSymbols();
|
|
13504
13533
|
const { serializePathWithQuery, parseHeaders } = requireUtil$5();
|
|
13505
|
-
const { STATUS_CODES } = require$$2;
|
|
13534
|
+
const { STATUS_CODES } = require$$2$1;
|
|
13506
13535
|
const {
|
|
13507
13536
|
types: {
|
|
13508
13537
|
isPromise
|
|
@@ -14543,7 +14572,7 @@ function requirePendingInterceptorsFormatter() {
|
|
|
14543
14572
|
if (hasRequiredPendingInterceptorsFormatter) return pendingInterceptorsFormatter;
|
|
14544
14573
|
hasRequiredPendingInterceptorsFormatter = 1;
|
|
14545
14574
|
const { Transform } = require$$0$2;
|
|
14546
|
-
const { Console } = require$$1$
|
|
14575
|
+
const { Console } = require$$1$2;
|
|
14547
14576
|
const PERSISTENT = process.versions.icu ? "✅" : "Y ";
|
|
14548
14577
|
const NOT_PERSISTENT = process.versions.icu ? "❌" : "N ";
|
|
14549
14578
|
pendingInterceptorsFormatter = class PendingInterceptorsFormatter {
|
|
@@ -14777,7 +14806,7 @@ function requireSnapshotUtils() {
|
|
|
14777
14806
|
match: new Set(matchHeaders.map((header) => caseSensitive ? header : header.toLowerCase()))
|
|
14778
14807
|
};
|
|
14779
14808
|
}
|
|
14780
|
-
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$
|
|
14809
|
+
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$3 : null;
|
|
14781
14810
|
const hashId = crypto2?.hash ? (value) => crypto2.hash("sha256", value, "base64url") : (value) => Buffer.from(value).toString("base64url");
|
|
14782
14811
|
function isUndiciHeaders(headers2) {
|
|
14783
14812
|
return Array.isArray(headers2) && (headers2.length & 1) === 0;
|
|
@@ -14854,8 +14883,8 @@ function requireSnapshotRecorder() {
|
|
|
14854
14883
|
if (hasRequiredSnapshotRecorder) return snapshotRecorder;
|
|
14855
14884
|
hasRequiredSnapshotRecorder = 1;
|
|
14856
14885
|
const { writeFile, readFile, mkdir } = require$$0$7;
|
|
14857
|
-
const { dirname, resolve } = require$$1$
|
|
14858
|
-
const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$
|
|
14886
|
+
const { dirname, resolve } = require$$1$3;
|
|
14887
|
+
const { setTimeout: setTimeout2, clearTimeout: clearTimeout2 } = require$$2$4;
|
|
14859
14888
|
const { InvalidArgumentError, UndiciError } = requireErrors();
|
|
14860
14889
|
const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = requireSnapshotUtils();
|
|
14861
14890
|
function normalizeUrlForMatching(url, matchQuery, normalizeQuery) {
|
|
@@ -16040,7 +16069,7 @@ function requireDns() {
|
|
|
16040
16069
|
if (hasRequiredDns) return dns;
|
|
16041
16070
|
hasRequiredDns = 1;
|
|
16042
16071
|
const { isIP: isIP2 } = require$$1;
|
|
16043
|
-
const { lookup } = require$$1$
|
|
16072
|
+
const { lookup } = require$$1$4;
|
|
16044
16073
|
const DecoratorHandler = requireDecoratorHandler();
|
|
16045
16074
|
const { InvalidArgumentError, InformationalError } = requireErrors();
|
|
16046
16075
|
const maxInt = Math.pow(2, 31) - 1;
|
|
@@ -19506,7 +19535,7 @@ function requireSqliteCacheStore() {
|
|
|
19506
19535
|
}
|
|
19507
19536
|
}
|
|
19508
19537
|
if (!DatabaseSync) {
|
|
19509
|
-
DatabaseSync = require$$
|
|
19538
|
+
DatabaseSync = require$$2.DatabaseSync;
|
|
19510
19539
|
}
|
|
19511
19540
|
this.#db = new DatabaseSync(opts?.location ?? ":memory:");
|
|
19512
19541
|
this.#db.exec(`
|
|
@@ -21471,7 +21500,7 @@ function requireSubresourceIntegrity() {
|
|
|
21471
21500
|
const validSRIHashAlgorithmTokenSet = /* @__PURE__ */ new Map([["sha256", 0], ["sha384", 1], ["sha512", 2]]);
|
|
21472
21501
|
let crypto2;
|
|
21473
21502
|
if (runtimeFeatures2.has("crypto")) {
|
|
21474
|
-
crypto2 = require$$2$
|
|
21503
|
+
crypto2 = require$$2$3;
|
|
21475
21504
|
const cryptoHashes = crypto2.getHashes();
|
|
21476
21505
|
if (cryptoHashes.length === 0) {
|
|
21477
21506
|
validSRIHashAlgorithmTokenSet.clear();
|
|
@@ -21665,7 +21694,7 @@ function requireFetch() {
|
|
|
21665
21694
|
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = requireDataUrl();
|
|
21666
21695
|
const { getGlobalDispatcher } = requireGlobal();
|
|
21667
21696
|
const { webidl } = requireWebidl();
|
|
21668
|
-
const { STATUS_CODES } = require$$2;
|
|
21697
|
+
const { STATUS_CODES } = require$$2$1;
|
|
21669
21698
|
const { bytesMatch } = requireSubresourceIntegrity();
|
|
21670
21699
|
const { isomorphicEncode } = requireInfra();
|
|
21671
21700
|
const GET_OR_HEAD = ["GET", "HEAD"];
|
|
@@ -24425,7 +24454,7 @@ function requireFrame() {
|
|
|
24425
24454
|
const BUFFER_SIZE = 8 * 1024;
|
|
24426
24455
|
let buffer = null;
|
|
24427
24456
|
let bufIdx = BUFFER_SIZE;
|
|
24428
|
-
const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$
|
|
24457
|
+
const randomFillSync = runtimeFeatures2.has("crypto") ? require$$2$3.randomFillSync : null;
|
|
24429
24458
|
function generateMask() {
|
|
24430
24459
|
if (bufIdx === BUFFER_SIZE) {
|
|
24431
24460
|
bufIdx = 0;
|
|
@@ -24530,7 +24559,7 @@ function requireConnection() {
|
|
|
24530
24559
|
const { WebsocketFrameSend } = requireFrame();
|
|
24531
24560
|
const assert = require$$0$1;
|
|
24532
24561
|
const { runtimeFeatures: runtimeFeatures2 } = requireRuntimeFeatures();
|
|
24533
|
-
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$
|
|
24562
|
+
const crypto2 = runtimeFeatures2.has("crypto") ? require$$2$3 : null;
|
|
24534
24563
|
let warningEmitted = false;
|
|
24535
24564
|
function establishWebSocketConnection(url, protocols, client2, handler, options) {
|
|
24536
24565
|
const requestURL = url;
|
|
@@ -27508,6 +27537,54 @@ function schemaViolations(schema, value, path2) {
|
|
|
27508
27537
|
}
|
|
27509
27538
|
return [];
|
|
27510
27539
|
}
|
|
27540
|
+
function parseExtraBody(raw, origin) {
|
|
27541
|
+
let parsed;
|
|
27542
|
+
try {
|
|
27543
|
+
parsed = JSON.parse(raw);
|
|
27544
|
+
} catch (error) {
|
|
27545
|
+
throw new Error(`${origin} is not valid JSON: ${error.message}`);
|
|
27546
|
+
}
|
|
27547
|
+
if (!isPlainObject(parsed)) {
|
|
27548
|
+
throw new Error(
|
|
27549
|
+
`${origin} must be a JSON object, for example {"thinking":{"type":"disabled"}}`
|
|
27550
|
+
);
|
|
27551
|
+
}
|
|
27552
|
+
return parsed;
|
|
27553
|
+
}
|
|
27554
|
+
function mergeExtraBody(body2, extra, reserved, providerName) {
|
|
27555
|
+
if (!extra || Object.keys(extra).length === 0) {
|
|
27556
|
+
return body2;
|
|
27557
|
+
}
|
|
27558
|
+
for (const path2 of reserved) {
|
|
27559
|
+
if (hasPath(extra, path2)) {
|
|
27560
|
+
throw new Error(
|
|
27561
|
+
`extraBody cannot override "${path2}" for the ${providerName} provider: it carries the image, the prompt, or the schema this tool depends on. Remove that field from ${providerName}.extraBody (or --extra-body).`
|
|
27562
|
+
);
|
|
27563
|
+
}
|
|
27564
|
+
}
|
|
27565
|
+
return deepMerge(body2, extra);
|
|
27566
|
+
}
|
|
27567
|
+
function deepMerge(base, overlay) {
|
|
27568
|
+
const merged = { ...base };
|
|
27569
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
27570
|
+
const current = merged[key];
|
|
27571
|
+
merged[key] = isPlainObject(current) && isPlainObject(value) ? deepMerge(current, value) : value;
|
|
27572
|
+
}
|
|
27573
|
+
return merged;
|
|
27574
|
+
}
|
|
27575
|
+
function hasPath(value, dottedPath) {
|
|
27576
|
+
let cursor = value;
|
|
27577
|
+
for (const segment of dottedPath.split(".")) {
|
|
27578
|
+
if (!isPlainObject(cursor) || !Object.hasOwn(cursor, segment)) {
|
|
27579
|
+
return false;
|
|
27580
|
+
}
|
|
27581
|
+
cursor = cursor[segment];
|
|
27582
|
+
}
|
|
27583
|
+
return true;
|
|
27584
|
+
}
|
|
27585
|
+
function isPlainObject(value) {
|
|
27586
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27587
|
+
}
|
|
27511
27588
|
function tryParseJson(text) {
|
|
27512
27589
|
try {
|
|
27513
27590
|
return JSON.parse(text);
|
|
@@ -27584,27 +27661,34 @@ Report your findings by calling the ${TOOL_NAME} tool.`;
|
|
|
27584
27661
|
"anthropic-version": "2023-06-01",
|
|
27585
27662
|
"Content-Type": "application/json"
|
|
27586
27663
|
},
|
|
27587
|
-
body: JSON.stringify(
|
|
27588
|
-
|
|
27589
|
-
max_tokens: 4096,
|
|
27590
|
-
tools: [
|
|
27664
|
+
body: JSON.stringify(
|
|
27665
|
+
mergeExtraBody(
|
|
27591
27666
|
{
|
|
27592
|
-
|
|
27593
|
-
|
|
27594
|
-
|
|
27595
|
-
|
|
27596
|
-
|
|
27597
|
-
|
|
27598
|
-
|
|
27599
|
-
|
|
27600
|
-
|
|
27601
|
-
|
|
27602
|
-
|
|
27603
|
-
{
|
|
27667
|
+
model,
|
|
27668
|
+
max_tokens: 4096,
|
|
27669
|
+
tools: [
|
|
27670
|
+
{
|
|
27671
|
+
name: TOOL_NAME,
|
|
27672
|
+
description: "Report the structured visual evidence extracted from the image.",
|
|
27673
|
+
input_schema: VISION_RESULT_SCHEMA
|
|
27674
|
+
}
|
|
27675
|
+
],
|
|
27676
|
+
tool_choice: { type: "tool", name: TOOL_NAME },
|
|
27677
|
+
messages: [
|
|
27678
|
+
{
|
|
27679
|
+
role: "user",
|
|
27680
|
+
content: [
|
|
27681
|
+
{ type: "image", source: imageSource },
|
|
27682
|
+
{ type: "text", text: prompt }
|
|
27683
|
+
]
|
|
27684
|
+
}
|
|
27604
27685
|
]
|
|
27605
|
-
}
|
|
27606
|
-
|
|
27607
|
-
|
|
27686
|
+
},
|
|
27687
|
+
options.settings?.extraBody,
|
|
27688
|
+
["model", "messages", "tools", "tool_choice", "stream"],
|
|
27689
|
+
"anthropic"
|
|
27690
|
+
)
|
|
27691
|
+
),
|
|
27608
27692
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27609
27693
|
});
|
|
27610
27694
|
if (!response2.ok) {
|
|
@@ -27871,20 +27955,31 @@ async function executeGeminiApi(options) {
|
|
|
27871
27955
|
"x-goog-api-key": apiKey,
|
|
27872
27956
|
"Content-Type": "application/json"
|
|
27873
27957
|
},
|
|
27874
|
-
body: JSON.stringify(
|
|
27875
|
-
|
|
27958
|
+
body: JSON.stringify(
|
|
27959
|
+
mergeExtraBody(
|
|
27876
27960
|
{
|
|
27877
|
-
|
|
27878
|
-
{
|
|
27879
|
-
|
|
27880
|
-
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
|
|
27885
|
-
|
|
27886
|
-
|
|
27887
|
-
|
|
27961
|
+
contents: [
|
|
27962
|
+
{
|
|
27963
|
+
parts: [
|
|
27964
|
+
{ inline_data: { mime_type: image.mimeType, data: image.data } },
|
|
27965
|
+
{ text: prompt }
|
|
27966
|
+
]
|
|
27967
|
+
}
|
|
27968
|
+
],
|
|
27969
|
+
generationConfig: {
|
|
27970
|
+
responseMimeType: "application/json",
|
|
27971
|
+
responseJsonSchema: VISION_RESULT_SCHEMA
|
|
27972
|
+
}
|
|
27973
|
+
},
|
|
27974
|
+
options.settings?.extraBody,
|
|
27975
|
+
[
|
|
27976
|
+
"contents",
|
|
27977
|
+
"generationConfig.responseMimeType",
|
|
27978
|
+
"generationConfig.responseJsonSchema"
|
|
27979
|
+
],
|
|
27980
|
+
"gemini-api"
|
|
27981
|
+
)
|
|
27982
|
+
),
|
|
27888
27983
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27889
27984
|
});
|
|
27890
27985
|
if (!response2.ok) {
|
|
@@ -27941,18 +28036,25 @@ Respond with ONE JSON object only, no markdown fences, no commentary. Fill this
|
|
|
27941
28036
|
Authorization: `Bearer ${apiKey}`,
|
|
27942
28037
|
"Content-Type": "application/json"
|
|
27943
28038
|
},
|
|
27944
|
-
body: JSON.stringify(
|
|
27945
|
-
|
|
27946
|
-
messages: [
|
|
28039
|
+
body: JSON.stringify(
|
|
28040
|
+
mergeExtraBody(
|
|
27947
28041
|
{
|
|
27948
|
-
|
|
27949
|
-
|
|
27950
|
-
{
|
|
27951
|
-
|
|
28042
|
+
model,
|
|
28043
|
+
messages: [
|
|
28044
|
+
{
|
|
28045
|
+
role: "user",
|
|
28046
|
+
content: [
|
|
28047
|
+
{ type: "image_url", image_url: { url: imageUrl } },
|
|
28048
|
+
{ type: "text", text: prompt }
|
|
28049
|
+
]
|
|
28050
|
+
}
|
|
27952
28051
|
]
|
|
27953
|
-
}
|
|
27954
|
-
|
|
27955
|
-
|
|
28052
|
+
},
|
|
28053
|
+
options.settings?.extraBody,
|
|
28054
|
+
["model", "messages", "stream"],
|
|
28055
|
+
"openai"
|
|
28056
|
+
)
|
|
28057
|
+
),
|
|
27956
28058
|
signal: AbortSignal.timeout(options.timeoutMs)
|
|
27957
28059
|
});
|
|
27958
28060
|
if (!response2.ok) {
|
|
@@ -28022,6 +28124,7 @@ function providerAliases() {
|
|
|
28022
28124
|
function listProviders() {
|
|
28023
28125
|
return [...new Set(Object.values(PROVIDERS).map((provider) => provider.name))];
|
|
28024
28126
|
}
|
|
28127
|
+
const STRING_FIELDS = ["apiKey", "baseUrl", "model"];
|
|
28025
28128
|
const CONFIG_DIR = path.join(os.homedir(), ".modlens");
|
|
28026
28129
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
28027
28130
|
const ENV_BINDINGS = {
|
|
@@ -28077,17 +28180,31 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28077
28180
|
const dot = dottedKey.indexOf(".");
|
|
28078
28181
|
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
28079
28182
|
throw new Error(
|
|
28080
|
-
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model>".`
|
|
28183
|
+
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
28081
28184
|
);
|
|
28082
28185
|
}
|
|
28083
28186
|
const providerName = dottedKey.slice(0, dot);
|
|
28084
28187
|
const field = dottedKey.slice(dot + 1);
|
|
28085
|
-
if (
|
|
28086
|
-
|
|
28188
|
+
if (field === "extraBody") {
|
|
28189
|
+
config2.providers ??= {};
|
|
28190
|
+
config2.providers[providerName] ??= {};
|
|
28191
|
+
if (value.trim() === "") {
|
|
28192
|
+
delete config2.providers[providerName].extraBody;
|
|
28193
|
+
} else {
|
|
28194
|
+
config2.providers[providerName].extraBody = parseExtraBody(
|
|
28195
|
+
value,
|
|
28196
|
+
`${providerName}.extraBody`
|
|
28197
|
+
);
|
|
28198
|
+
}
|
|
28199
|
+
} else if (!STRING_FIELDS.includes(field)) {
|
|
28200
|
+
throw new Error(
|
|
28201
|
+
`Unknown config field: ${field}. Use apiKey, baseUrl, model, or extraBody.`
|
|
28202
|
+
);
|
|
28203
|
+
} else {
|
|
28204
|
+
config2.providers ??= {};
|
|
28205
|
+
config2.providers[providerName] ??= {};
|
|
28206
|
+
config2.providers[providerName][field] = value;
|
|
28087
28207
|
}
|
|
28088
|
-
config2.providers ??= {};
|
|
28089
|
-
config2.providers[providerName] ??= {};
|
|
28090
|
-
config2.providers[providerName][field] = value;
|
|
28091
28208
|
}
|
|
28092
28209
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
28093
28210
|
fs.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
@@ -28126,7 +28243,7 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28126
28243
|
const fileSettings = config2.providers?.[name] ?? {};
|
|
28127
28244
|
const bindings = ENV_BINDINGS[name] ?? {};
|
|
28128
28245
|
const fields = {};
|
|
28129
|
-
for (const field of
|
|
28246
|
+
for (const field of STRING_FIELDS) {
|
|
28130
28247
|
const envName = bindings[field];
|
|
28131
28248
|
const envValue = envName ? env[envName]?.trim() : void 0;
|
|
28132
28249
|
const value = envValue ?? fileSettings[field];
|
|
@@ -28136,6 +28253,9 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28136
28253
|
fields[field] = `${shown} (${source})`;
|
|
28137
28254
|
}
|
|
28138
28255
|
}
|
|
28256
|
+
if (fileSettings.extraBody !== void 0) {
|
|
28257
|
+
fields.extraBody = `${JSON.stringify(fileSettings.extraBody)} (file)`;
|
|
28258
|
+
}
|
|
28139
28259
|
if (Object.keys(fields).length > 0) {
|
|
28140
28260
|
providers[name] = fields;
|
|
28141
28261
|
}
|
|
@@ -28273,7 +28393,8 @@ async function analyzeImage(options) {
|
|
|
28273
28393
|
options,
|
|
28274
28394
|
resolvedInput,
|
|
28275
28395
|
timeoutMs,
|
|
28276
|
-
config2
|
|
28396
|
+
config2,
|
|
28397
|
+
warnings
|
|
28277
28398
|
);
|
|
28278
28399
|
attempts.push({
|
|
28279
28400
|
provider: provider.name,
|
|
@@ -28323,8 +28444,14 @@ async function analyzeImage(options) {
|
|
|
28323
28444
|
`Every configured vision provider failed for this image. ${attempts.map((attempt) => `${attempt.provider}: ${attempt.error}`).join(" | ")}`
|
|
28324
28445
|
);
|
|
28325
28446
|
}
|
|
28326
|
-
async function runProvider(provider, model, options, resolvedInput, timeoutMs, config2) {
|
|
28327
|
-
const
|
|
28447
|
+
async function runProvider(provider, model, options, resolvedInput, timeoutMs, config2, warnings) {
|
|
28448
|
+
const configured = resolveProviderSettings(provider.name, config2);
|
|
28449
|
+
const settings = options.extraBody ? { ...configured, extraBody: options.extraBody } : configured;
|
|
28450
|
+
if (settings.extraBody && !provider.execute) {
|
|
28451
|
+
warnings.push(
|
|
28452
|
+
`${provider.name} is a CLI provider and takes no request body, so extraBody was ignored for this run.`
|
|
28453
|
+
);
|
|
28454
|
+
}
|
|
28328
28455
|
const providerOptions = {
|
|
28329
28456
|
imageSource: resolvedInput.source,
|
|
28330
28457
|
imageKind: resolvedInput.kind,
|
|
@@ -29207,8 +29334,11 @@ function recoverPastedImages(options = {}) {
|
|
|
29207
29334
|
return result;
|
|
29208
29335
|
}
|
|
29209
29336
|
const program = new Command();
|
|
29210
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.
|
|
29211
|
-
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").
|
|
29337
|
+
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.4.0");
|
|
29338
|
+
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(
|
|
29339
|
+
"--extra-body <json>",
|
|
29340
|
+
`JSON merged into the API request body, e.g. '{"thinking":{"type":"disabled"}}'`
|
|
29341
|
+
).action(async (options) => {
|
|
29212
29342
|
try {
|
|
29213
29343
|
const timeoutMs = Number.parseInt(options.timeout, 10);
|
|
29214
29344
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -29221,7 +29351,8 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
|
|
|
29221
29351
|
prompt: options.prompt,
|
|
29222
29352
|
timeoutMs,
|
|
29223
29353
|
providerBin: options.providerBin,
|
|
29224
|
-
workdir: options.workdir
|
|
29354
|
+
workdir: options.workdir,
|
|
29355
|
+
extraBody: options.extraBody ? parseExtraBody(options.extraBody, "--extra-body") : void 0
|
|
29225
29356
|
});
|
|
29226
29357
|
const output = JSON.stringify(result, null, 2);
|
|
29227
29358
|
if (options.output) {
|
|
@@ -29302,6 +29433,7 @@ config.command("init").description(`Create a starter config at ${CONFIG_PATH}`).
|
|
|
29302
29433
|
"Everything is optional. Two things you can set:",
|
|
29303
29434
|
" modlens config set provider <name> which provider analyzes images",
|
|
29304
29435
|
" modlens config set <provider>.<apiKey|baseUrl|model> <value> provider credentials",
|
|
29436
|
+
` modlens config set <provider>.extraBody '{"thinking":{"type":"disabled"}}' vendor request fields`,
|
|
29305
29437
|
""
|
|
29306
29438
|
].join("\n")
|
|
29307
29439
|
);
|
package/docs/troubleshooting.md
CHANGED
|
@@ -127,6 +127,23 @@ antigravity-cli provider timed out after 210000 ms.
|
|
|
127
127
|
|
|
128
128
|
Retry once with `--timeout 300000`. Dense images on agy legitimately take 15-40 seconds, and `-m gemini-3.1-pro-high` is slower still. Engines that ignore SIGTERM are escalated to SIGKILL, so a timeout returns promptly regardless.
|
|
129
129
|
|
|
130
|
+
## Every read is slow on a reasoning model
|
|
131
|
+
|
|
132
|
+
A model that thinks by default spends its budget before it starts transcribing, which a vision read does not need. There is no `--no-thinking` flag because each vendor names the switch differently, so pass the vendor's own field:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}'
|
|
136
|
+
modlens -i shot.png --extra-body '{"reasoning_effort":"low"}' # one run only
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The per-vendor spellings, which models cannot turn it off at all, and how to tell whether the field actually landed are in [Configuration](../skills/modlens/references/configure.md#turning-thinking-off).
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
extraBody cannot override "messages" for the openai provider
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That field carries the image, the prompt, or the schema enforcement. Remove it and keep the vendor knobs. A 400 from the gateway naming a field you set means that endpoint uses a different spelling, and a run on `antigravity-cli` or `claude-cli` says in `meta.warnings` that it ignored the value, since a CLI provider has no request body.
|
|
146
|
+
|
|
130
147
|
## Windows
|
|
131
148
|
|
|
132
149
|
ModLens runs on Windows. Three platform differences are worth knowing:
|
package/package.json
CHANGED
package/skills/modlens/SKILL.md
CHANGED
|
@@ -33,11 +33,11 @@ The launcher finds a working way to run modlens and forwards your arguments to i
|
|
|
33
33
|
|
|
34
34
|
### If you cannot run the launcher script
|
|
35
35
|
|
|
36
|
-
Some harnesses forbid running scripts. Reason through the same order by hand and run the first line that works (the pinned version is 3.
|
|
36
|
+
Some harnesses forbid running scripts. Reason through the same order by hand and run the first line that works (the pinned version is 3.4.0):
|
|
37
37
|
|
|
38
|
-
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.
|
|
39
|
-
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.
|
|
40
|
-
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.
|
|
38
|
+
1. A `modlens` on `PATH` whose major version is 3 and is at least 3.4.0: `modlens <args>`.
|
|
39
|
+
2. Otherwise, if `npx` exists: `npx --yes --package @liustack/modlens@3.4.0 modlens <args>`.
|
|
40
|
+
3. Otherwise, if `bunx` exists: `bunx --bun @liustack/modlens@3.4.0 <args>`.
|
|
41
41
|
4. Otherwise none of these runtimes is here. Tell the user no JavaScript runtime was found and that installing Node 22.13+ (https://nodejs.org) or Bun (https://bun.sh) is the next step. Do not claim modlens itself failed.
|
|
42
42
|
|
|
43
43
|
`references/runtime.md` documents the version pin, the compatibility rule, and the diagnostic fields.
|
|
@@ -78,6 +78,8 @@ modlens -i <image> -o <output.json> -m <model> --prompt "<extra focus>" --timeou
|
|
|
78
78
|
|
|
79
79
|
Speed expectations: `gemini-api` typically 5-10 seconds, `antigravity-cli` 15-40 seconds and `claude-cli` 20-45 seconds (full agent loops), `openai`/`anthropic` depend on the endpoint. For dense or hard images on antigravity-cli, try `-m gemini-3.1-pro-high`.
|
|
80
80
|
|
|
81
|
+
If every read is slow because the configured model thinks before answering, pass the vendor's own switch through the request body, for example `--extra-body '{"thinking":{"type":"disabled"}}'`, or store it with `modlens config set <provider>.extraBody '<json>'`. The spelling differs per endpoint, so read `references/configure.md` before guessing.
|
|
82
|
+
|
|
81
83
|
## Finding the image path in the chat
|
|
82
84
|
|
|
83
85
|
Harnesses rarely hand you a clean path. First identify which harness you are in, then use its route. Never mix routes across harnesses.
|
|
@@ -51,6 +51,12 @@ Output is a fixed JSON shape:
|
|
|
51
51
|
| `--timeout <ms>` | Provider timeout | `180000` |
|
|
52
52
|
| `--provider-bin <path>` | Provider binary path | `agy` / `claude` |
|
|
53
53
|
| `--workdir <path>` | Working directory for the provider | a fresh isolated directory per run |
|
|
54
|
+
| `--extra-body <json>` | JSON merged into the API request body, e.g. `'{"thinking":{"type":"disabled"}}'` | the provider's `extraBody` from the config |
|
|
55
|
+
|
|
56
|
+
`--extra-body` is how vendor-specific knobs get through, turning thinking off
|
|
57
|
+
being the common one. It applies to the three API providers and replaces the
|
|
58
|
+
configured `extraBody` for that run. Per-vendor spellings and the fields it
|
|
59
|
+
refuses to touch are in [Configuration](configure.md).
|
|
54
60
|
|
|
55
61
|
The default `-m` model depends on the provider:
|
|
56
62
|
|
|
@@ -10,7 +10,7 @@ Read this when the user asks how to set up, configure, or switch ModLens provide
|
|
|
10
10
|
modlens config init # write a starter config (refuses to overwrite; --force to redo)
|
|
11
11
|
modlens config show # effective file, API keys masked
|
|
12
12
|
modlens config set provider <name> # change the default provider
|
|
13
|
-
modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model
|
|
13
|
+
modlens config set <provider>.<field> <value> # fields: apiKey, baseUrl, model, extraBody
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
`config set` writes the file with 0600 permissions.
|
|
@@ -32,7 +32,8 @@ Everything lives under two top-level keys, both optional. A missing file means a
|
|
|
32
32
|
"openai": {
|
|
33
33
|
"apiKey": "sk-...",
|
|
34
34
|
"baseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
35
|
-
"model": "qwen3.6-27b"
|
|
35
|
+
"model": "qwen3.6-27b",
|
|
36
|
+
"extraBody": { "thinking": { "type": "disabled" } }
|
|
36
37
|
},
|
|
37
38
|
"anthropic": { "apiKey": "sk-ant-..." },
|
|
38
39
|
"claude-cli": { "model": "haiku" }
|
|
@@ -43,7 +44,8 @@ Everything lives under two top-level keys, both optional. A missing file means a
|
|
|
43
44
|
Field semantics:
|
|
44
45
|
|
|
45
46
|
- `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 means `antigravity-cli`.
|
|
46
|
-
- `providers.<name>.<field>`:
|
|
47
|
+
- `providers.<name>.<field>`: four fields exist, `apiKey`, `baseUrl`, `model`, and `extraBody`. 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.
|
|
48
|
+
- `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 the schema enforcement are refused with an error naming the field. 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`.
|
|
47
49
|
- Environment variables override the file for these bindings: `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OPENAI_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`. Nothing else is read from the environment except `MODLENS_HARNESS` (paste-recovery scope, unrelated to this file).
|
|
48
50
|
- Unknown top-level keys and unknown provider names are ignored rather than rejected, so a typo fails quiet: run `modlens doctor` after hand-editing, it shows which file and env values are actually in effect.
|
|
49
51
|
|
|
@@ -105,6 +107,37 @@ Rides an existing `claude` sign-in, so it costs the user's Claude subscription q
|
|
|
105
107
|
modlens config set provider claude-cli # make it the default if the user wants
|
|
106
108
|
```
|
|
107
109
|
|
|
110
|
+
## Turning thinking off
|
|
111
|
+
|
|
112
|
+
A reasoning model spends its thinking budget before it answers. Reading text out of an image needs none of that, so on a model that thinks by default the run is slower and more expensive for nothing. Every vendor names the switch differently, and there is no portable one, so modlens sends whatever you put in `extraBody` and leaves the naming to the vendor's own docs.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
modlens config set openai.extraBody '{"thinking":{"type":"disabled"}}' # persist it
|
|
116
|
+
modlens -i shot.png --extra-body '{"thinking":{"type":"disabled"}}' # one run only
|
|
117
|
+
modlens config set openai.extraBody '' # clear it
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`--extra-body` replaces the stored object for that run rather than merging into it.
|
|
121
|
+
|
|
122
|
+
Known spellings, current as of August 2026:
|
|
123
|
+
|
|
124
|
+
| Endpoint | Field to send |
|
|
125
|
+
| :-- | :-- |
|
|
126
|
+
| MiMo official API (`api.xiaomimimo.com/v1`) | `{"thinking":{"type":"disabled"}}` |
|
|
127
|
+
| MiMo Responses-format route | `{"reasoning":{"effort":"none"}}` |
|
|
128
|
+
| Qwen, GLM, MiMo and friends self-hosted on vLLM or SGLang | `{"chat_template_kwargs":{"enable_thinking":false}}` |
|
|
129
|
+
| OpenAI-style gateways that accept an effort level | `{"reasoning_effort":"low"}` |
|
|
130
|
+
| `gemini-api`, Gemini 3 family | `{"generationConfig":{"thinkingConfig":{"thinkingLevel":"LOW"}}}` |
|
|
131
|
+
| `gemini-api`, Gemini 2.5 Flash and Flash Lite | `{"generationConfig":{"thinkingConfig":{"thinkingBudget":0}}}` |
|
|
132
|
+
| `anthropic` | nothing to do, thinking is off unless it is asked for |
|
|
133
|
+
|
|
134
|
+
Three things that bite:
|
|
135
|
+
|
|
136
|
+
- Not every model can turn it off. Gemini 3 Pro and Gemini 2.5 Pro have no off switch, only a lower level. Some models ignore an effort field entirely and think anyway.
|
|
137
|
+
- Strict clouds (Groq and Cerebras among them) reject fields they do not recognize with a 400. If a request that worked before now fails with a 400 naming your field, that gateway wants a different spelling, not this one.
|
|
138
|
+
- Others accept an unknown field and quietly ignore it, so check that it took effect instead of assuming. Compare `meta.durationSeconds` and the token counts in `meta.usage` against a run without `extraBody`. If neither moved, the field did not land.
|
|
139
|
+
- A weaker model may need its thinking to fill the schema. Measured on one flowchart: `gemini-3.6-flash` at `thinkingLevel: LOW` came back in 5.7s instead of 12s with the same regions and the same transcription, but `qwen3.6-27b` on DashScope with `enable_thinking: false` started omitting the required `type` on layout regions, which modlens rejects rather than passing off as evidence. If shape errors appear right after you turn thinking off, that is the trade, so turn it back on for that model or move to a route with server-side schema enforcement.
|
|
140
|
+
|
|
108
141
|
## Choosing a provider for the user
|
|
109
142
|
|
|
110
143
|
- Wants zero setup and free: `antigravity-cli` (needs agy sign-in, 15-40s per image).
|
|
@@ -125,4 +158,6 @@ chains, and the result's `meta.attempts` shows what a run actually tried.
|
|
|
125
158
|
- `Provider CLI not found: agy`: install Antigravity CLI or switch provider.
|
|
126
159
|
- `Claude CLI reported ...` or empty result: check `claude` login state.
|
|
127
160
|
- openai route `does not match the vision schema`: retry once, then switch to gemini-api or anthropic.
|
|
161
|
+
- `extraBody cannot override "<field>"`: that field carries the image, the prompt, or the schema. Drop it from the object and keep the vendor knobs.
|
|
162
|
+
- A 400 that names a field you set in `extraBody`: that gateway does not know it. See the thinking section above for the other spellings.
|
|
128
163
|
- `config init` refusing to run: the file exists; use `modlens config show` first, `--force` only if the user agrees to overwrite.
|
|
@@ -24,7 +24,7 @@ $ErrorActionPreference = 'Stop'
|
|
|
24
24
|
# package.json version, and the release script rewrites it on every bump.
|
|
25
25
|
$Package = '@liustack/modlens'
|
|
26
26
|
$Bin = 'modlens'
|
|
27
|
-
$Pinned = '3.
|
|
27
|
+
$Pinned = '3.4.0'
|
|
28
28
|
# -------------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
$NativeNote = 'no native artifact is published for this tool yet; phase A ships npm launch paths only'
|
|
@@ -22,7 +22,7 @@ set -eu
|
|
|
22
22
|
# package.json version, and the release script rewrites it on every bump.
|
|
23
23
|
PKG="@liustack/modlens"
|
|
24
24
|
BIN="modlens"
|
|
25
|
-
PINNED="3.
|
|
25
|
+
PINNED="3.4.0"
|
|
26
26
|
# -------------------------------------------------------------------------------
|
|
27
27
|
|
|
28
28
|
NATIVE_NOTE="no native artifact is published for this tool yet; phase A ships npm launch paths only"
|