@liustack/modlens 3.2.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 +10 -0
- package/README.md +11 -0
- package/README.zh-CN.md +11 -0
- package/dist/main.js +386 -163
- package/docs/security.md +1 -1
- package/docs/troubleshooting.md +17 -0
- package/package.json +1 -1
- package/skills/modlens/SKILL.md +9 -5
- package/skills/modlens/references/cli.md +6 -0
- package/skills/modlens/references/configure.md +45 -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,15 @@
|
|
|
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
|
+
|
|
8
|
+
## 3.3.0 - 2026-08-07
|
|
9
|
+
|
|
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.
|
|
11
|
+
- Behavior change: `config set provider <name>` is now a preference, not a pin. It moves that provider to the front of its allowed region (for a remote URL an agent still stays behind the inline providers), and the rest of the chain backs it up on failure, matching modsearch's engine setting. To pin exactly one provider with no fallback, pass `-p <name>`, which keeps its original error when it fails.
|
|
12
|
+
|
|
3
13
|
## 3.2.0 - 2026-08-07
|
|
4
14
|
|
|
5
15
|
- A remote image URL with no explicit `-p` now runs on `gemini-api` whenever a Gemini key is configured, even if the default provider is an agent. The inline path downloads the image itself, behind the private-address guards, the magic-byte image check, and the 25 MB cap; an agent fetching the URL on its own passes through none of those. Without a Gemini key the run stays on the configured default, a local image never reroutes, and an explicit `-p` always wins.
|
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 = {
|
|
@@ -28053,9 +28156,6 @@ function loadConfigFile(configPath = CONFIG_PATH) {
|
|
|
28053
28156
|
);
|
|
28054
28157
|
}
|
|
28055
28158
|
}
|
|
28056
|
-
function defaultProviderName(config2) {
|
|
28057
|
-
return config2.provider?.trim() || "antigravity-cli";
|
|
28058
|
-
}
|
|
28059
28159
|
function resolveProviderSettings(providerName, config2, env = process.env) {
|
|
28060
28160
|
const aliasNames = Object.entries(providerAliases()).filter(([alias, canonical]) => canonical === providerName && alias !== providerName).map(([alias]) => alias);
|
|
28061
28161
|
const fromFile = {
|
|
@@ -28080,17 +28180,31 @@ function setConfigValue(dottedKey, value, configPath = CONFIG_PATH) {
|
|
|
28080
28180
|
const dot = dottedKey.indexOf(".");
|
|
28081
28181
|
if (dot <= 0 || dot === dottedKey.length - 1) {
|
|
28082
28182
|
throw new Error(
|
|
28083
|
-
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model>".`
|
|
28183
|
+
`Invalid config key: ${dottedKey}. Use "provider" or "<provider>.<apiKey|baseUrl|model|extraBody>".`
|
|
28084
28184
|
);
|
|
28085
28185
|
}
|
|
28086
28186
|
const providerName = dottedKey.slice(0, dot);
|
|
28087
28187
|
const field = dottedKey.slice(dot + 1);
|
|
28088
|
-
if (
|
|
28089
|
-
|
|
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;
|
|
28090
28207
|
}
|
|
28091
|
-
config2.providers ??= {};
|
|
28092
|
-
config2.providers[providerName] ??= {};
|
|
28093
|
-
config2.providers[providerName][field] = value;
|
|
28094
28208
|
}
|
|
28095
28209
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
28096
28210
|
fs.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
|
|
@@ -28129,7 +28243,7 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28129
28243
|
const fileSettings = config2.providers?.[name] ?? {};
|
|
28130
28244
|
const bindings = ENV_BINDINGS[name] ?? {};
|
|
28131
28245
|
const fields = {};
|
|
28132
|
-
for (const field of
|
|
28246
|
+
for (const field of STRING_FIELDS) {
|
|
28133
28247
|
const envName = bindings[field];
|
|
28134
28248
|
const envValue = envName ? env[envName]?.trim() : void 0;
|
|
28135
28249
|
const value = envValue ?? fileSettings[field];
|
|
@@ -28139,6 +28253,9 @@ function renderEffectiveConfig(config2, env = process.env) {
|
|
|
28139
28253
|
fields[field] = `${shown} (${source})`;
|
|
28140
28254
|
}
|
|
28141
28255
|
}
|
|
28256
|
+
if (fileSettings.extraBody !== void 0) {
|
|
28257
|
+
fields.extraBody = `${JSON.stringify(fileSettings.extraBody)} (file)`;
|
|
28258
|
+
}
|
|
28142
28259
|
if (Object.keys(fields).length > 0) {
|
|
28143
28260
|
providers[name] = fields;
|
|
28144
28261
|
}
|
|
@@ -28157,32 +28274,184 @@ function maskKey(key) {
|
|
|
28157
28274
|
}
|
|
28158
28275
|
return `${key.slice(0, 6)}...${key.slice(-2)}`;
|
|
28159
28276
|
}
|
|
28277
|
+
const PROVIDER_DESCRIPTORS = [
|
|
28278
|
+
{
|
|
28279
|
+
name: "antigravity-cli",
|
|
28280
|
+
kind: "subprocess",
|
|
28281
|
+
bin: "agy",
|
|
28282
|
+
install: "curl -fsSL https://antigravity.google/cli/install.sh | bash && agy # sign in, then exit"
|
|
28283
|
+
},
|
|
28284
|
+
{
|
|
28285
|
+
name: "gemini-api",
|
|
28286
|
+
kind: "api",
|
|
28287
|
+
required: [{ field: "apiKey", env: "GEMINI_API_KEY" }],
|
|
28288
|
+
fix: "modlens config set gemini-api.apiKey <key> # free key: https://aistudio.google.com"
|
|
28289
|
+
},
|
|
28290
|
+
{
|
|
28291
|
+
name: "openai",
|
|
28292
|
+
kind: "api",
|
|
28293
|
+
required: [
|
|
28294
|
+
{ field: "baseUrl", env: "OPENAI_BASE_URL" },
|
|
28295
|
+
{ field: "apiKey", env: "OPENAI_API_KEY" },
|
|
28296
|
+
{ field: "model" }
|
|
28297
|
+
],
|
|
28298
|
+
fix: "modlens config set openai.baseUrl <url> / openai.apiKey <key> / openai.model <name>"
|
|
28299
|
+
},
|
|
28300
|
+
{
|
|
28301
|
+
name: "anthropic",
|
|
28302
|
+
kind: "api",
|
|
28303
|
+
required: [{ field: "apiKey", env: "ANTHROPIC_API_KEY" }],
|
|
28304
|
+
fix: "modlens config set anthropic.apiKey <key>"
|
|
28305
|
+
},
|
|
28306
|
+
{
|
|
28307
|
+
name: "claude-cli",
|
|
28308
|
+
kind: "subprocess",
|
|
28309
|
+
bin: "claude",
|
|
28310
|
+
install: "install the Claude Code CLI, then run `claude` once to sign in"
|
|
28311
|
+
}
|
|
28312
|
+
];
|
|
28313
|
+
function findOnPath(bin, env) {
|
|
28314
|
+
const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
28315
|
+
for (const dir of dirs) {
|
|
28316
|
+
const full = path.join(dir, bin);
|
|
28317
|
+
try {
|
|
28318
|
+
if (fs.statSync(full).isFile()) {
|
|
28319
|
+
return full;
|
|
28320
|
+
}
|
|
28321
|
+
} catch {
|
|
28322
|
+
}
|
|
28323
|
+
}
|
|
28324
|
+
return null;
|
|
28325
|
+
}
|
|
28326
|
+
function providerAvailable(name, config2, env = process.env) {
|
|
28327
|
+
const descriptor = PROVIDER_DESCRIPTORS.find((d) => d.name === name);
|
|
28328
|
+
if (!descriptor) {
|
|
28329
|
+
return false;
|
|
28330
|
+
}
|
|
28331
|
+
if (descriptor.kind === "subprocess") {
|
|
28332
|
+
return findOnPath(descriptor.bin, env) !== null;
|
|
28333
|
+
}
|
|
28334
|
+
const settings = resolveProviderSettings(name, config2, env);
|
|
28335
|
+
return (descriptor.required ?? []).every((req) => Boolean(settings[req.field]?.trim()));
|
|
28336
|
+
}
|
|
28337
|
+
const LOCAL_FAILOVER_ORDER = [
|
|
28338
|
+
"antigravity-cli",
|
|
28339
|
+
"gemini-api",
|
|
28340
|
+
"openai",
|
|
28341
|
+
"anthropic",
|
|
28342
|
+
"claude-cli"
|
|
28343
|
+
];
|
|
28344
|
+
const REMOTE_FAILOVER_ORDER = ["gemini-api", "openai", "anthropic", "antigravity-cli"];
|
|
28345
|
+
function providerChain(kind, config2, env = process.env) {
|
|
28346
|
+
const names = [...kind === "remote" ? REMOTE_FAILOVER_ORDER : LOCAL_FAILOVER_ORDER];
|
|
28347
|
+
const preferred = config2.provider?.trim();
|
|
28348
|
+
if (preferred) {
|
|
28349
|
+
let canonical = null;
|
|
28350
|
+
try {
|
|
28351
|
+
canonical = resolveProvider(preferred).name;
|
|
28352
|
+
} catch {
|
|
28353
|
+
canonical = null;
|
|
28354
|
+
}
|
|
28355
|
+
const index = canonical ? names.indexOf(canonical) : -1;
|
|
28356
|
+
if (index > 0 && canonical) {
|
|
28357
|
+
const isAgent = Boolean(resolveProvider(canonical).isolateWorkdir);
|
|
28358
|
+
if (kind === "local" || !isAgent) {
|
|
28359
|
+
names.splice(index, 1);
|
|
28360
|
+
names.unshift(canonical);
|
|
28361
|
+
}
|
|
28362
|
+
}
|
|
28363
|
+
}
|
|
28364
|
+
return names.filter((name) => providerAvailable(name, config2, env)).map((name) => resolveProvider(name));
|
|
28365
|
+
}
|
|
28160
28366
|
const DEFAULT_TIMEOUT_MS = 18e4;
|
|
28161
28367
|
const KILL_GRACE_MS = 3e4;
|
|
28162
28368
|
const DRAIN_GRACE_MS = 500;
|
|
28163
28369
|
const SIGKILL_GRACE_MS = 2e3;
|
|
28164
|
-
function chooseProviderName(requested, config2, kind, env = process.env) {
|
|
28165
|
-
const name = requested || defaultProviderName(config2);
|
|
28166
|
-
if (requested || kind !== "remote") {
|
|
28167
|
-
return name;
|
|
28168
|
-
}
|
|
28169
|
-
if (!resolveProvider(name).isolateWorkdir) {
|
|
28170
|
-
return name;
|
|
28171
|
-
}
|
|
28172
|
-
return resolveProviderSettings("gemini-api", config2, env).apiKey ? "gemini-api" : name;
|
|
28173
|
-
}
|
|
28174
28370
|
async function analyzeImage(options) {
|
|
28175
28371
|
const resolvedInput = resolveInput(options.input);
|
|
28176
28372
|
if (resolvedInput.kind === "local") {
|
|
28177
28373
|
validateInputFile(resolvedInput.source);
|
|
28178
28374
|
}
|
|
28179
28375
|
const config2 = options.config ?? loadConfigFile();
|
|
28180
|
-
const
|
|
28181
|
-
|
|
28182
|
-
|
|
28183
|
-
|
|
28376
|
+
const chain = options.provider ? [resolveProvider(options.provider)] : options.providerBin ? [resolveProvider("antigravity-cli")] : providerChain(resolvedInput.kind, config2);
|
|
28377
|
+
if (chain.length === 0) {
|
|
28378
|
+
throw new Error(
|
|
28379
|
+
"No vision provider is set up on this machine. Install Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash, then run agy once to sign in), or configure a key: modlens config set gemini-api.apiKey <key>. Run modlens doctor for the full picture."
|
|
28380
|
+
);
|
|
28381
|
+
}
|
|
28184
28382
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
28185
|
-
const
|
|
28383
|
+
const attempts = [];
|
|
28384
|
+
const warnings = [];
|
|
28385
|
+
let lastError;
|
|
28386
|
+
for (const provider of chain) {
|
|
28387
|
+
const startedAt = Date.now();
|
|
28388
|
+
const model = (attempts.length === 0 ? options.model : void 0) || resolveProviderSettings(provider.name, config2).model || provider.defaultModel;
|
|
28389
|
+
try {
|
|
28390
|
+
const parsed = await runProvider(
|
|
28391
|
+
provider,
|
|
28392
|
+
model,
|
|
28393
|
+
options,
|
|
28394
|
+
resolvedInput,
|
|
28395
|
+
timeoutMs,
|
|
28396
|
+
config2,
|
|
28397
|
+
warnings
|
|
28398
|
+
);
|
|
28399
|
+
attempts.push({
|
|
28400
|
+
provider: provider.name,
|
|
28401
|
+
ok: true,
|
|
28402
|
+
durationSeconds: (Date.now() - startedAt) / 1e3
|
|
28403
|
+
});
|
|
28404
|
+
if (attempts.length > 1) {
|
|
28405
|
+
const failed = attempts.slice(0, -1);
|
|
28406
|
+
warnings.push(
|
|
28407
|
+
`Failed over to ${provider.name} after: ${failed.map((attempt) => `${attempt.provider} (${attempt.error})`).join("; ")}.`
|
|
28408
|
+
);
|
|
28409
|
+
if (options.model) {
|
|
28410
|
+
warnings.push(
|
|
28411
|
+
`The explicit model applied to ${failed[0].provider} only; ${provider.name} ran its own default.`
|
|
28412
|
+
);
|
|
28413
|
+
}
|
|
28414
|
+
}
|
|
28415
|
+
return {
|
|
28416
|
+
image: resolvedInput.source,
|
|
28417
|
+
provider: provider.name,
|
|
28418
|
+
result: parsed.result,
|
|
28419
|
+
meta: {
|
|
28420
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28421
|
+
model,
|
|
28422
|
+
conversationId: parsed.meta.conversationId,
|
|
28423
|
+
durationSeconds: parsed.meta.durationSeconds,
|
|
28424
|
+
usage: parsed.meta.usage,
|
|
28425
|
+
attempts,
|
|
28426
|
+
warnings
|
|
28427
|
+
}
|
|
28428
|
+
};
|
|
28429
|
+
} catch (error) {
|
|
28430
|
+
lastError = error;
|
|
28431
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28432
|
+
attempts.push({
|
|
28433
|
+
provider: provider.name,
|
|
28434
|
+
ok: false,
|
|
28435
|
+
durationSeconds: (Date.now() - startedAt) / 1e3,
|
|
28436
|
+
error: message.slice(0, 300)
|
|
28437
|
+
});
|
|
28438
|
+
}
|
|
28439
|
+
}
|
|
28440
|
+
if (chain.length === 1) {
|
|
28441
|
+
throw lastError;
|
|
28442
|
+
}
|
|
28443
|
+
throw new Error(
|
|
28444
|
+
`Every configured vision provider failed for this image. ${attempts.map((attempt) => `${attempt.provider}: ${attempt.error}`).join(" | ")}`
|
|
28445
|
+
);
|
|
28446
|
+
}
|
|
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
|
+
}
|
|
28186
28455
|
const providerOptions = {
|
|
28187
28456
|
imageSource: resolvedInput.source,
|
|
28188
28457
|
imageKind: resolvedInput.kind,
|
|
@@ -28228,18 +28497,7 @@ async function analyzeImage(options) {
|
|
|
28228
28497
|
`${provider.name} returned a result that does not match the vision schema (missing: ${missing.join(", ")}).`
|
|
28229
28498
|
);
|
|
28230
28499
|
}
|
|
28231
|
-
return
|
|
28232
|
-
image: resolvedInput.source,
|
|
28233
|
-
provider: provider.name,
|
|
28234
|
-
result: parsed.result,
|
|
28235
|
-
meta: {
|
|
28236
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28237
|
-
model,
|
|
28238
|
-
conversationId: parsed.meta.conversationId,
|
|
28239
|
-
durationSeconds: parsed.meta.durationSeconds,
|
|
28240
|
-
usage: parsed.meta.usage
|
|
28241
|
-
}
|
|
28242
|
-
};
|
|
28500
|
+
return parsed;
|
|
28243
28501
|
}
|
|
28244
28502
|
function resolveInput(input) {
|
|
28245
28503
|
const trimmed = input.trim();
|
|
@@ -28449,42 +28707,6 @@ function detectHarness() {
|
|
|
28449
28707
|
return detectHarnessDetailed().harness;
|
|
28450
28708
|
}
|
|
28451
28709
|
const MIN_NODE = "22.13";
|
|
28452
|
-
const DESCRIPTORS = [
|
|
28453
|
-
{
|
|
28454
|
-
name: "antigravity-cli",
|
|
28455
|
-
kind: "subprocess",
|
|
28456
|
-
bin: "agy",
|
|
28457
|
-
install: "curl -fsSL https://antigravity.google/cli/install.sh | bash && agy # sign in, then exit"
|
|
28458
|
-
},
|
|
28459
|
-
{
|
|
28460
|
-
name: "gemini-api",
|
|
28461
|
-
kind: "api",
|
|
28462
|
-
required: [{ field: "apiKey", env: "GEMINI_API_KEY" }],
|
|
28463
|
-
fix: "modlens config set gemini-api.apiKey <key> # free key: https://aistudio.google.com"
|
|
28464
|
-
},
|
|
28465
|
-
{
|
|
28466
|
-
name: "openai",
|
|
28467
|
-
kind: "api",
|
|
28468
|
-
required: [
|
|
28469
|
-
{ field: "baseUrl", env: "OPENAI_BASE_URL" },
|
|
28470
|
-
{ field: "apiKey", env: "OPENAI_API_KEY" },
|
|
28471
|
-
{ field: "model" }
|
|
28472
|
-
],
|
|
28473
|
-
fix: "modlens config set openai.baseUrl <url> / openai.apiKey <key> / openai.model <name>"
|
|
28474
|
-
},
|
|
28475
|
-
{
|
|
28476
|
-
name: "anthropic",
|
|
28477
|
-
kind: "api",
|
|
28478
|
-
required: [{ field: "apiKey", env: "ANTHROPIC_API_KEY" }],
|
|
28479
|
-
fix: "modlens config set anthropic.apiKey <key>"
|
|
28480
|
-
},
|
|
28481
|
-
{
|
|
28482
|
-
name: "claude-cli",
|
|
28483
|
-
kind: "subprocess",
|
|
28484
|
-
bin: "claude",
|
|
28485
|
-
install: "install the Claude Code CLI, then run `claude` once to sign in"
|
|
28486
|
-
}
|
|
28487
|
-
];
|
|
28488
28710
|
function versionParts(version) {
|
|
28489
28711
|
const match = /(\d+)\.(\d+)/.exec(version.replace(/^v/, ""));
|
|
28490
28712
|
if (!match) {
|
|
@@ -28497,19 +28719,6 @@ function meetsMinimum(version, minimum) {
|
|
|
28497
28719
|
const [minMajor, minMinor] = versionParts(minimum);
|
|
28498
28720
|
return major > minMajor || major === minMajor && minor >= minMinor;
|
|
28499
28721
|
}
|
|
28500
|
-
function findOnPath(bin, env) {
|
|
28501
|
-
const dirs = (env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
28502
|
-
for (const dir of dirs) {
|
|
28503
|
-
const full = path.join(dir, bin);
|
|
28504
|
-
try {
|
|
28505
|
-
if (fs.statSync(full).isFile()) {
|
|
28506
|
-
return full;
|
|
28507
|
-
}
|
|
28508
|
-
} catch {
|
|
28509
|
-
}
|
|
28510
|
-
}
|
|
28511
|
-
return null;
|
|
28512
|
-
}
|
|
28513
28722
|
function checkNodeSqlite() {
|
|
28514
28723
|
const realEmit = process.emitWarning;
|
|
28515
28724
|
process.emitWarning = () => {
|
|
@@ -28617,8 +28826,12 @@ function buildDoctorReport(input) {
|
|
|
28617
28826
|
meetsMinimum: meetsMinimum(process.version, MIN_NODE)
|
|
28618
28827
|
},
|
|
28619
28828
|
nodeSqlite: checkNodeSqlite(),
|
|
28620
|
-
providers:
|
|
28829
|
+
providers: PROVIDER_DESCRIPTORS.map((d) => inspectProvider(d, input.config, env)),
|
|
28621
28830
|
selection: resolveSelection(input.config, input.providerFlag),
|
|
28831
|
+
chains: {
|
|
28832
|
+
local: providerChain("local", input.config, env).map((p) => p.name),
|
|
28833
|
+
remote: providerChain("remote", input.config, env).map((p) => p.name)
|
|
28834
|
+
},
|
|
28622
28835
|
harness: (() => {
|
|
28623
28836
|
const detection = detectHarnessDetailed();
|
|
28624
28837
|
return { detected: detection.harness, source: detection.source };
|
|
@@ -28653,6 +28866,11 @@ function renderDoctorReport(report) {
|
|
|
28653
28866
|
lines.push(` ${report.selection.provider}${canonicalNote}`);
|
|
28654
28867
|
lines.push(` reason: ${report.selection.reason}`);
|
|
28655
28868
|
lines.push("");
|
|
28869
|
+
lines.push("Failover chains (what a run tries, in order)");
|
|
28870
|
+
const chainLine = (chain) => chain.length > 0 ? chain.join(" -> ") : "(none available)";
|
|
28871
|
+
lines.push(` local: ${chainLine(report.chains.local)}`);
|
|
28872
|
+
lines.push(` remote: ${chainLine(report.chains.remote)}`);
|
|
28873
|
+
lines.push("");
|
|
28656
28874
|
lines.push("Harness");
|
|
28657
28875
|
lines.push(
|
|
28658
28876
|
report.harness.detected ? ` ${report.harness.detected} (via ${report.harness.source})` : ` none detected (${report.harness.source})`
|
|
@@ -29116,8 +29334,11 @@ function recoverPastedImages(options = {}) {
|
|
|
29116
29334
|
return result;
|
|
29117
29335
|
}
|
|
29118
29336
|
const program = new Command();
|
|
29119
|
-
program.name("modlens").description("Plug-in vision for text-only LLMs: image in, structured JSON evidence out").version("3.
|
|
29120
|
-
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) => {
|
|
29121
29342
|
try {
|
|
29122
29343
|
const timeoutMs = Number.parseInt(options.timeout, 10);
|
|
29123
29344
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
@@ -29130,7 +29351,8 @@ program.command("analyze", { isDefault: true }).description("Analyze an image in
|
|
|
29130
29351
|
prompt: options.prompt,
|
|
29131
29352
|
timeoutMs,
|
|
29132
29353
|
providerBin: options.providerBin,
|
|
29133
|
-
workdir: options.workdir
|
|
29354
|
+
workdir: options.workdir,
|
|
29355
|
+
extraBody: options.extraBody ? parseExtraBody(options.extraBody, "--extra-body") : void 0
|
|
29134
29356
|
});
|
|
29135
29357
|
const output = JSON.stringify(result, null, 2);
|
|
29136
29358
|
if (options.output) {
|
|
@@ -29211,6 +29433,7 @@ config.command("init").description(`Create a starter config at ${CONFIG_PATH}`).
|
|
|
29211
29433
|
"Everything is optional. Two things you can set:",
|
|
29212
29434
|
" modlens config set provider <name> which provider analyzes images",
|
|
29213
29435
|
" modlens config set <provider>.<apiKey|baseUrl|model> <value> provider credentials",
|
|
29436
|
+
` modlens config set <provider>.extraBody '{"thinking":{"type":"disabled"}}' vendor request fields`,
|
|
29214
29437
|
""
|
|
29215
29438
|
].join("\n")
|
|
29216
29439
|
);
|
package/docs/security.md
CHANGED
|
@@ -22,7 +22,7 @@ The `claude-cli` provider runs with `--allowedTools Read` only, so it can read l
|
|
|
22
22
|
|
|
23
23
|
Both subprocess providers also run in a throwaway directory created fresh per call and removed afterward. For a local image it holds a private copy of that one image and nothing else, and it is a real copy, never a hardlink, so a provider writing to its temp path cannot touch the original. For a remote image the directory is empty and the agent downloads into it. Without this, text inside an image could steer a broadly-permissioned agent into reading files next to the original, or whatever project the caller happened to be in. Passing `--workdir` opts out and runs where you point it.
|
|
24
24
|
|
|
25
|
-
This is exposure reduction, not an OS sandbox: the agent can still read absolute paths, reach the network, and spawn processes. Treat it as a narrower default, not a security boundary. For images you do not trust, prefer an inline API provider (`-p gemini-api`), which hands the bytes to an HTTP endpoint and runs no local agent. Remote URLs already
|
|
25
|
+
This is exposure reduction, not an OS sandbox: the agent can still read absolute paths, reach the network, and spawn processes. Treat it as a narrower default, not a security boundary. For images you do not trust, prefer an inline API provider (`-p gemini-api`), which hands the bytes to an HTTP endpoint and runs no local agent. Remote URLs already prefer that path: the failover chain for a remote URL tries the inline API providers first and the agent last, because the inline path downloads the image itself, behind the private-address guards, the magic-byte image check, and the size cap, none of which apply when an agent fetches the URL on its own. An explicit `-p` pins one provider and overrides the chain.
|
|
26
26
|
|
|
27
27
|
## Image content is untrusted input
|
|
28
28
|
|
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.
|
|
@@ -56,7 +56,9 @@ modlens config show
|
|
|
56
56
|
|
|
57
57
|
`modlens config init` writes a starter config to `~/.modlens/config.json` when none exists. Full setup recipes per provider: `references/configure.md`.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Failover is automatic: a run 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` first, then `gemini-api`, `openai`, `anthropic`, `claude-cli`. A remote URL tries the inline API providers first (`gemini-api`, `openai`, `anthropic`) and the agent last, because only the inline download path runs the private-address guards, the magic-byte image check, and the size cap. A provider set with `config set provider <name>` is a preference that moves to the front of its allowed region, not a pin. An explicit `-p` pins exactly one provider with no fallback.
|
|
60
|
+
|
|
61
|
+
In the result, the top-level `provider` names who actually answered, `meta.attempts` lists every provider tried with timings and failure reasons, and `meta.warnings` carries failover notices. Relay a failover warning when the answer's provider surprised the user.
|
|
60
62
|
|
|
61
63
|
## Command
|
|
62
64
|
|
|
@@ -76,6 +78,8 @@ modlens -i <image> -o <output.json> -m <model> --prompt "<extra focus>" --timeou
|
|
|
76
78
|
|
|
77
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`.
|
|
78
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
|
+
|
|
79
83
|
## Finding the image path in the chat
|
|
80
84
|
|
|
81
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).
|
|
@@ -112,10 +145,19 @@ modlens config set provider claude-cli # make it the default if the user wants
|
|
|
112
145
|
- Already pays for Claude: `claude-cli` (no extra key) or `anthropic` (API billing).
|
|
113
146
|
- Has a favorite multimodal endpoint (qwen, GLM, ...): `openai`.
|
|
114
147
|
|
|
148
|
+
Every configured provider also backs up the others: a run tries them in a
|
|
149
|
+
fixed order (local images agent-first; remote URLs inline-API-first, agent
|
|
150
|
+
last) and fails over on an error, a timeout, or a schema-violating result.
|
|
151
|
+
`config set provider <name>` moves a provider to the front of its allowed
|
|
152
|
+
region; `-p <name>` pins exactly one with no fallback. `doctor` prints the
|
|
153
|
+
chains, and the result's `meta.attempts` shows what a run actually tried.
|
|
154
|
+
|
|
115
155
|
## Troubleshooting
|
|
116
156
|
|
|
117
157
|
- Error names a missing env var or `config set` command: run exactly that.
|
|
118
158
|
- `Provider CLI not found: agy`: install Antigravity CLI or switch provider.
|
|
119
159
|
- `Claude CLI reported ...` or empty result: check `claude` login state.
|
|
120
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.
|
|
121
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"
|