scoutline 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/quota.d.ts +9 -4
- package/dist/commands/quota.d.ts.map +1 -1
- package/dist/commands/quota.js +29 -17
- package/dist/commands/quota.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/research-state.d.ts +104 -0
- package/dist/lib/research-state.d.ts.map +1 -0
- package/dist/lib/research-state.js +214 -0
- package/dist/lib/research-state.js.map +1 -0
- package/dist/providers/minimax/sdk-client.d.ts +29 -0
- package/dist/providers/minimax/sdk-client.d.ts.map +1 -0
- package/dist/providers/minimax/sdk-client.js +50 -0
- package/dist/providers/minimax/sdk-client.js.map +1 -0
- package/dist/providers/zai/adapter.d.ts.map +1 -1
- package/dist/providers/zai/adapter.js +176 -24
- package/dist/providers/zai/adapter.js.map +1 -1
- package/dist/providers/zai/media.d.ts +13 -0
- package/dist/providers/zai/media.d.ts.map +1 -1
- package/dist/providers/zai/media.js +115 -1
- package/dist/providers/zai/media.js.map +1 -1
- package/dist/providers/zai/repository.d.ts.map +1 -1
- package/dist/providers/zai/repository.js +46 -21
- package/dist/providers/zai/repository.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research state file — resume mechanism for interrupted research
|
|
3
|
+
* (tech-plan §3, T07).
|
|
4
|
+
*
|
|
5
|
+
* Research costs 4-250 credits per request. A research task runs
|
|
6
|
+
* asynchronously server-side: POST /research creates it and returns a
|
|
7
|
+
* `request_id`, then GET /research/{id} polls until completion. If the
|
|
8
|
+
* CLI exits (Ctrl-C, crash) mid-poll, the task keeps running and
|
|
9
|
+
* consuming credits. Without a persistence mechanism, the next identical
|
|
10
|
+
* request would POST a SECOND task — a double charge.
|
|
11
|
+
*
|
|
12
|
+
* This module persists `{ requestId, identityHash, createdAt, status }`
|
|
13
|
+
* to `~/.scoutline/research/<state-hash>.json` so the next invocation of
|
|
14
|
+
* the same request detects the in-flight task and polls it instead of
|
|
15
|
+
* creating a new one. The state-hash is deterministic for a given
|
|
16
|
+
* `{provider, capability, credentialFingerprint, request}` tuple (see
|
|
17
|
+
* `computeResearchStateHash`).
|
|
18
|
+
*
|
|
19
|
+
* Boundary rules (ARCHITECTURE.md §2):
|
|
20
|
+
* - May import cache-root resolution and normalized errors.
|
|
21
|
+
* - Must NOT import transport, command presentation, or a Provider
|
|
22
|
+
* Adapter.
|
|
23
|
+
*
|
|
24
|
+
* Resilience contract (tech-plan §3 / G1-G3):
|
|
25
|
+
* - `write()` uses `{ flag: "wx" }` for atomic creation. A concurrent
|
|
26
|
+
* invocation that finds the file already exists gets EEXIST and
|
|
27
|
+
* polls the existing task instead of creating a new one.
|
|
28
|
+
* - `read()` catches JSON parse errors, deletes the corrupt file, and
|
|
29
|
+
* returns `null` (treated as absent → new task created).
|
|
30
|
+
* - `remove()` deletes the file and ignores ENOENT (already gone).
|
|
31
|
+
*/
|
|
32
|
+
import crypto from "node:crypto";
|
|
33
|
+
import * as fs from "node:fs/promises";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { researchStateDir } from "./cache.js";
|
|
36
|
+
/**
|
|
37
|
+
* Recursively sort object keys so `JSON.stringify` produces a stable
|
|
38
|
+
* representation regardless of insertion order. Mirrors the cache module's
|
|
39
|
+
* `sortKeysDeep` so the same request hashes identically here and in the
|
|
40
|
+
* response-cache key.
|
|
41
|
+
*/
|
|
42
|
+
function sortKeysDeep(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.map((element) => sortKeysDeep(element));
|
|
45
|
+
}
|
|
46
|
+
if (value && typeof value === "object") {
|
|
47
|
+
const input = value;
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const key of Object.keys(input).sort()) {
|
|
50
|
+
out[key] = sortKeysDeep(input[key]);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Compute the deterministic state-file identity hash for a research
|
|
58
|
+
* request (tech-plan §3 / CR3).
|
|
59
|
+
*
|
|
60
|
+
* state-hash = SHA-256(recursively-key-sorted-JSON({
|
|
61
|
+
* provider, capability, credentialFingerprint, request
|
|
62
|
+
* }))
|
|
63
|
+
*
|
|
64
|
+
* Same canonical approach as `buildProviderCacheKey`'s request hashing,
|
|
65
|
+
* extended to include provider + capability + credential. Rotating the
|
|
66
|
+
* API key orphans old state files (correct — the old task belongs to the
|
|
67
|
+
* old key's billing). The hash never contains a raw credential.
|
|
68
|
+
*/
|
|
69
|
+
export function computeResearchStateHash(input) {
|
|
70
|
+
const payload = {
|
|
71
|
+
provider: input.provider,
|
|
72
|
+
capability: input.capability,
|
|
73
|
+
credentialFingerprint: input.credentialFingerprint,
|
|
74
|
+
request: sortKeysDeep(input.request),
|
|
75
|
+
};
|
|
76
|
+
return crypto.createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Production state-file implementation
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
/**
|
|
82
|
+
* Build a production {@link ResearchStateFile} backed by files under
|
|
83
|
+
* `~/.scoutline/research/` (via {@link researchStateDir}). One JSON file
|
|
84
|
+
* per in-flight task, named `<state-hash>.json`.
|
|
85
|
+
*
|
|
86
|
+
* - `write()` atomically creates the file with `{ flag: "wx" }`. A
|
|
87
|
+
* concurrent invocation that finds it exists throws EEXIST; the caller
|
|
88
|
+
* catches that and polls the existing task.
|
|
89
|
+
* - `read()` catches JSON parse errors, deletes the corrupt file, and
|
|
90
|
+
* returns `null`.
|
|
91
|
+
* - `remove()` deletes the file and ignores ENOENT.
|
|
92
|
+
*/
|
|
93
|
+
export function createProductionResearchStateFile() {
|
|
94
|
+
function filePath(identityHash) {
|
|
95
|
+
return path.join(researchStateDir(), `${identityHash}.json`);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
async read(identityHash) {
|
|
99
|
+
if (!identityHash)
|
|
100
|
+
return null;
|
|
101
|
+
const file = filePath(identityHash);
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = await fs.readFile(file, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(raw);
|
|
111
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
112
|
+
await fs.unlink(file).catch(() => { });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const obj = parsed;
|
|
116
|
+
const requestId = obj.requestId;
|
|
117
|
+
const storedHash = obj.identityHash;
|
|
118
|
+
const createdAt = obj.createdAt;
|
|
119
|
+
const status = obj.status;
|
|
120
|
+
if (typeof requestId !== "string" ||
|
|
121
|
+
requestId.length === 0 ||
|
|
122
|
+
typeof storedHash !== "string" ||
|
|
123
|
+
typeof createdAt !== "string" ||
|
|
124
|
+
(status !== "pending" && status !== "in_progress")) {
|
|
125
|
+
await fs.unlink(file).catch(() => { });
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { requestId, identityHash: storedHash, createdAt, status };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Corrupt JSON — delete the file so the next run creates a fresh
|
|
132
|
+
// task instead of forever reading garbage.
|
|
133
|
+
await fs.unlink(file).catch(() => { });
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async write(identityHash, state) {
|
|
138
|
+
const dir = researchStateDir();
|
|
139
|
+
await fs.mkdir(dir, { recursive: true });
|
|
140
|
+
const file = filePath(identityHash);
|
|
141
|
+
const payload = JSON.stringify(state);
|
|
142
|
+
// `{ flag: "wx" }` atomically creates the file only if it does not
|
|
143
|
+
// exist. A concurrent invocation that lost the race gets EEXIST;
|
|
144
|
+
// the caller catches it and polls the existing task instead of
|
|
145
|
+
// creating a second one.
|
|
146
|
+
await fs.writeFile(file, payload, { flag: "wx" });
|
|
147
|
+
},
|
|
148
|
+
async remove(identityHash) {
|
|
149
|
+
const file = filePath(identityHash);
|
|
150
|
+
await fs.unlink(file).catch((err) => {
|
|
151
|
+
// ENOENT is expected (already removed, or never written). Swallow
|
|
152
|
+
// it; any other error re-throws.
|
|
153
|
+
if (err && err.code === "ENOENT")
|
|
154
|
+
return;
|
|
155
|
+
throw err;
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Convenience helper exported for the Adapter and tests: builds an
|
|
162
|
+
* in-memory {@link ResearchStateFile} that throws EEXIST on a second
|
|
163
|
+
* write to the same hash, mirroring the production `{ flag: "wx" }`
|
|
164
|
+
* contract exactly. The adapter's lifecycle must not depend on whether
|
|
165
|
+
* the state file is disk-backed or memory-backed.
|
|
166
|
+
*/
|
|
167
|
+
export function createInMemoryResearchStateFile() {
|
|
168
|
+
const store = new Map();
|
|
169
|
+
return {
|
|
170
|
+
store,
|
|
171
|
+
async read(identityHash) {
|
|
172
|
+
const raw = store.get(identityHash);
|
|
173
|
+
if (raw === undefined)
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(raw);
|
|
177
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
178
|
+
store.delete(identityHash);
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const obj = parsed;
|
|
182
|
+
const requestId = obj.requestId;
|
|
183
|
+
const storedHash = obj.identityHash;
|
|
184
|
+
const createdAt = obj.createdAt;
|
|
185
|
+
const status = obj.status;
|
|
186
|
+
if (typeof requestId !== "string" ||
|
|
187
|
+
requestId.length === 0 ||
|
|
188
|
+
typeof storedHash !== "string" ||
|
|
189
|
+
typeof createdAt !== "string" ||
|
|
190
|
+
(status !== "pending" && status !== "in_progress")) {
|
|
191
|
+
store.delete(identityHash);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return { requestId, identityHash: storedHash, createdAt, status };
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
store.delete(identityHash);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
async write(identityHash, state) {
|
|
202
|
+
if (store.has(identityHash)) {
|
|
203
|
+
const err = new Error(`EEXIST: file already exists, write '${identityHash}.json'`);
|
|
204
|
+
err.code = "EEXIST";
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
store.set(identityHash, JSON.stringify(state));
|
|
208
|
+
},
|
|
209
|
+
async remove(identityHash) {
|
|
210
|
+
store.delete(identityHash);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=research-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"research-state.js","sourceRoot":"","sources":["../../src/lib/research-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiniMax SDK client factory (DESIGN.md §12 — P2-04).
|
|
3
|
+
*
|
|
4
|
+
* This is the ONLY module that imports `mmx-cli/sdk`. The Adapter and
|
|
5
|
+
* registry consume {@link createMiniMaxSdk} plus the injectable
|
|
6
|
+
* {@link MiniMaxSdkConstructor} / {@link MiniMaxSdkPort} types declared
|
|
7
|
+
* in `providers/types.ts`, so the transitional SDK never leaks past
|
|
8
|
+
* this boundary.
|
|
9
|
+
*
|
|
10
|
+
* The characterized SDK reads its own config directory during
|
|
11
|
+
* construction (it calls `getConfigDir()`, which honours
|
|
12
|
+
* `MMX_CONFIG_DIR`). To prevent the SDK from touching the user's real
|
|
13
|
+
* `~/.mmx` state, the factory temporarily points `MMX_CONFIG_DIR` at a
|
|
14
|
+
* unique nonexistent path for the synchronous construction call only,
|
|
15
|
+
* then restores the prior value in `finally`. The temporary directory
|
|
16
|
+
* is NEVER created on disk.
|
|
17
|
+
*/
|
|
18
|
+
import type { MiniMaxSdkConstructor, MiniMaxSdkPort } from "../types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Construct a MiniMax SDK port. The optional `constructor` lets tests
|
|
21
|
+
* inject a fake; production omits it and uses the pinned `mmx-cli/sdk`
|
|
22
|
+
* implementation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createMiniMaxSdk(options: {
|
|
25
|
+
apiKey: string;
|
|
26
|
+
region: "global" | "cn";
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
}, constructor?: MiniMaxSdkConstructor): MiniMaxSdkPort;
|
|
29
|
+
//# sourceMappingURL=sdk-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-client.d.ts","sourceRoot":"","sources":["../../../src/providers/minimax/sdk-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAIzE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EACrE,WAAW,CAAC,EAAE,qBAAqB,GAClC,cAAc,CAoBhB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiniMax SDK client factory (DESIGN.md §12 — P2-04).
|
|
3
|
+
*
|
|
4
|
+
* This is the ONLY module that imports `mmx-cli/sdk`. The Adapter and
|
|
5
|
+
* registry consume {@link createMiniMaxSdk} plus the injectable
|
|
6
|
+
* {@link MiniMaxSdkConstructor} / {@link MiniMaxSdkPort} types declared
|
|
7
|
+
* in `providers/types.ts`, so the transitional SDK never leaks past
|
|
8
|
+
* this boundary.
|
|
9
|
+
*
|
|
10
|
+
* The characterized SDK reads its own config directory during
|
|
11
|
+
* construction (it calls `getConfigDir()`, which honours
|
|
12
|
+
* `MMX_CONFIG_DIR`). To prevent the SDK from touching the user's real
|
|
13
|
+
* `~/.mmx` state, the factory temporarily points `MMX_CONFIG_DIR` at a
|
|
14
|
+
* unique nonexistent path for the synchronous construction call only,
|
|
15
|
+
* then restores the prior value in `finally`. The temporary directory
|
|
16
|
+
* is NEVER created on disk.
|
|
17
|
+
*/
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { MiniMaxSDK } from "mmx-cli/sdk";
|
|
22
|
+
const MMX_CONFIG_DIR = "MMX_CONFIG_DIR";
|
|
23
|
+
/**
|
|
24
|
+
* Construct a MiniMax SDK port. The optional `constructor` lets tests
|
|
25
|
+
* inject a fake; production omits it and uses the pinned `mmx-cli/sdk`
|
|
26
|
+
* implementation.
|
|
27
|
+
*/
|
|
28
|
+
export function createMiniMaxSdk(options, constructor) {
|
|
29
|
+
const Ctor = constructor ?? MiniMaxSDK;
|
|
30
|
+
const hadPrev = Object.prototype.hasOwnProperty.call(process.env, MMX_CONFIG_DIR);
|
|
31
|
+
const prev = process.env[MMX_CONFIG_DIR];
|
|
32
|
+
// A unique nonexistent path. It is deliberately never created.
|
|
33
|
+
const temporaryDir = path.join(tmpdir(), `scoutline-minimax-${randomUUID()}`);
|
|
34
|
+
process.env[MMX_CONFIG_DIR] = temporaryDir;
|
|
35
|
+
try {
|
|
36
|
+
// Construction is synchronous and reads MMX_CONFIG_DIR via the
|
|
37
|
+
// SDK's getConfigDir(). The async search/vision calls happen
|
|
38
|
+
// after this block, with the original env already restored.
|
|
39
|
+
return new Ctor(options);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (hadPrev) {
|
|
43
|
+
process.env[MMX_CONFIG_DIR] = prev;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
delete process.env[MMX_CONFIG_DIR];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=sdk-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-client.js","sourceRoot":"","sources":["../../../src/providers/minimax/sdk-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,OAAqE,EACrE,WAAmC;IAEnC,MAAM,IAAI,GAAG,WAAW,IAAK,UAA+C,CAAC;IAE7E,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACzC,+DAA+D;IAC/D,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,qBAAqB,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9E,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,YAAY,CAAC;IAC3C,IAAI,CAAC;QACH,+DAA+D;QAC/D,6DAA6D;QAC7D,4DAA4D;QAC5D,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/zai/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/zai/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,KAAK,EAIV,kBAAkB,EAElB,sBAAsB,EAGvB,MAAM,aAAa,CAAC;AAw0BrB;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,CAAC,EAAE,sBAAsB,GAAG,kBAAkB,CAyG7F"}
|
|
@@ -24,13 +24,14 @@
|
|
|
24
24
|
* and location to the Provider. It NEVER sends count.
|
|
25
25
|
*/
|
|
26
26
|
import crypto from "node:crypto";
|
|
27
|
+
import { promises as fsPromises } from "node:fs";
|
|
27
28
|
import { ApiError, AuthError, NetworkError, TimeoutError, UnsupportedOptionError, ValidationError, } from "../../lib/errors.js";
|
|
28
29
|
import { getMcpToolName } from "../../lib/mcp-config.js";
|
|
29
30
|
import { ZaiMcpClient, } from "../../lib/mcp-client.js";
|
|
30
31
|
import { buildLegacyRepositoryCacheKey } from "../../lib/cache.js";
|
|
31
32
|
import { applySearchTopic } from "../../lib/search-topic.js";
|
|
32
33
|
import { isZaiConfigured, requireZaiApiKey } from "./credentials.js";
|
|
33
|
-
import { resolveImageSource, resolveVideoSource } from "./media.js";
|
|
34
|
+
import { resolveImageSource, resolveVideoSource, fetchImageSource, fetchVideoSource, } from "./media.js";
|
|
34
35
|
import { createZaiQuotaCapability } from "./quota.js";
|
|
35
36
|
import { createZaiRepositoryCapability } from "./repository.js";
|
|
36
37
|
import { createZaiReaderCapability } from "./reader.js";
|
|
@@ -397,40 +398,191 @@ function createZaiVisionCapability(options) {
|
|
|
397
398
|
// Unsupported operations never reach here: the descriptor-level
|
|
398
399
|
// gate and `supports()` reject first (defence in depth).
|
|
399
400
|
resolveApiKey();
|
|
400
|
-
const
|
|
401
|
-
// Disable client-owned cache and retry so shared execution is the
|
|
402
|
-
// single policy owner. Vision enables the Z.AI vision MCP server.
|
|
403
|
-
const clientOptions = {
|
|
404
|
-
enableVision: true,
|
|
405
|
-
noCache: true,
|
|
406
|
-
disableRetry: true,
|
|
407
|
-
};
|
|
408
|
-
const client = clientFactory(clientOptions);
|
|
401
|
+
const tempPaths = [];
|
|
409
402
|
try {
|
|
410
|
-
|
|
411
|
-
|
|
403
|
+
// First attempt: HTTP(S) URLs pass straight through to the
|
|
404
|
+
// Provider's server-side fetcher.
|
|
405
|
+
const first = buildZaiVisionInvocation(request, resolveImageSource, resolveVideoSource);
|
|
406
|
+
try {
|
|
407
|
+
return normalizeZaiVisionResult(await invokeZaiVisionOnce(clientFactory, first.toolName, first.args));
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
// Automatic URL -> local fallback (Issue E): Z.AI's vision MCP
|
|
411
|
+
// refuses base64 data URIs and its server-side URL fetcher is
|
|
412
|
+
// unreliable — it rejects some image URLs with code 1210,
|
|
413
|
+
// returns empty, or hangs. The MCP surfaces these inconsistently
|
|
414
|
+
// (a 1210 reaches the Adapter as a sanitized ApiError 500 with
|
|
415
|
+
// the original detail discarded, so it cannot be detected by
|
|
416
|
+
// status code or message). Retry by fetching each URL source
|
|
417
|
+
// here to a temp file (validated against the same Z.AI media
|
|
418
|
+
// limits) and passing that path.
|
|
419
|
+
//
|
|
420
|
+
// The fallback fires for ANY transport/processing failure on a
|
|
421
|
+
// URL source except auth/quota (those are not source-related and
|
|
422
|
+
// a local fetch cannot remedy them). Auth (401/403) and
|
|
423
|
+
// exhausted-quota failures therefore propagate untouched.
|
|
424
|
+
if (isUrlVisionSource(request) && isFallbackEligibleError(error)) {
|
|
425
|
+
try {
|
|
426
|
+
const fetched = await prefetchVisionUrlSources(request);
|
|
427
|
+
for (const local of fetched.values())
|
|
428
|
+
tempPaths.push(local);
|
|
429
|
+
const resolveImage = makePrefetchResolver(fetched, resolveImageSource);
|
|
430
|
+
const resolveVideo = makePrefetchResolver(fetched, resolveVideoSource);
|
|
431
|
+
const fallback = buildZaiVisionInvocation(request, resolveImage, resolveVideo);
|
|
432
|
+
return normalizeZaiVisionResult(await invokeZaiVisionOnce(clientFactory, fallback.toolName, fallback.args));
|
|
433
|
+
}
|
|
434
|
+
catch (fallbackError) {
|
|
435
|
+
// The local fetch or the retried attempt failed. Surface a
|
|
436
|
+
// TERMINAL error (422, not in the shared retry policy's
|
|
437
|
+
// retryable set) so the policy does not re-run the whole
|
|
438
|
+
// URL attempt and multiply latency without benefit. The
|
|
439
|
+
// message names both the original Provider failure and the
|
|
440
|
+
// fallback failure so the user knows the fallback was
|
|
441
|
+
// attempted and why it could not recover.
|
|
442
|
+
throw new ApiError(`Z.AI vision request failed for the URL source and the local-fetch fallback also failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`, 422);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
412
447
|
}
|
|
413
448
|
finally {
|
|
414
|
-
await
|
|
449
|
+
await cleanupTempPaths(tempPaths);
|
|
415
450
|
}
|
|
416
451
|
},
|
|
417
452
|
};
|
|
418
453
|
return capability;
|
|
419
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Perform one vision transport attempt. A fresh client is constructed per
|
|
457
|
+
* attempt (the Adapter never retries internally; shared execution owns the
|
|
458
|
+
* retry policy) and closed exactly once in `finally`. Close failure never
|
|
459
|
+
* replaces a successful result nor masks the primary failure.
|
|
460
|
+
*/
|
|
461
|
+
async function invokeZaiVisionOnce(clientFactory, toolName, args) {
|
|
462
|
+
const clientOptions = {
|
|
463
|
+
enableVision: true,
|
|
464
|
+
noCache: true,
|
|
465
|
+
disableRetry: true,
|
|
466
|
+
};
|
|
467
|
+
const client = clientFactory(clientOptions);
|
|
468
|
+
try {
|
|
469
|
+
return await invokeZaiVision(client, toolName, args);
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
await client.close().catch(() => { });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Whether a `VisionRequest` carries at least one HTTP(S) source. The
|
|
477
|
+
* URL->local fallback only applies when a Provider-attempted URL is the
|
|
478
|
+
* likely cause of a client error; local-file sources already resolved
|
|
479
|
+
* before transport and are not re-attempted with a fetch.
|
|
480
|
+
*/
|
|
481
|
+
function isUrlVisionSource(request) {
|
|
482
|
+
return visionSourceUrls(request).length > 0;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Collect the HTTP(S) source strings carried by a `VisionRequest`.
|
|
486
|
+
* `diff` carries two image sources; `video` carries one video source;
|
|
487
|
+
* every other operation carries one image source.
|
|
488
|
+
*/
|
|
489
|
+
function visionSourceUrls(request) {
|
|
490
|
+
const out = [];
|
|
491
|
+
const pushIfUrl = (s) => {
|
|
492
|
+
if (typeof s === "string" && (s.startsWith("http://") || s.startsWith("https://"))) {
|
|
493
|
+
out.push(s);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
switch (request.operation) {
|
|
497
|
+
case "diff":
|
|
498
|
+
pushIfUrl(request.expectedSource);
|
|
499
|
+
pushIfUrl(request.actualSource);
|
|
500
|
+
break;
|
|
501
|
+
case "video":
|
|
502
|
+
pushIfUrl(request.source);
|
|
503
|
+
break;
|
|
504
|
+
default:
|
|
505
|
+
pushIfUrl(request.source);
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Whether a normalized vision error is eligible for the URL -> local
|
|
511
|
+
* fallback. The fallback fires for any transport or processing failure
|
|
512
|
+
* that a local fetch could plausibly remedy: client errors (400/422, e.g.
|
|
513
|
+
* a 1210 image-format rejection), server errors (5xx), timeouts, and
|
|
514
|
+
* network errors. Auth failures (401/403) and exhausted-quota failures are
|
|
515
|
+
* NOT eligible — a local fetch cannot fix a missing credential or a spent
|
|
516
|
+
* quota, so those propagate to the user unchanged.
|
|
517
|
+
*
|
|
518
|
+
* Note: the vision MCP surfaces a code 1210 image-format rejection as a
|
|
519
|
+
* sanitized `ApiError` 500 with the original detail discarded, so this
|
|
520
|
+
* check is deliberately type-and-status-based rather than message-based.
|
|
521
|
+
*/
|
|
522
|
+
function isFallbackEligibleError(error) {
|
|
523
|
+
if (error instanceof ApiError) {
|
|
524
|
+
return error.statusCode !== 401 && error.statusCode !== 403;
|
|
525
|
+
}
|
|
526
|
+
return error instanceof TimeoutError || error instanceof NetworkError;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Prefetch each HTTP(S) source URL on the request to a validated temp
|
|
530
|
+
* file path, returning a Map from the original URL to its local path.
|
|
531
|
+
* Video sources use the video media limits; every other source uses the
|
|
532
|
+
* image limits. A fetch failure throws the normalized media error and
|
|
533
|
+
* aborts the fallback (the original client error is then surfaced).
|
|
534
|
+
*/
|
|
535
|
+
async function prefetchVisionUrlSources(request) {
|
|
536
|
+
const isVideo = request.operation === "video";
|
|
537
|
+
const fetched = new Map();
|
|
538
|
+
for (const url of visionSourceUrls(request)) {
|
|
539
|
+
const local = isVideo ? await fetchVideoSource(url) : await fetchImageSource(url);
|
|
540
|
+
fetched.set(url, local);
|
|
541
|
+
}
|
|
542
|
+
return fetched;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Build a sync media resolver for the fallback attempt. A source present
|
|
546
|
+
* in the prefetched `urlToPath` map is substituted with its local temp
|
|
547
|
+
* path; every other source falls through to the normal passthrough
|
|
548
|
+
* resolver (`resolveImageSource`/`resolveVideoSource`).
|
|
549
|
+
*/
|
|
550
|
+
function makePrefetchResolver(urlToPath, passthrough) {
|
|
551
|
+
return (source) => urlToPath.get(source) ?? passthrough(source);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Unlink every prefetched temp file. Each unlink is best-effort: a
|
|
555
|
+
* missing file or permission failure never replaces a successful result
|
|
556
|
+
* nor masks a primary failure.
|
|
557
|
+
*/
|
|
558
|
+
async function cleanupTempPaths(tempPaths) {
|
|
559
|
+
for (const p of tempPaths) {
|
|
560
|
+
try {
|
|
561
|
+
await fsPromises.unlink(p);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Best-effort cleanup; ignore.
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
420
568
|
/**
|
|
421
569
|
* Map a discriminated `VisionRequest` to its dedicated Z.AI MCP tool name
|
|
422
|
-
* and arguments, resolving media through the
|
|
570
|
+
* and arguments, resolving media through the supplied resolvers. Field
|
|
423
571
|
* names mirror the characterized transport schema (see `mcp-client.ts`
|
|
424
572
|
* and the live discovery fixtures). Optional fields are omitted when
|
|
425
573
|
* absent so the Provider receives the same request shape Phase 1 sent.
|
|
574
|
+
*
|
|
575
|
+
* The image/video resolvers are injected so the first attempt can pass
|
|
576
|
+
* HTTP(S) URLs straight through (`resolveImageSource`) while a fallback
|
|
577
|
+
* attempt can substitute a fetched temp-file path (`fetchImageSource`).
|
|
426
578
|
*/
|
|
427
|
-
function buildZaiVisionInvocation(request) {
|
|
579
|
+
function buildZaiVisionInvocation(request, resolveImage, resolveVideo) {
|
|
428
580
|
switch (request.operation) {
|
|
429
581
|
case "interpret-image":
|
|
430
582
|
return {
|
|
431
583
|
toolName: VISION_ANALYZE_TOOL_PUBLIC_NAME,
|
|
432
584
|
args: {
|
|
433
|
-
image_source:
|
|
585
|
+
image_source: resolveImage(request.source),
|
|
434
586
|
prompt: request.instruction,
|
|
435
587
|
},
|
|
436
588
|
};
|
|
@@ -438,14 +590,14 @@ function buildZaiVisionInvocation(request) {
|
|
|
438
590
|
return {
|
|
439
591
|
toolName: VISION_UI_TO_ARTIFACT_TOOL_PUBLIC_NAME,
|
|
440
592
|
args: {
|
|
441
|
-
image_source:
|
|
593
|
+
image_source: resolveImage(request.source),
|
|
442
594
|
output_type: request.outputType,
|
|
443
595
|
prompt: request.instruction,
|
|
444
596
|
},
|
|
445
597
|
};
|
|
446
598
|
case "extract-text": {
|
|
447
599
|
const args = {
|
|
448
|
-
image_source:
|
|
600
|
+
image_source: resolveImage(request.source),
|
|
449
601
|
prompt: request.instruction,
|
|
450
602
|
};
|
|
451
603
|
if (request.programmingLanguage) {
|
|
@@ -455,7 +607,7 @@ function buildZaiVisionInvocation(request) {
|
|
|
455
607
|
}
|
|
456
608
|
case "diagnose-error": {
|
|
457
609
|
const args = {
|
|
458
|
-
image_source:
|
|
610
|
+
image_source: resolveImage(request.source),
|
|
459
611
|
prompt: request.instruction,
|
|
460
612
|
};
|
|
461
613
|
if (request.context) {
|
|
@@ -465,7 +617,7 @@ function buildZaiVisionInvocation(request) {
|
|
|
465
617
|
}
|
|
466
618
|
case "diagram": {
|
|
467
619
|
const args = {
|
|
468
|
-
image_source:
|
|
620
|
+
image_source: resolveImage(request.source),
|
|
469
621
|
prompt: request.instruction,
|
|
470
622
|
};
|
|
471
623
|
if (request.diagramType) {
|
|
@@ -475,7 +627,7 @@ function buildZaiVisionInvocation(request) {
|
|
|
475
627
|
}
|
|
476
628
|
case "chart": {
|
|
477
629
|
const args = {
|
|
478
|
-
image_source:
|
|
630
|
+
image_source: resolveImage(request.source),
|
|
479
631
|
prompt: request.instruction,
|
|
480
632
|
};
|
|
481
633
|
if (request.focus) {
|
|
@@ -487,8 +639,8 @@ function buildZaiVisionInvocation(request) {
|
|
|
487
639
|
return {
|
|
488
640
|
toolName: VISION_DIFF_TOOL_PUBLIC_NAME,
|
|
489
641
|
args: {
|
|
490
|
-
expected_image_source:
|
|
491
|
-
actual_image_source:
|
|
642
|
+
expected_image_source: resolveImage(request.expectedSource),
|
|
643
|
+
actual_image_source: resolveImage(request.actualSource),
|
|
492
644
|
prompt: request.instruction,
|
|
493
645
|
},
|
|
494
646
|
};
|
|
@@ -496,7 +648,7 @@ function buildZaiVisionInvocation(request) {
|
|
|
496
648
|
return {
|
|
497
649
|
toolName: VISION_VIDEO_TOOL_PUBLIC_NAME,
|
|
498
650
|
args: {
|
|
499
|
-
video_source:
|
|
651
|
+
video_source: resolveVideo(request.source),
|
|
500
652
|
prompt: request.instruction,
|
|
501
653
|
},
|
|
502
654
|
};
|