dsh-agy 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/lib/{accounts-DW-BL9c3.mjs → accounts-DtIsWCeF.mjs} +15 -11
- package/lib/{accounts-DW-BL9c3.mjs.map → accounts-DtIsWCeF.mjs.map} +1 -1
- package/lib/cli/index.mjs +6 -6
- package/lib/client.js +181 -23
- package/lib/{constants-CmA9wXNe.mjs → constants-AdsD4sKh.mjs} +3 -3
- package/lib/{constants-CmA9wXNe.mjs.map → constants-AdsD4sKh.mjs.map} +1 -1
- package/lib/{exchange-C-tndv7j.mjs → exchange-B_rHhU8K.mjs} +3 -3
- package/lib/{exchange-C-tndv7j.mjs.map → exchange-B_rHhU8K.mjs.map} +1 -1
- package/lib/{import-SxPDjH3u.mjs → import-BqyA9tzt.mjs} +4 -4
- package/lib/{import-SxPDjH3u.mjs.map → import-BqyA9tzt.mjs.map} +1 -1
- package/lib/index.mjs +2 -2
- package/lib/{models-CjB0rXtI.mjs → models-D_54Ro5z.mjs} +3 -3
- package/lib/{models-CjB0rXtI.mjs.map → models-D_54Ro5z.mjs.map} +1 -1
- package/lib/{parse-38V1zT1B.mjs → parse-CeJc7cHo.mjs} +141 -19
- package/lib/parse-CeJc7cHo.mjs.map +1 -0
- package/lib/{plugin-common-CKTYXxSc.mjs → plugin-common-BlGbNHxX.mjs} +426 -73
- package/lib/plugin-common-BlGbNHxX.mjs.map +1 -0
- package/lib/{proxy-BbuQX5U9.mjs → proxy-B7VnbAfO.mjs} +5 -2
- package/lib/proxy-B7VnbAfO.mjs.map +1 -0
- package/lib/{quota-summary-DEA3P3Zh.mjs → quota-summary-DarIZkmo.mjs} +3 -3
- package/lib/{quota-summary-DEA3P3Zh.mjs.map → quota-summary-DarIZkmo.mjs.map} +1 -1
- package/lib/{translate-BR1kuCaz.mjs → translate-vh3M5H_9.mjs} +51 -9
- package/lib/translate-vh3M5H_9.mjs.map +1 -0
- package/lib/web/plugin.d.mts +23 -1
- package/lib/web/plugin.d.mts.map +1 -1
- package/lib/web/plugin.mjs +86 -19
- package/lib/web/plugin.mjs.map +1 -1
- package/package.json +1 -1
- package/lib/parse-38V1zT1B.mjs.map +0 -1
- package/lib/plugin-common-CKTYXxSc.mjs.map +0 -1
- package/lib/proxy-BbuQX5U9.mjs.map +0 -1
- package/lib/translate-BR1kuCaz.mjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { m as fetchAgyFirstOk, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-
|
|
2
|
-
import { a as probeFetch, o as proxiedFetch, t as accountFetch } from "./proxy-
|
|
3
|
-
import { C as isSessionAccumulationOverflow, S as describeFetchError, _ as persistMasterKey, b as classifyFetchError, f as migrateToAgyDir, g as loadMasterKey, h as deriveKey, m as createAesGcmCodec, p as MASTER_KEY_REF, r as AgySessionManager, s as resolveAntigravityVersion, t as JsonAccountStore, u as pickProbeProxyUrl, v as resolveDshHome, x as classifyHttpError, y as resolveMasterKeyCodec } from "./accounts-
|
|
1
|
+
import { m as fetchAgyFirstOk, v as AgyAuthError, y as AgyPoolBlockedError } from "./constants-AdsD4sKh.mjs";
|
|
2
|
+
import { a as probeFetch, o as proxiedFetch, t as accountFetch } from "./proxy-B7VnbAfO.mjs";
|
|
3
|
+
import { C as isSessionAccumulationOverflow, S as describeFetchError, _ as persistMasterKey, b as classifyFetchError, f as migrateToAgyDir, g as loadMasterKey, h as deriveKey, m as createAesGcmCodec, p as MASTER_KEY_REF, r as AgySessionManager, s as resolveAntigravityVersion, t as JsonAccountStore, u as pickProbeProxyUrl, v as resolveDshHome, x as classifyHttpError, y as resolveMasterKeyCodec } from "./accounts-DtIsWCeF.mjs";
|
|
4
4
|
import { i as generateAntigravityRequestId, n as currentSessionGeneration, r as deriveAntigravitySessionId, t as bumpSessionGeneration } from "./identity-Ci8EGc1F.mjs";
|
|
5
|
-
import { i as setThoughtSignature, r as resolveMultimodalFiles, t as toAgyRequestBody } from "./translate-
|
|
6
|
-
import { n as parseAgySse, t as
|
|
7
|
-
import { i as resolveAgyModel, n as listAgyModels, t as catalogModelList } from "./models-
|
|
5
|
+
import { i as setThoughtSignature, r as resolveMultimodalFiles, t as toAgyRequestBody } from "./translate-vh3M5H_9.mjs";
|
|
6
|
+
import { n as UnmappedFinishReasonError, r as parseAgySse, t as AgyStreamIdleTimeoutError } from "./parse-CeJc7cHo.mjs";
|
|
7
|
+
import { i as resolveAgyModel, n as listAgyModels, t as catalogModelList } from "./models-D_54Ro5z.mjs";
|
|
8
8
|
import { randomBytes } from "node:crypto";
|
|
9
9
|
import { LlmAdapter, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -29,6 +29,22 @@ function collectImageRefs(options) {
|
|
|
29
29
|
const UPSTREAM_ERROR_CODE = "UPSTREAM";
|
|
30
30
|
/** First-class DSH retryable code: the default retry policy honors SERVER (5xx), not UPSTREAM. */
|
|
31
31
|
const SERVER_ERROR_CODE = "SERVER";
|
|
32
|
+
/** Abort-aware delay helper for transient stream retries. */
|
|
33
|
+
async function sleepWithSignal(ms, signal) {
|
|
34
|
+
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
35
|
+
await new Promise((resolve, reject) => {
|
|
36
|
+
const onAbort = () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
39
|
+
};
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
signal?.removeEventListener("abort", onAbort);
|
|
42
|
+
resolve();
|
|
43
|
+
}, ms);
|
|
44
|
+
if (!signal) return;
|
|
45
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
32
48
|
/** Stable ledger key for an account: email when present, else the generated id. */
|
|
33
49
|
function ledgerAccountKey(session) {
|
|
34
50
|
return session.account.email ?? session.account.id;
|
|
@@ -215,12 +231,10 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
215
231
|
throw error;
|
|
216
232
|
}
|
|
217
233
|
if (!session) throw new LlmError("No agy account configured — run `dsh-agy login` to authenticate.", "NO_CREDENTIAL");
|
|
218
|
-
const multimodalFiles = await resolveMultimodalFiles(options);
|
|
234
|
+
const multimodalFiles = await resolveMultimodalFiles(options, { maxBytes: this.options.maxInlineBytes?.() });
|
|
219
235
|
holder.account = session.account;
|
|
220
236
|
this.options.noteRequestStarted?.(session.account);
|
|
221
237
|
const conversationAccount = ledgerAccountKey(session);
|
|
222
|
-
/** Wall-clock origin for this attempt's latency figures. */
|
|
223
|
-
const startedAt = Date.now();
|
|
224
238
|
const routing = {
|
|
225
239
|
proxyUrl: session.account.proxy,
|
|
226
240
|
streaming: true
|
|
@@ -235,8 +249,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
235
249
|
* names a fresh upstream session and recovers the conversation. This is not
|
|
236
250
|
* an account fault, so it does not go through `reportFailure`: the account
|
|
237
251
|
* stays healthy and only the derived id changes.
|
|
252
|
+
*
|
|
253
|
+
* `attemptStartedAt` is the clock of the outer attempt that owns this send,
|
|
254
|
+
* so a transport failure is billed to that attempt rather than to the call.
|
|
238
255
|
*/
|
|
239
|
-
const sendAttempt = async () => {
|
|
256
|
+
const sendAttempt = async (attemptStartedAt) => {
|
|
240
257
|
for (let attempt = 0;; attempt++) {
|
|
241
258
|
const generation = conversationKey !== void 0 && conversationAccount !== void 0 ? currentSessionGeneration(conversationAccount, conversationKey) : 0;
|
|
242
259
|
const requestId = generateAntigravityRequestId();
|
|
@@ -264,7 +281,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
264
281
|
this.recordUsage(session, options.model, {
|
|
265
282
|
ok: false,
|
|
266
283
|
reason: "network-error"
|
|
267
|
-
},
|
|
284
|
+
}, attemptStartedAt);
|
|
268
285
|
throw new LlmError(classified.message ?? "agy fetch failed", "TRANSPORT", { cause: error });
|
|
269
286
|
}
|
|
270
287
|
if (response.ok) return { response };
|
|
@@ -279,74 +296,171 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
279
296
|
};
|
|
280
297
|
}
|
|
281
298
|
};
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Maximum internal retries when the upstream ends a stream with
|
|
301
|
+
* `MALFORMED_FUNCTION_CALL` before any user-facing text chunks have been
|
|
302
|
+
* yielded to DSH. Gemini models occasionally glitch on tool-call JSON syntax;
|
|
303
|
+
* re-sampling cleanly recovers the turn without crashing the conversation.
|
|
304
|
+
*
|
|
305
|
+
* This deliberately generalises the `sendAttempt` accumulation-wall resend
|
|
306
|
+
* into the stream path: the invariant that one DSH call is one provider
|
|
307
|
+
* attempt now has this second, documented exception (see AGENTS.md, "the
|
|
308
|
+
* adapter does not silent-retry"). Both exceptions share the same shape —
|
|
309
|
+
* the account is healthy and the request was accepted — and neither goes
|
|
310
|
+
* through `reportFailure`, so the pool sees nothing.
|
|
311
|
+
*/
|
|
312
|
+
const MAX_MALFORMED_RETRIES = 3;
|
|
313
|
+
for (let malformedAttempt = 0;; malformedAttempt++) {
|
|
314
|
+
const attemptStartedAt = Date.now();
|
|
315
|
+
const { response, bodyText } = await sendAttempt(attemptStartedAt);
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
const classified = classifyHttpError(response.status, response.headers, bodyText);
|
|
318
|
+
await this.options.reportFailure(classified.kind, session, {
|
|
319
|
+
retryAfterMs: classified.retryAfterMs,
|
|
320
|
+
status: response.status,
|
|
321
|
+
rateLimitCategory: classified.rateLimitCategory,
|
|
322
|
+
resetTime: classified.resetTime,
|
|
323
|
+
model: options.model,
|
|
324
|
+
verificationUrl: classified.verificationUrl
|
|
325
|
+
});
|
|
326
|
+
this.recordUsage(session, options.model, {
|
|
327
|
+
ok: false,
|
|
328
|
+
rateLimited: classified.kind === "rate-limit",
|
|
329
|
+
reason: classified.kind
|
|
330
|
+
}, attemptStartedAt);
|
|
331
|
+
if (classified.kind === "rate-limit") {
|
|
332
|
+
if (classified.rateLimitCategory === "quota_exhausted") throw new LlmError(`agy daily quota exhausted (${response.status}): ${classified.message ?? ""}`, QUOTA_EXCEEDED_CODE);
|
|
333
|
+
throw new LlmError(`agy rate-limited (${response.status}): ${classified.message ?? ""}`, "RATE_LIMIT", {
|
|
334
|
+
providerRetryAfterMs: classified.retryAfterMs ?? void 0,
|
|
335
|
+
requestId: ProviderRequestId(generateAntigravityRequestId())
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (classified.kind === "verification-required") {
|
|
339
|
+
const appeal = classified.verificationUrl ? ` Verify at: ${classified.verificationUrl}` : "";
|
|
340
|
+
throw new LlmError(`agy account needs verification (${response.status}): ${classified.message ?? ""}${appeal}`, "RATE_LIMIT", { requestId: ProviderRequestId(generateAntigravityRequestId()) });
|
|
341
|
+
}
|
|
342
|
+
if (classified.kind === "auth-failure") throw new LlmError(`agy authentication failed (${response.status}) — run \`dsh-agy login\``, "INVALID_CREDENTIAL");
|
|
343
|
+
if (classified.status !== void 0 && classified.status >= 500) throw new LlmError(`agy upstream error (${response.status}): ${classified.message ?? ""}`, SERVER_ERROR_CODE, {
|
|
301
344
|
providerRetryAfterMs: classified.retryAfterMs ?? void 0,
|
|
302
345
|
requestId: ProviderRequestId(generateAntigravityRequestId())
|
|
303
346
|
});
|
|
347
|
+
throw new LlmError(`agy upstream error (${response.status}): ${classified.message ?? ""}`, UPSTREAM_ERROR_CODE);
|
|
304
348
|
}
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
349
|
+
if (!response.body) throw new LlmError("agy stream returned no body", UPSTREAM_ERROR_CODE);
|
|
350
|
+
const bufferedChunks = [];
|
|
351
|
+
/**
|
|
352
|
+
* Thought signatures observed during this attempt, held back with the
|
|
353
|
+
* chunks that carried them. `onToolSignature` fires as soon as the parser
|
|
354
|
+
* sees a signature, i.e. BEFORE the attempt is known to be keepable, so
|
|
355
|
+
* caching eagerly would let a discarded attempt write the process-global
|
|
356
|
+
* signature cache (keyed by `toolCallId` — and issue #99 records those ids
|
|
357
|
+
* colliding across distant turns, which would poison the very retry it
|
|
358
|
+
* triggered).
|
|
359
|
+
*/
|
|
360
|
+
const pendingSignatures = [];
|
|
361
|
+
let yieldedDirect = false;
|
|
318
362
|
let usage;
|
|
319
363
|
let ttftMs;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Persist the signatures observed so far.
|
|
366
|
+
*
|
|
367
|
+
* Called at every point where the chunks that carried them have been
|
|
368
|
+
* handed off — never on the discard path, so a thrown-away attempt leaves
|
|
369
|
+
* no trace. The hand-off is the only sound anchor: a `functionCall` can
|
|
370
|
+
* arrive AFTER the first `text-delta` (text-then-tool is a common shape),
|
|
371
|
+
* so anchoring at stream end would drop the signature of an already
|
|
372
|
+
* delivered call the moment the stream dies mid-body, silently degrading
|
|
373
|
+
* the next round to the sentinel.
|
|
374
|
+
*/
|
|
375
|
+
const commitSignatures = () => {
|
|
376
|
+
for (const [toolCallId, signature] of pendingSignatures) setThoughtSignature(toolCallId, signature);
|
|
377
|
+
pendingSignatures.length = 0;
|
|
378
|
+
};
|
|
379
|
+
/** Hand the held chunks to the consumer. */
|
|
380
|
+
const drainBuffer = () => {
|
|
381
|
+
const drained = bufferedChunks.slice();
|
|
382
|
+
bufferedChunks.length = 0;
|
|
383
|
+
return drained;
|
|
384
|
+
};
|
|
385
|
+
try {
|
|
386
|
+
for await (const chunk of parseAgySse(response.body, {
|
|
387
|
+
signal: options.signal,
|
|
388
|
+
onToolSignature: (toolCallId, signature) => {
|
|
389
|
+
pendingSignatures.push([toolCallId, signature]);
|
|
390
|
+
}
|
|
391
|
+
})) {
|
|
392
|
+
if (chunk.type === "usage") usage = {
|
|
393
|
+
input: chunk.usage.inputTokens,
|
|
394
|
+
output: chunk.usage.outputTokens,
|
|
395
|
+
cacheRead: chunk.usage.cacheReadTokens ?? 0,
|
|
396
|
+
cacheWrite: chunk.usage.cacheWriteTokens ?? 0
|
|
397
|
+
};
|
|
398
|
+
if (yieldedDirect) {
|
|
399
|
+
if (pendingSignatures.length !== 0) commitSignatures();
|
|
400
|
+
yield chunk;
|
|
401
|
+
} else if (chunk.type === "text-delta") {
|
|
402
|
+
yieldedDirect = true;
|
|
403
|
+
commitSignatures();
|
|
404
|
+
ttftMs = Date.now() - attemptStartedAt;
|
|
405
|
+
for (const buffered of drainBuffer()) yield buffered;
|
|
406
|
+
yield chunk;
|
|
407
|
+
} else bufferedChunks.push(chunk);
|
|
324
408
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
409
|
+
if (ttftMs === void 0) ttftMs = Date.now() - attemptStartedAt;
|
|
410
|
+
commitSignatures();
|
|
411
|
+
for (const buffered of drainBuffer()) yield buffered;
|
|
412
|
+
await this.options.markSuccess?.(session);
|
|
413
|
+
this.recordUsage(session, options.model, {
|
|
414
|
+
ok: true,
|
|
415
|
+
usage,
|
|
416
|
+
ttftMs
|
|
417
|
+
}, attemptStartedAt);
|
|
418
|
+
return;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
421
|
+
this.recordUsage(session, options.model, {
|
|
422
|
+
ok: false,
|
|
423
|
+
reason: "aborted",
|
|
424
|
+
usage
|
|
425
|
+
}, attemptStartedAt);
|
|
426
|
+
await response.body?.cancel().catch(() => {});
|
|
427
|
+
throw new LlmError("agy stream aborted", "ABORTED", { cause: error });
|
|
428
|
+
}
|
|
429
|
+
if (error instanceof AgyStreamIdleTimeoutError && !yieldedDirect) {
|
|
430
|
+
await this.options.reportFailure("network-error", session);
|
|
431
|
+
this.recordUsage(session, options.model, {
|
|
432
|
+
ok: false,
|
|
433
|
+
reason: "network-error",
|
|
434
|
+
usage
|
|
435
|
+
}, attemptStartedAt);
|
|
436
|
+
await response.body?.cancel().catch(() => {});
|
|
437
|
+
throw new LlmError(`${error.message} — nothing was delivered, retrying`, "TIMEOUT", { cause: error });
|
|
438
|
+
}
|
|
439
|
+
if (error instanceof UnmappedFinishReasonError && error.reason === "MALFORMED_FUNCTION_CALL" && !yieldedDirect && malformedAttempt < MAX_MALFORMED_RETRIES) {
|
|
440
|
+
this.recordUsage(session, options.model, {
|
|
441
|
+
ok: false,
|
|
442
|
+
reason: "request-error",
|
|
443
|
+
usage
|
|
444
|
+
}, attemptStartedAt);
|
|
445
|
+
await response.body?.cancel().catch(() => {});
|
|
446
|
+
try {
|
|
447
|
+
await sleepWithSignal(200 * (malformedAttempt + 1), options.signal);
|
|
448
|
+
} catch (sleepError) {
|
|
449
|
+
if (sleepError instanceof DOMException && sleepError.name === "AbortError") throw new LlmError("agy stream aborted", "ABORTED", { cause: sleepError });
|
|
450
|
+
throw sleepError;
|
|
451
|
+
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const unmappedFinish = error instanceof UnmappedFinishReasonError;
|
|
455
|
+
await this.options.reportFailure(unmappedFinish ? "request-error" : "network-error", session);
|
|
456
|
+
this.recordUsage(session, options.model, {
|
|
457
|
+
ok: false,
|
|
458
|
+
reason: unmappedFinish ? "request-error" : "network-error",
|
|
459
|
+
usage
|
|
460
|
+
}, attemptStartedAt);
|
|
461
|
+
await response.body?.cancel().catch(() => {});
|
|
462
|
+
throw new LlmError(error instanceof Error ? describeFetchError(error) : "agy stream parse failed", UPSTREAM_ERROR_CODE, { cause: error });
|
|
334
463
|
}
|
|
335
|
-
await this.options.markSuccess?.(session);
|
|
336
|
-
this.recordUsage(session, options.model, {
|
|
337
|
-
ok: true,
|
|
338
|
-
usage,
|
|
339
|
-
ttftMs
|
|
340
|
-
}, startedAt);
|
|
341
|
-
} catch (error) {
|
|
342
|
-
if (error instanceof DOMException && error.name === "AbortError") throw new LlmError("agy stream aborted", "ABORTED", { cause: error });
|
|
343
|
-
const unmappedFinish = error instanceof UnmappedFinishReasonError;
|
|
344
|
-
await this.options.reportFailure(unmappedFinish ? "request-error" : "network-error", session);
|
|
345
|
-
this.recordUsage(session, options.model, {
|
|
346
|
-
ok: false,
|
|
347
|
-
reason: unmappedFinish ? "request-error" : "network-error"
|
|
348
|
-
}, startedAt);
|
|
349
|
-
throw new LlmError(error instanceof Error ? describeFetchError(error) : "agy stream parse failed", UPSTREAM_ERROR_CODE, { cause: error });
|
|
350
464
|
}
|
|
351
465
|
}
|
|
352
466
|
/**
|
|
@@ -557,6 +671,10 @@ var ModelVisibility = class ModelVisibility {
|
|
|
557
671
|
this.reload();
|
|
558
672
|
}
|
|
559
673
|
};
|
|
674
|
+
/** Whether `value` may be stored as a per-file inline cap: an integer MB in range. */
|
|
675
|
+
function isValidMultimodalMb(value) {
|
|
676
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 100;
|
|
677
|
+
}
|
|
560
678
|
//#endregion
|
|
561
679
|
//#region src/thinking-types.ts
|
|
562
680
|
/**
|
|
@@ -823,6 +941,238 @@ var ThinkingBudgetStore = class ThinkingBudgetStore {
|
|
|
823
941
|
return this.snapshot();
|
|
824
942
|
}
|
|
825
943
|
};
|
|
944
|
+
/** Environment override for the cap, in MB. */
|
|
945
|
+
const MULTIMODAL_ENV_VAR = "DSH_AGY_MULTIMODAL_MAX_INLINE_MB";
|
|
946
|
+
/**
|
|
947
|
+
* Rebuild the document, keeping only fields that are set.
|
|
948
|
+
*
|
|
949
|
+
* ONE place builds a document, so `parse`/`snapshot`/`write` cannot disagree
|
|
950
|
+
* about which fields exist: `thinking-budget.ts` spells its document out in each
|
|
951
|
+
* of the three, and a field added to only two of them is silently dropped by the
|
|
952
|
+
* third (that is why its `write` and `snapshot` both enumerate `claudeBudget`
|
|
953
|
+
* and `tieredBudget` by hand). `undefined` is dropped rather than serialized as
|
|
954
|
+
* `null`, so "unset" has exactly one on-disk form.
|
|
955
|
+
*/
|
|
956
|
+
function toDocument(doc) {
|
|
957
|
+
return {
|
|
958
|
+
version: 1,
|
|
959
|
+
...doc.maxInlineMb === void 0 ? {} : { maxInlineMb: doc.maxInlineMb }
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Keep the stored value only when it is usable (tolerates hand edits).
|
|
964
|
+
*
|
|
965
|
+
* Dropped rather than clamped: a clamped cap would silently inline files the
|
|
966
|
+
* user asked to keep out, and the fallback — no stored value — is a valid state
|
|
967
|
+
* that simply means the built-in default.
|
|
968
|
+
*/
|
|
969
|
+
function sanitizeMultimodalDocument(raw) {
|
|
970
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return toDocument({ version: 1 });
|
|
971
|
+
const value = raw.maxInlineMb;
|
|
972
|
+
return toDocument({
|
|
973
|
+
version: 1,
|
|
974
|
+
...isValidMultimodalMb(value) ? { maxInlineMb: value } : {}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
function parseMultimodalDocument(text) {
|
|
978
|
+
let raw;
|
|
979
|
+
try {
|
|
980
|
+
raw = JSON.parse(text);
|
|
981
|
+
} catch {
|
|
982
|
+
return toDocument({ version: 1 });
|
|
983
|
+
}
|
|
984
|
+
return sanitizeMultimodalDocument(raw);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The env override, or undefined when unset or unusable.
|
|
988
|
+
*
|
|
989
|
+
* `DSH_AGY_*` rather than `AGY_*`: that prefix is this plugin's own runtime
|
|
990
|
+
* knobs (`risk.ts`'s kill switch and fingerprint mode, `plugin-common.ts`'s
|
|
991
|
+
* health-probe interval), while `AGY_CLIENT_ID`/`AGY_CLIENT_SECRET` are the
|
|
992
|
+
* OAuth client-credential namespace — a knob that changes request behavior
|
|
993
|
+
* belongs to the former.
|
|
994
|
+
*
|
|
995
|
+
* This is the codebase's first NUMERIC env knob, and it inherits `envFlag`'s
|
|
996
|
+
* posture: anything not recognized yields the fallback (here "no override")
|
|
997
|
+
* rather than throwing or warning, because the fallback chain (store, then
|
|
998
|
+
* default) is always a valid state. Recognized means a plain decimal integer —
|
|
999
|
+
* `30MB`, `1.5` and `1e2` are ignored rather than guessed at, so the value that
|
|
1000
|
+
* takes effect is the one written.
|
|
1001
|
+
*
|
|
1002
|
+
* Read per call, not cached at module load, so a test (or a process that sets it
|
|
1003
|
+
* late) sees the change without a restart.
|
|
1004
|
+
*/
|
|
1005
|
+
function envMaxInlineMb() {
|
|
1006
|
+
const raw = process.env[MULTIMODAL_ENV_VAR];
|
|
1007
|
+
if (raw === void 0) return void 0;
|
|
1008
|
+
const trimmed = raw.trim();
|
|
1009
|
+
if (!/^\d+$/.test(trimmed)) return void 0;
|
|
1010
|
+
const value = Number(trimmed);
|
|
1011
|
+
return isValidMultimodalMb(value) ? value : void 0;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* In-memory authoritative copy of the cap, backed by a JSON file.
|
|
1015
|
+
*
|
|
1016
|
+
* `maxInlineBytes` sits on the GENERATION HOT PATH (the adapter resolves the cap
|
|
1017
|
+
* for every request), so the cross-instance revalidation it needs is
|
|
1018
|
+
* rate-limited — the same window, shared with `thinking-budget.ts` rather than
|
|
1019
|
+
* re-declared: two settings read on one hot path should not drift into two
|
|
1020
|
+
* different staleness bounds. Writes go to the file immediately and bypass the
|
|
1021
|
+
* limit so a concurrent edit is never merged into a stale copy.
|
|
1022
|
+
*
|
|
1023
|
+
* No file lock: a settings write is atomic (tmp+rename) and the accepted loss is
|
|
1024
|
+
* one concurrent toggle, which is the same trade `thinking-budget.ts` makes.
|
|
1025
|
+
*/
|
|
1026
|
+
var MultimodalConfigStore = class MultimodalConfigStore {
|
|
1027
|
+
file;
|
|
1028
|
+
revalidateIntervalMs;
|
|
1029
|
+
doc;
|
|
1030
|
+
/** Raw text this instance last read or wrote; see `reloadNow`. */
|
|
1031
|
+
raw;
|
|
1032
|
+
/** When `reloadNow` last ran, for the hot-path throttle. */
|
|
1033
|
+
checkedAt;
|
|
1034
|
+
constructor(options = {}) {
|
|
1035
|
+
this.file = options.file ?? migrateToAgyDir("agy-multimodal.json").file;
|
|
1036
|
+
this.revalidateIntervalMs = options.revalidateIntervalMs ?? 1e3;
|
|
1037
|
+
const initial = this.readWithRaw();
|
|
1038
|
+
this.doc = initial.doc;
|
|
1039
|
+
this.raw = initial.raw;
|
|
1040
|
+
this.checkedAt = Date.now();
|
|
1041
|
+
}
|
|
1042
|
+
/** Path of the backing file. */
|
|
1043
|
+
get path() {
|
|
1044
|
+
return this.file;
|
|
1045
|
+
}
|
|
1046
|
+
static serialize(doc) {
|
|
1047
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Read the document together with the raw text it came from.
|
|
1051
|
+
*
|
|
1052
|
+
* Content, not mtime: two writers can land in the same filesystem timestamp
|
|
1053
|
+
* tick (Windows resolves it coarsely enough to have failed CI in this repo),
|
|
1054
|
+
* and a missed change is exactly the bug this check exists to prevent. A
|
|
1055
|
+
* missing or unreadable file means "nothing configured" — and must NOT create
|
|
1056
|
+
* it, because a settings read runs in processes that never write.
|
|
1057
|
+
*/
|
|
1058
|
+
readWithRaw() {
|
|
1059
|
+
try {
|
|
1060
|
+
const raw = readFileSync(this.file, "utf8");
|
|
1061
|
+
return {
|
|
1062
|
+
doc: parseMultimodalDocument(raw),
|
|
1063
|
+
raw
|
|
1064
|
+
};
|
|
1065
|
+
} catch {
|
|
1066
|
+
return {
|
|
1067
|
+
doc: toDocument({ version: 1 }),
|
|
1068
|
+
raw: ""
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Re-read the file if another writer changed it.
|
|
1074
|
+
*
|
|
1075
|
+
* Unconditional — callers on the hot path use `reloadIfChanged` instead.
|
|
1076
|
+
*/
|
|
1077
|
+
reloadNow() {
|
|
1078
|
+
let current;
|
|
1079
|
+
try {
|
|
1080
|
+
current = readFileSync(this.file, "utf8");
|
|
1081
|
+
} catch {
|
|
1082
|
+
current = "";
|
|
1083
|
+
}
|
|
1084
|
+
this.checkedAt = Date.now();
|
|
1085
|
+
if (current === this.raw) return;
|
|
1086
|
+
const fresh = this.readWithRaw();
|
|
1087
|
+
this.doc = fresh.doc;
|
|
1088
|
+
this.raw = fresh.raw;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Hot-path revalidation: `reloadNow`, rate-limited.
|
|
1092
|
+
*
|
|
1093
|
+
* Without the limit every generation pays a blocking `readFileSync` plus a
|
|
1094
|
+
* UTF-8 decode for a value that changes only when a user saves the settings
|
|
1095
|
+
* card, so the worst case here is that a change takes up to
|
|
1096
|
+
* `revalidateIntervalMs` to reach an already-running adapter — the right trade
|
|
1097
|
+
* for a setting, and not right for the write path, which is why `write` calls
|
|
1098
|
+
* `reloadNow` directly.
|
|
1099
|
+
*/
|
|
1100
|
+
reloadIfChanged() {
|
|
1101
|
+
if (Date.now() - this.checkedAt < this.revalidateIntervalMs) return;
|
|
1102
|
+
this.reloadNow();
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Write the whole document atomically.
|
|
1106
|
+
*
|
|
1107
|
+
* Always re-reads first, so a concurrent edit by the OTHER instance in this
|
|
1108
|
+
* process (main plugin + web entry) is merged rather than overwritten: the
|
|
1109
|
+
* caller passes a mutation, not a full replacement built from a stale copy.
|
|
1110
|
+
*/
|
|
1111
|
+
write(mutate) {
|
|
1112
|
+
this.reloadNow();
|
|
1113
|
+
const next = toDocument(this.doc);
|
|
1114
|
+
mutate(next);
|
|
1115
|
+
const text = MultimodalConfigStore.serialize(next);
|
|
1116
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
1117
|
+
const tmp = `${this.file}.tmp`;
|
|
1118
|
+
writeFileSync(tmp, text, { mode: 384 });
|
|
1119
|
+
renameSync(tmp, this.file);
|
|
1120
|
+
this.doc = next;
|
|
1121
|
+
this.raw = text;
|
|
1122
|
+
}
|
|
1123
|
+
/** The STORED cap in MB, or undefined when nothing is stored. */
|
|
1124
|
+
maxInlineMb() {
|
|
1125
|
+
this.reloadIfChanged();
|
|
1126
|
+
return this.doc.maxInlineMb;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* The cap actually in force: env overrides the stored value, which overrides
|
|
1130
|
+
* the built-in default.
|
|
1131
|
+
*
|
|
1132
|
+
* The env var is read HERE rather than at construction so the override is
|
|
1133
|
+
* live for a long-running process and flippable in a test.
|
|
1134
|
+
*/
|
|
1135
|
+
effectiveMaxInlineMb() {
|
|
1136
|
+
const fromEnv = envMaxInlineMb();
|
|
1137
|
+
if (fromEnv !== void 0) return {
|
|
1138
|
+
value: fromEnv,
|
|
1139
|
+
source: "env"
|
|
1140
|
+
};
|
|
1141
|
+
const stored = this.maxInlineMb();
|
|
1142
|
+
if (stored !== void 0) return {
|
|
1143
|
+
value: stored,
|
|
1144
|
+
source: "stored"
|
|
1145
|
+
};
|
|
1146
|
+
return {
|
|
1147
|
+
value: 20,
|
|
1148
|
+
source: "default"
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
/** The effective cap in bytes, as the multimodal resolver wants it. */
|
|
1152
|
+
maxInlineBytes() {
|
|
1153
|
+
return this.effectiveMaxInlineMb().value * 1024 * 1024;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* The settings state: the stored value (null when unset) plus which source is
|
|
1157
|
+
* effective. See `MultimodalView` for why the two are separate fields.
|
|
1158
|
+
*/
|
|
1159
|
+
snapshot() {
|
|
1160
|
+
return {
|
|
1161
|
+
value: this.maxInlineMb() ?? null,
|
|
1162
|
+
source: this.effectiveMaxInlineMb().source,
|
|
1163
|
+
max: 100
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
/** Replace the cap, or clear it when `value` is undefined. */
|
|
1167
|
+
setMaxInlineMb(value) {
|
|
1168
|
+
if (value !== void 0 && !isValidMultimodalMb(value)) throw new Error(`multimodal inline cap must be an integer in [1, 100] MB`);
|
|
1169
|
+
this.write((doc) => {
|
|
1170
|
+
if (value === void 0) delete doc.maxInlineMb;
|
|
1171
|
+
else doc.maxInlineMb = value;
|
|
1172
|
+
});
|
|
1173
|
+
return this.snapshot();
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
826
1176
|
//#endregion
|
|
827
1177
|
//#region src/usage-types.ts
|
|
828
1178
|
/** Zeroed counters, for accumulation. */
|
|
@@ -1590,6 +1940,7 @@ async function buildAgyRuntime(ctx) {
|
|
|
1590
1940
|
});
|
|
1591
1941
|
const modelVisibility = new ModelVisibility();
|
|
1592
1942
|
const thinkingBudget = new ThinkingBudgetStore();
|
|
1943
|
+
const multimodalConfig = new MultimodalConfigStore();
|
|
1593
1944
|
let invalidateModelCache = () => {};
|
|
1594
1945
|
const sessions = new AgySessionManager({
|
|
1595
1946
|
store,
|
|
@@ -1613,6 +1964,7 @@ async function buildAgyRuntime(ctx) {
|
|
|
1613
1964
|
thinkingBudgetFor: (level) => thinkingBudget.budgetFor(level),
|
|
1614
1965
|
claudeBudgetFor: () => thinkingBudget.claudeBudget(),
|
|
1615
1966
|
tieredBudgetFor: () => thinkingBudget.tieredBudget(),
|
|
1967
|
+
maxInlineBytes: () => multimodalConfig.maxInlineBytes(),
|
|
1616
1968
|
recordUsage: (record) => {
|
|
1617
1969
|
stats.record({
|
|
1618
1970
|
...record,
|
|
@@ -1635,10 +1987,11 @@ async function buildAgyRuntime(ctx) {
|
|
|
1635
1987
|
stats,
|
|
1636
1988
|
recentStore,
|
|
1637
1989
|
modelVisibility,
|
|
1638
|
-
thinkingBudget
|
|
1990
|
+
thinkingBudget,
|
|
1991
|
+
multimodalConfig
|
|
1639
1992
|
};
|
|
1640
1993
|
}
|
|
1641
1994
|
//#endregion
|
|
1642
1995
|
export { CLAUDE_BUDGET_MIN as a, CLAUDE_BUDGET_MAX as i, dayKey as n, THINKING_BUDGET_MAX as o, foldWindowBreakdown as r, createAgyRuntime as t };
|
|
1643
1996
|
|
|
1644
|
-
//# sourceMappingURL=plugin-common-
|
|
1997
|
+
//# sourceMappingURL=plugin-common-BlGbNHxX.mjs.map
|