dsh-packycode-api 1.1.8 → 1.2.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 +20 -0
- package/README.en.md +32 -4
- package/README.md +29 -5
- package/docs/research/image-api.md +75 -0
- package/lib/client.js +727 -108
- package/lib/index.js +1037 -216
- package/package.json +8 -4
package/lib/index.js
CHANGED
|
@@ -2,19 +2,22 @@ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, posix, resolve, sep, win32 } from "node:path";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
|
-
import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { HarnessError, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, createUserMessage, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, statSync } from "node:fs";
|
|
8
|
-
import { mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
11
|
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
14
|
-
import {
|
|
14
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
15
|
+
import { lazyStream } from "@earendil-works/pi-ai/api/lazy";
|
|
16
|
+
import { clampThinkingLevel, createProvider } from "@earendil-works/pi-ai";
|
|
15
17
|
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
16
18
|
import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
17
19
|
import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.lazy";
|
|
20
|
+
import { googleGenerativeAIApi } from "@earendil-works/pi-ai/api/google-generative-ai.lazy";
|
|
18
21
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19
22
|
//#region src/request.js
|
|
20
23
|
/**
|
|
@@ -181,6 +184,8 @@ const CHANNEL = "/packycode-api";
|
|
|
181
184
|
const SETTINGS_NAMESPACE = "packycode-api";
|
|
182
185
|
/** Credential record id inside the plugin's own scope. */
|
|
183
186
|
const KEY_RECORD_ID = "keys";
|
|
187
|
+
/** Image credentials never participate in the conversation key ring. */
|
|
188
|
+
const IMAGE_KEY_RECORD_ID = "image-keys";
|
|
184
189
|
/**
|
|
185
190
|
* Model-traffic host. Each model declares the protocol its gateway route
|
|
186
191
|
* actually serves: `cf.api.fan/v1` answers chat/completions and responses,
|
|
@@ -267,6 +272,51 @@ function accountHostOrder(hosts) {
|
|
|
267
272
|
]);
|
|
268
273
|
}
|
|
269
274
|
//#endregion
|
|
275
|
+
//#region src/secret-redactor.js
|
|
276
|
+
/**
|
|
277
|
+
* Redaction of plugin-owned credentials and ephemeral bridge capabilities.
|
|
278
|
+
* Work on decoded strings where possible; JSON-escaped forms also cover
|
|
279
|
+
* diagnostics containing serialized data. Delta redaction holds only the
|
|
280
|
+
* longest suffix that could still become a secret in the next chunk.
|
|
281
|
+
*/
|
|
282
|
+
const strings = (values) => [...new Set(values.filter((value) => typeof value === "string" && value !== ""))].sort((a, b) => b.length - a.length);
|
|
283
|
+
const variants = (values) => strings(values.flatMap((value) => typeof value === "string" ? [value, JSON.stringify(value).slice(1, -1)] : []));
|
|
284
|
+
function redactSecrets(text, values) {
|
|
285
|
+
let safe = String(text);
|
|
286
|
+
for (const secret of variants(values)) safe = safe.split(secret).join("[redacted]");
|
|
287
|
+
return safe;
|
|
288
|
+
}
|
|
289
|
+
/** Protect secrets fragmented across consecutive decoded text deltas. */
|
|
290
|
+
function createDeltaRedactor(values) {
|
|
291
|
+
const secrets = variants(values);
|
|
292
|
+
let pending = "";
|
|
293
|
+
return {
|
|
294
|
+
write(text) {
|
|
295
|
+
const safe = redactSecrets(pending + text, secrets);
|
|
296
|
+
let held = 0;
|
|
297
|
+
for (const secret of secrets) {
|
|
298
|
+
const maximum = Math.min(safe.length, secret.length - 1);
|
|
299
|
+
for (let length = maximum; length > held; length--) if (safe.endsWith(secret.slice(0, length))) {
|
|
300
|
+
held = length;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
pending = held === 0 ? "" : safe.slice(-held);
|
|
305
|
+
return held === 0 ? safe : safe.slice(0, -held);
|
|
306
|
+
},
|
|
307
|
+
/**
|
|
308
|
+
* At a block/tool boundary, mask a still-ambiguous prefix so a subsequent
|
|
309
|
+
* block cannot reconstruct the credential. At end of output it is safe
|
|
310
|
+
* to flush an incomplete prefix literally; no later delta can complete it.
|
|
311
|
+
*/
|
|
312
|
+
flush({ boundary = false } = {}) {
|
|
313
|
+
const result = boundary && pending !== "" ? "[redacted]" : pending;
|
|
314
|
+
pending = "";
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
270
320
|
//#region src/balance.js
|
|
271
321
|
/**
|
|
272
322
|
* Account and per-key balance.
|
|
@@ -373,7 +423,11 @@ async function getJson(url, key, { fetchImpl, timeoutMs, signal }) {
|
|
|
373
423
|
throw new Error(error.message, { cause: error });
|
|
374
424
|
}
|
|
375
425
|
if (!response.ok) throw new Error(statusMessage(response.status));
|
|
376
|
-
|
|
426
|
+
try {
|
|
427
|
+
return await response.json();
|
|
428
|
+
} catch {
|
|
429
|
+
throw new Error("the gateway returned unreadable balance data");
|
|
430
|
+
}
|
|
377
431
|
}
|
|
378
432
|
async function withFallback(hosts, path, key, options) {
|
|
379
433
|
const tried = [];
|
|
@@ -398,7 +452,7 @@ async function withFallback(hosts, path, key, options) {
|
|
|
398
452
|
* @param options - key listing, host resolution, and transport knobs.
|
|
399
453
|
* @returns `{ read({force, signal}), invalidate() }`.
|
|
400
454
|
*/
|
|
401
|
-
function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now = Date.now, ttlMs = DEFAULT_BALANCE_TTL_MS, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
455
|
+
function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now = Date.now, ttlMs = DEFAULT_BALANCE_TTL_MS, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, includeAccount = true } = {}) {
|
|
402
456
|
if (typeof listKeys !== "function") throw new Error("PackyCode balance reader requires listKeys");
|
|
403
457
|
if (typeof getHosts !== "function") throw new Error("PackyCode balance reader requires getHosts");
|
|
404
458
|
/** Last successful per-key usage, by key id, and the last account figures. */
|
|
@@ -455,17 +509,21 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
455
509
|
signal?.throwIfAborted();
|
|
456
510
|
const hosts = getHosts();
|
|
457
511
|
const keys = await listKeys();
|
|
512
|
+
const secrets = keys.map((entry) => entry.key);
|
|
513
|
+
const safe = (value) => redactSecrets(value, secrets);
|
|
458
514
|
for (const id of lastGood.keys()) if (!keys.some((entry) => entry.id === id)) lastGood.delete(id);
|
|
459
515
|
const entries = [];
|
|
460
516
|
const usableIds = [];
|
|
461
517
|
for (const entry of keys) {
|
|
462
518
|
const base = {
|
|
463
519
|
id: entry.id,
|
|
464
|
-
label: entry.label,
|
|
520
|
+
label: safe(entry.label),
|
|
465
521
|
disabled: entry.disabled === true
|
|
466
522
|
};
|
|
467
523
|
try {
|
|
468
524
|
const usage = await loadKey(entry, hosts, signal);
|
|
525
|
+
if (usage.name !== void 0) usage.name = safe(usage.name);
|
|
526
|
+
if (usage.resetPeriod !== void 0) usage.resetPeriod = safe(usage.resetPeriod);
|
|
469
527
|
lastGood.set(entry.id, {
|
|
470
528
|
usage,
|
|
471
529
|
fetchedAt: now()
|
|
@@ -477,7 +535,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
477
535
|
usableIds.push(entry.id);
|
|
478
536
|
} catch (error) {
|
|
479
537
|
signal?.throwIfAborted();
|
|
480
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
538
|
+
const message = safe(error instanceof Error ? error.message : String(error));
|
|
481
539
|
const previous = lastGood.get(entry.id);
|
|
482
540
|
entries.push(previous === void 0 ? {
|
|
483
541
|
...base,
|
|
@@ -493,7 +551,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
493
551
|
}
|
|
494
552
|
}
|
|
495
553
|
let account;
|
|
496
|
-
try {
|
|
554
|
+
if (includeAccount) try {
|
|
497
555
|
account = await loadAccount(keys, hosts, signal, usableIds);
|
|
498
556
|
lastAccount = {
|
|
499
557
|
account,
|
|
@@ -501,7 +559,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
501
559
|
};
|
|
502
560
|
} catch (error) {
|
|
503
561
|
signal?.throwIfAborted();
|
|
504
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
562
|
+
const message = safe(error instanceof Error ? error.message : String(error));
|
|
505
563
|
account = lastAccount === void 0 || !keys.some((entry) => entry.id === lastAccount.account.keyId) ? { error: message } : {
|
|
506
564
|
...lastAccount.account,
|
|
507
565
|
stale: {
|
|
@@ -512,7 +570,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
512
570
|
}
|
|
513
571
|
return {
|
|
514
572
|
keys: entries,
|
|
515
|
-
account,
|
|
573
|
+
...includeAccount ? { account } : {},
|
|
516
574
|
hosts,
|
|
517
575
|
fetchedAt: now()
|
|
518
576
|
};
|
|
@@ -544,8 +602,9 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = ambientFetch, now
|
|
|
544
602
|
* model does not support is refused before the request is sent: Claude Opus
|
|
545
603
|
* 5.5 (platform.claude.com effort docs), GPT-6.1 Sol and GPT-6 Astra
|
|
546
604
|
* `reasoning.effort` (developers.openai.com model pages), DeepSeek thinking
|
|
547
|
-
* mode (`thinking` toggle plus `low|high|max` effort),
|
|
548
|
-
* (docs.x.ai/developers/grok-4-7)
|
|
605
|
+
* mode (`thinking` toggle plus `low|high|max` effort), Grok 4.7
|
|
606
|
+
* (docs.x.ai/developers/grok-4-7), and Gemini 4 Argon
|
|
607
|
+
* (ai.google.dev/gemini-api/docs/models/gemini-4-argon and docs/openai).
|
|
549
608
|
*
|
|
550
609
|
* @module dsh-packycode-api/catalog
|
|
551
610
|
*/
|
|
@@ -559,6 +618,7 @@ const NO_COST = Object.freeze({
|
|
|
559
618
|
const COMPLETIONS_BASE = `${DEFAULT_API_HOST}/v1`;
|
|
560
619
|
const RESPONSES_BASE = `${DEFAULT_API_HOST}/v1`;
|
|
561
620
|
const MESSAGES_BASE = DEFAULT_API_HOST;
|
|
621
|
+
const GEMINI_BASE = `${DEFAULT_API_HOST}/v1beta`;
|
|
562
622
|
/**
|
|
563
623
|
* pi-ai thinking-level maps. A level pinned to `null` is unsupported; an
|
|
564
624
|
* absent key keeps pi-ai's own default for that level, which is how `off`
|
|
@@ -592,6 +652,12 @@ const GROK_LEVELS = Object.freeze({
|
|
|
592
652
|
...ALWAYS_THINKING_LEVELS,
|
|
593
653
|
max: null
|
|
594
654
|
});
|
|
655
|
+
/** Gemini 4 Argon and 3.8 Flash support low | medium | high, with no off. */
|
|
656
|
+
const GEMINI_LEVELS = Object.freeze({
|
|
657
|
+
...ALWAYS_THINKING_LEVELS,
|
|
658
|
+
xhigh: null,
|
|
659
|
+
max: null
|
|
660
|
+
});
|
|
595
661
|
const PACKYCODE_MODELS = Object.freeze([
|
|
596
662
|
Object.freeze({
|
|
597
663
|
id: "deepseek-flash",
|
|
@@ -652,6 +718,32 @@ const PACKYCODE_MODELS = Object.freeze([
|
|
|
652
718
|
thinkingLevelMap: GROK_LEVELS,
|
|
653
719
|
compat: Object.freeze({ supportsLongCacheRetention: false })
|
|
654
720
|
}),
|
|
721
|
+
Object.freeze({
|
|
722
|
+
id: "gemini-4-argon",
|
|
723
|
+
name: "Gemini 4 Argon",
|
|
724
|
+
api: "google-generative-ai",
|
|
725
|
+
provider: PROVIDER,
|
|
726
|
+
baseUrl: GEMINI_BASE,
|
|
727
|
+
reasoning: true,
|
|
728
|
+
input: Object.freeze(["text", "image"]),
|
|
729
|
+
cost: NO_COST,
|
|
730
|
+
contextWindow: 1048576,
|
|
731
|
+
maxTokens: 65536,
|
|
732
|
+
thinkingLevelMap: GEMINI_LEVELS
|
|
733
|
+
}),
|
|
734
|
+
Object.freeze({
|
|
735
|
+
id: "gemini-3.8-flash",
|
|
736
|
+
name: "Gemini 3.8 Flash",
|
|
737
|
+
api: "google-generative-ai",
|
|
738
|
+
provider: PROVIDER,
|
|
739
|
+
baseUrl: GEMINI_BASE,
|
|
740
|
+
reasoning: true,
|
|
741
|
+
input: Object.freeze(["text", "image"]),
|
|
742
|
+
cost: NO_COST,
|
|
743
|
+
contextWindow: 1048576,
|
|
744
|
+
maxTokens: 65536,
|
|
745
|
+
thinkingLevelMap: GEMINI_LEVELS
|
|
746
|
+
}),
|
|
655
747
|
Object.freeze({
|
|
656
748
|
id: "claude-opus-5-5",
|
|
657
749
|
name: "Claude Opus 5.5",
|
|
@@ -2188,51 +2280,6 @@ function withProgramDirOnPath(env, program, platform = process.platform) {
|
|
|
2188
2280
|
};
|
|
2189
2281
|
}
|
|
2190
2282
|
//#endregion
|
|
2191
|
-
//#region src/secret-redactor.js
|
|
2192
|
-
/**
|
|
2193
|
-
* Redaction of plugin-owned credentials and ephemeral bridge capabilities.
|
|
2194
|
-
* Work on decoded strings where possible; JSON-escaped forms also cover
|
|
2195
|
-
* diagnostics containing serialized data. Delta redaction holds only the
|
|
2196
|
-
* longest suffix that could still become a secret in the next chunk.
|
|
2197
|
-
*/
|
|
2198
|
-
const strings = (values) => [...new Set(values.filter((value) => typeof value === "string" && value !== ""))].sort((a, b) => b.length - a.length);
|
|
2199
|
-
const variants = (values) => strings(values.flatMap((value) => typeof value === "string" ? [value, JSON.stringify(value).slice(1, -1)] : []));
|
|
2200
|
-
function redactSecrets(text, values) {
|
|
2201
|
-
let safe = String(text);
|
|
2202
|
-
for (const secret of variants(values)) safe = safe.split(secret).join("[redacted]");
|
|
2203
|
-
return safe;
|
|
2204
|
-
}
|
|
2205
|
-
/** Protect secrets fragmented across consecutive decoded text deltas. */
|
|
2206
|
-
function createDeltaRedactor(values) {
|
|
2207
|
-
const secrets = variants(values);
|
|
2208
|
-
let pending = "";
|
|
2209
|
-
return {
|
|
2210
|
-
write(text) {
|
|
2211
|
-
const safe = redactSecrets(pending + text, secrets);
|
|
2212
|
-
let held = 0;
|
|
2213
|
-
for (const secret of secrets) {
|
|
2214
|
-
const maximum = Math.min(safe.length, secret.length - 1);
|
|
2215
|
-
for (let length = maximum; length > held; length--) if (safe.endsWith(secret.slice(0, length))) {
|
|
2216
|
-
held = length;
|
|
2217
|
-
break;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
pending = held === 0 ? "" : safe.slice(-held);
|
|
2221
|
-
return held === 0 ? safe : safe.slice(0, -held);
|
|
2222
|
-
},
|
|
2223
|
-
/**
|
|
2224
|
-
* At a block/tool boundary, mask a still-ambiguous prefix so a subsequent
|
|
2225
|
-
* block cannot reconstruct the credential. At end of output it is safe
|
|
2226
|
-
* to flush an incomplete prefix literally; no later delta can complete it.
|
|
2227
|
-
*/
|
|
2228
|
-
flush({ boundary = false } = {}) {
|
|
2229
|
-
const result = boundary && pending !== "" ? "[redacted]" : pending;
|
|
2230
|
-
pending = "";
|
|
2231
|
-
return result;
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2235
|
-
//#endregion
|
|
2236
2283
|
//#region src/claude-cli.js
|
|
2237
2284
|
/**
|
|
2238
2285
|
* Bridge to the local Claude Code CLI.
|
|
@@ -4542,68 +4589,760 @@ function createClaudeCodeTool({ runner, modeCeiling, planActive }) {
|
|
|
4542
4589
|
});
|
|
4543
4590
|
}
|
|
4544
4591
|
//#endregion
|
|
4545
|
-
//#region src/
|
|
4592
|
+
//#region src/image-models.js
|
|
4593
|
+
/** Images API models are separate from the conversational pi-ai catalog. */
|
|
4594
|
+
const DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
|
4595
|
+
const IMAGE_MODELS = Object.freeze({
|
|
4596
|
+
"gpt-image-2": Object.freeze([
|
|
4597
|
+
"auto",
|
|
4598
|
+
"low",
|
|
4599
|
+
"medium",
|
|
4600
|
+
"high"
|
|
4601
|
+
]),
|
|
4602
|
+
"gpt-image-2.5-flare": Object.freeze([
|
|
4603
|
+
"auto",
|
|
4604
|
+
"low",
|
|
4605
|
+
"medium",
|
|
4606
|
+
"high",
|
|
4607
|
+
"xhigh",
|
|
4608
|
+
"max"
|
|
4609
|
+
]),
|
|
4610
|
+
"gpt-image-2.5-sunburst": Object.freeze([
|
|
4611
|
+
"auto",
|
|
4612
|
+
"low",
|
|
4613
|
+
"medium",
|
|
4614
|
+
"high",
|
|
4615
|
+
"xhigh",
|
|
4616
|
+
"max"
|
|
4617
|
+
])
|
|
4618
|
+
});
|
|
4619
|
+
const IMAGE_MODEL_IDS = Object.freeze(Object.keys(IMAGE_MODELS));
|
|
4620
|
+
const IMAGE_QUALITIES = Object.freeze([
|
|
4621
|
+
"auto",
|
|
4622
|
+
"low",
|
|
4623
|
+
"medium",
|
|
4624
|
+
"high",
|
|
4625
|
+
"xhigh",
|
|
4626
|
+
"max"
|
|
4627
|
+
]);
|
|
4628
|
+
function validateImageOptions({ model = DEFAULT_IMAGE_MODEL, quality = "auto", size = "auto", background = "auto" } = {}) {
|
|
4629
|
+
if (!IMAGE_MODEL_IDS.includes(model)) throw new Error("PackyCode image model is not supported");
|
|
4630
|
+
if (!IMAGE_MODELS[model].includes(quality)) throw new Error("PackyCode image quality is not supported by the selected model");
|
|
4631
|
+
if (![
|
|
4632
|
+
"auto",
|
|
4633
|
+
"transparent",
|
|
4634
|
+
"opaque"
|
|
4635
|
+
].includes(background)) throw new Error("PackyCode image background is not supported");
|
|
4636
|
+
if (size !== "auto") {
|
|
4637
|
+
const match = typeof size === "string" ? /^(\d{1,4})x(\d{1,4})$/u.exec(size) : null;
|
|
4638
|
+
const width = Number(match?.[1]);
|
|
4639
|
+
const height = Number(match?.[2]);
|
|
4640
|
+
const pixels = width * height;
|
|
4641
|
+
if (match === null || width % 16 !== 0 || height % 16 !== 0 || Math.max(width, height) > 3840 || Math.max(width, height) > Math.min(width, height) * 3 || pixels < 655360 || pixels > 8294400) throw new Error("PackyCode image size must be auto or a supported widthxheight resolution");
|
|
4642
|
+
}
|
|
4643
|
+
return {
|
|
4644
|
+
model,
|
|
4645
|
+
quality,
|
|
4646
|
+
size,
|
|
4647
|
+
background
|
|
4648
|
+
};
|
|
4649
|
+
}
|
|
4650
|
+
//#endregion
|
|
4651
|
+
//#region src/preferences.js
|
|
4546
4652
|
/**
|
|
4547
|
-
*
|
|
4548
|
-
*
|
|
4653
|
+
* Where the plugin's preferences are read from and written to, on both
|
|
4654
|
+
* settings generations DSH has shipped.
|
|
4549
4655
|
*
|
|
4550
|
-
*
|
|
4551
|
-
*
|
|
4552
|
-
*
|
|
4553
|
-
*
|
|
4554
|
-
*
|
|
4555
|
-
*
|
|
4656
|
+
* - DSH 0.1.5 installs a settings *section*: the section hands back a live
|
|
4657
|
+
* source and writes go through `settings.update(namespace, patch)`.
|
|
4658
|
+
* - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
|
|
4659
|
+
* plugin's own row, read through a live reference, written through
|
|
4660
|
+
* `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
|
|
4661
|
+
* Only volatile fields are writable there, so the host overrides stay
|
|
4662
|
+
* read-only while web search and model visibility are editable.
|
|
4556
4663
|
*
|
|
4557
|
-
* @module dsh-packycode-api/
|
|
4664
|
+
* @module dsh-packycode-api/preferences
|
|
4558
4665
|
*/
|
|
4559
|
-
/** How long a request with a choice of keys waits for the first group reading. */
|
|
4560
|
-
const DEFAULT_GROUP_WAIT_MS = 6e3;
|
|
4561
4666
|
/**
|
|
4562
|
-
*
|
|
4563
|
-
*
|
|
4564
|
-
*
|
|
4667
|
+
* Mark a preference editable without restarting the plugin. DSH 0.2 stores
|
|
4668
|
+
* plugin preferences as volatile Config fields and writes only those, so the
|
|
4669
|
+
* schema version that implements `volatile()` is a peer requirement.
|
|
4670
|
+
*
|
|
4671
|
+
* Name the cause when it is missing instead of letting a bare
|
|
4672
|
+
* `schema.volatile is not a function` reach the host log: the Loader creates
|
|
4673
|
+
* no fiber for a plugin whose module throws while it is imported, so DSH shows
|
|
4674
|
+
* nothing at all — no settings section, no model route, no RPC channel — and
|
|
4675
|
+
* the only explanation is that one log line.
|
|
4565
4676
|
*/
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4677
|
+
const live = (schema) => {
|
|
4678
|
+
if (typeof schema?.volatile !== "function") throw new Error("PackyCode needs @deepseek-ai/schemastery ^3.18.4, which implements Schema.volatile(); the copy resolved for this plugin is older. A plugin installed from a registry has no node_modules of its own, so this usually means a stale dependency tree inside the installed package directory (for example <profile>/node_modules/dsh-packycode-api/node_modules, left by an earlier local-path install) is shadowing the DSH installation. Remove that directory, or uninstall the plugin and install it again, then restart DSH.");
|
|
4679
|
+
return schema.volatile();
|
|
4680
|
+
};
|
|
4681
|
+
/** Read a Config field that may arrive as a volatile reference. */
|
|
4682
|
+
const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
4683
|
+
/** A preference target nothing can write to yet. */
|
|
4684
|
+
function unwritable(message) {
|
|
4685
|
+
return {
|
|
4686
|
+
writable: false,
|
|
4687
|
+
set: async () => {
|
|
4688
|
+
throw new Error(message);
|
|
4689
|
+
}
|
|
4579
4690
|
};
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Connect the preferences to the settings service the host provides.
|
|
4694
|
+
* @param settingsCtx - the scope the `settings` inject handed over.
|
|
4695
|
+
* @param options - the plugin context and Config, the section namespace, the
|
|
4696
|
+
* current source and how to replace it, what to do when the section changes,
|
|
4697
|
+
* and the preference targets this fills in (host overrides, search, models).
|
|
4698
|
+
*/
|
|
4699
|
+
function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference, imagePreference }) {
|
|
4700
|
+
const settings = settingsCtx.settings;
|
|
4701
|
+
if (typeof settings?.installSection === "function") {
|
|
4702
|
+
try {
|
|
4703
|
+
settings.installSection(ctx, namespace, Config, source(), {
|
|
4704
|
+
setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
|
|
4705
|
+
onChange
|
|
4706
|
+
});
|
|
4707
|
+
if (typeof settings.update === "function") {
|
|
4708
|
+
const write = (patch) => settings.update(namespace, patch);
|
|
4709
|
+
const target = {
|
|
4710
|
+
writable: settings.writable !== false,
|
|
4711
|
+
set: write
|
|
4712
|
+
};
|
|
4713
|
+
Object.assign(hostPreference, target);
|
|
4714
|
+
Object.assign(searchPreference, target);
|
|
4715
|
+
Object.assign(modelPreference, target);
|
|
4716
|
+
if (imagePreference !== void 0) Object.assign(imagePreference, target);
|
|
4717
|
+
}
|
|
4718
|
+
} catch (error) {
|
|
4719
|
+
ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
|
|
4720
|
+
}
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
if (typeof settings?.update !== "function") return;
|
|
4724
|
+
const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
|
|
4725
|
+
const target = {
|
|
4726
|
+
writable: settings.writable !== false,
|
|
4727
|
+
set: (patch) => settings.update(rowId, patch)
|
|
4596
4728
|
};
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4729
|
+
Object.assign(searchPreference, target);
|
|
4730
|
+
Object.assign(modelPreference, target);
|
|
4731
|
+
if (imagePreference !== void 0) Object.assign(imagePreference, target);
|
|
4732
|
+
if (typeof settings.configure === "function") {
|
|
4733
|
+
const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
|
|
4734
|
+
try {
|
|
4735
|
+
scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
|
|
4736
|
+
} catch (error) {
|
|
4737
|
+
ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
//#endregion
|
|
4742
|
+
//#region src/image-settings.js
|
|
4743
|
+
function readImagePreference(value = {}) {
|
|
4744
|
+
const model = fieldValue(value.imageModel);
|
|
4745
|
+
const imageModel = IMAGE_MODEL_IDS.includes(model) ? model : DEFAULT_IMAGE_MODEL;
|
|
4746
|
+
const quality = fieldValue(value.imageQuality);
|
|
4747
|
+
return {
|
|
4748
|
+
imageEnabled: fieldValue(value.imageEnabled) !== false,
|
|
4749
|
+
imageModel,
|
|
4750
|
+
imageQuality: IMAGE_MODELS[imageModel].includes(quality) ? quality : "auto"
|
|
4603
4751
|
};
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4752
|
+
}
|
|
4753
|
+
/** No request is sent by a settings or credential mutation. */
|
|
4754
|
+
function createImageController({ ring, balance, readPreference, writePreference, isWritable }) {
|
|
4755
|
+
let mounted;
|
|
4756
|
+
let disposeTool;
|
|
4757
|
+
let generation = 0;
|
|
4758
|
+
let closed = false;
|
|
4759
|
+
const preference = () => readImagePreference(readPreference());
|
|
4760
|
+
const reconcile = async () => {
|
|
4761
|
+
const current = ++generation;
|
|
4762
|
+
const key = await ring.activeKey();
|
|
4763
|
+
if (closed || current !== generation) return;
|
|
4764
|
+
const enabled = preference().imageEnabled && key !== void 0 && mounted !== void 0;
|
|
4765
|
+
if (enabled && disposeTool === void 0) disposeTool = mounted();
|
|
4766
|
+
if (!enabled && disposeTool !== void 0) {
|
|
4767
|
+
disposeTool();
|
|
4768
|
+
disposeTool = void 0;
|
|
4769
|
+
}
|
|
4770
|
+
};
|
|
4771
|
+
const status = async () => ({
|
|
4772
|
+
...preference(),
|
|
4773
|
+
...await ring.list(),
|
|
4774
|
+
writable: isWritable() === true,
|
|
4775
|
+
available: mounted !== void 0,
|
|
4776
|
+
toolRegistered: disposeTool !== void 0,
|
|
4777
|
+
models: IMAGE_MODEL_IDS.map((id) => ({
|
|
4778
|
+
id,
|
|
4779
|
+
qualities: [...IMAGE_MODELS[id]]
|
|
4780
|
+
}))
|
|
4781
|
+
});
|
|
4782
|
+
return {
|
|
4783
|
+
preference,
|
|
4784
|
+
status,
|
|
4785
|
+
invalidateBalance: () => balance?.invalidate(),
|
|
4786
|
+
async readBalance({ force = false, signal } = {}) {
|
|
4787
|
+
if (balance === void 0) throw new Error("PackyCode image balance is unavailable in this runtime");
|
|
4788
|
+
try {
|
|
4789
|
+
const [value, state] = await Promise.all([balance.read({
|
|
4790
|
+
force,
|
|
4791
|
+
signal
|
|
4792
|
+
}), ring.list()]);
|
|
4793
|
+
return {
|
|
4794
|
+
...value,
|
|
4795
|
+
activeId: state.activeId
|
|
4796
|
+
};
|
|
4797
|
+
} catch {
|
|
4798
|
+
throw new Error("PackyCode image balance could not be read");
|
|
4799
|
+
}
|
|
4800
|
+
},
|
|
4801
|
+
reconcile,
|
|
4802
|
+
mount(register) {
|
|
4803
|
+
mounted = register;
|
|
4804
|
+
closed = false;
|
|
4805
|
+
reconcile().catch(() => void 0);
|
|
4806
|
+
return () => {
|
|
4807
|
+
closed = true;
|
|
4808
|
+
generation += 1;
|
|
4809
|
+
mounted = void 0;
|
|
4810
|
+
disposeTool?.();
|
|
4811
|
+
disposeTool = void 0;
|
|
4812
|
+
};
|
|
4813
|
+
},
|
|
4814
|
+
async update(value = {}) {
|
|
4815
|
+
if (isWritable() !== true) throw new Error("PackyCode image settings are not writable in this profile");
|
|
4816
|
+
const patch = {};
|
|
4817
|
+
if (Object.hasOwn(value, "imageEnabled")) {
|
|
4818
|
+
if (typeof value.imageEnabled !== "boolean") throw new Error("PackyCode image switch must be a boolean");
|
|
4819
|
+
patch.imageEnabled = value.imageEnabled;
|
|
4820
|
+
}
|
|
4821
|
+
if (Object.hasOwn(value, "imageModel")) patch.imageModel = value.imageModel;
|
|
4822
|
+
if (Object.hasOwn(value, "imageQuality")) patch.imageQuality = value.imageQuality;
|
|
4823
|
+
const next = {
|
|
4824
|
+
...preference(),
|
|
4825
|
+
...patch
|
|
4826
|
+
};
|
|
4827
|
+
if (patch.imageModel !== void 0 && patch.imageQuality === void 0 && !IMAGE_MODELS[patch.imageModel]?.includes(next.imageQuality)) patch.imageQuality = next.imageQuality = "auto";
|
|
4828
|
+
validateImageOptions({
|
|
4829
|
+
model: next.imageModel,
|
|
4830
|
+
quality: next.imageQuality
|
|
4831
|
+
});
|
|
4832
|
+
try {
|
|
4833
|
+
await writePreference(patch);
|
|
4834
|
+
} catch {
|
|
4835
|
+
throw new Error("PackyCode could not save the image settings");
|
|
4836
|
+
}
|
|
4837
|
+
await reconcile();
|
|
4838
|
+
return status();
|
|
4839
|
+
},
|
|
4840
|
+
async mutate(operation, payload = {}) {
|
|
4841
|
+
const operations = {
|
|
4842
|
+
add: () => ring.add(payload.key, payload.label),
|
|
4843
|
+
remove: () => ring.remove(String(payload.id ?? "")),
|
|
4844
|
+
select: () => ring.select(String(payload.id ?? "")),
|
|
4845
|
+
enable: () => ring.enable(String(payload.id ?? ""))
|
|
4846
|
+
};
|
|
4847
|
+
if (!Object.hasOwn(operations, operation)) throw new Error("Unknown PackyCode image key operation");
|
|
4848
|
+
await operations[operation]();
|
|
4849
|
+
balance?.invalidate();
|
|
4850
|
+
await reconcile();
|
|
4851
|
+
return status();
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
//#endregion
|
|
4856
|
+
//#region src/images.js
|
|
4857
|
+
/** Host-only Images API transport and DSH tool. No subscription/LLM key fallback. */
|
|
4858
|
+
const IMAGE_TOOL_NAME = "packycode_image_generate";
|
|
4859
|
+
const IMAGE_TIMEOUT_MS = 5 * 6e4;
|
|
4860
|
+
const MAX_IMAGE_BYTES = 48 * 1024 * 1024;
|
|
4861
|
+
const MAX_REFERENCE_IMAGES = 16;
|
|
4862
|
+
const ENVELOPE_BYTES = 1024 * 1024;
|
|
4863
|
+
const EDIT_MEDIA_TYPES = [
|
|
4864
|
+
"image/png",
|
|
4865
|
+
"image/jpeg",
|
|
4866
|
+
"image/webp"
|
|
4867
|
+
];
|
|
4868
|
+
const SIGNATURE = "89504e470d0a1a0a";
|
|
4869
|
+
const ATTACHMENT_ID = /^sha256:[0-9a-f]{64}$/u;
|
|
4870
|
+
var ImageRequestError = class extends Error {};
|
|
4871
|
+
const nonempty = (value) => typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
4872
|
+
/** Some model calls fill unused object fields with a zero-valued reference. */
|
|
4873
|
+
function emptyMaskReference(value) {
|
|
4874
|
+
return value !== null && typeof value === "object" && (value.attachmentId === "" || value.attachmentId === `sha256:${"0".repeat(64)}`) && value.bytes === 0 && value.width === 0 && value.height === 0 && (value.name === void 0 || value.name === "") && (value.originalDimensions === void 0 || value.originalDimensions.width === 0 && value.originalDimensions.height === 0);
|
|
4875
|
+
}
|
|
4876
|
+
/** A bounded reader applies to both successful payloads and error envelopes. */
|
|
4877
|
+
async function readJson(response, maximum, signal) {
|
|
4878
|
+
const length = Number(response.headers.get("content-length"));
|
|
4879
|
+
if (Number.isFinite(length) && length > maximum || response.body === null) {
|
|
4880
|
+
await response.body?.cancel().catch(() => void 0);
|
|
4881
|
+
throw new ImageRequestError("PackyCode image response is unreadable or exceeds the size limit");
|
|
4882
|
+
}
|
|
4883
|
+
const reader = response.body.getReader();
|
|
4884
|
+
const cancel = () => {
|
|
4885
|
+
reader.cancel().catch(() => void 0);
|
|
4886
|
+
};
|
|
4887
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
4888
|
+
const chunks = [];
|
|
4889
|
+
let total = 0;
|
|
4890
|
+
try {
|
|
4891
|
+
signal.throwIfAborted();
|
|
4892
|
+
while (true) {
|
|
4893
|
+
const { done, value } = await reader.read();
|
|
4894
|
+
signal.throwIfAborted();
|
|
4895
|
+
if (done) break;
|
|
4896
|
+
total += value.byteLength;
|
|
4897
|
+
if (total > maximum) throw new ImageRequestError("PackyCode image response exceeds the size limit");
|
|
4898
|
+
chunks.push(Buffer.from(value));
|
|
4899
|
+
}
|
|
4900
|
+
try {
|
|
4901
|
+
return JSON.parse(Buffer.concat(chunks, total).toString("utf8"));
|
|
4902
|
+
} catch {
|
|
4903
|
+
throw new ImageRequestError("PackyCode returned an unreadable image response");
|
|
4904
|
+
}
|
|
4905
|
+
} finally {
|
|
4906
|
+
signal.removeEventListener("abort", cancel);
|
|
4907
|
+
await reader.cancel().catch(() => void 0);
|
|
4908
|
+
reader.releaseLock();
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
function pngDimensions(data) {
|
|
4912
|
+
const buffer = Buffer.from(data);
|
|
4913
|
+
if (buffer.length < 24 || buffer.subarray(0, 8).toString("hex") !== SIGNATURE || buffer.readUInt32BE(8) !== 13 || buffer.subarray(12, 16).toString("ascii") !== "IHDR") throw new ImageRequestError("PackyCode returned an invalid PNG image");
|
|
4914
|
+
const width = buffer.readUInt32BE(16);
|
|
4915
|
+
const height = buffer.readUInt32BE(20);
|
|
4916
|
+
if (width === 0 || height === 0 || Math.max(width, height) > 3840 || width * height > 8294400) throw new ImageRequestError("PackyCode returned unsupported image dimensions");
|
|
4917
|
+
return {
|
|
4918
|
+
width,
|
|
4919
|
+
height
|
|
4920
|
+
};
|
|
4921
|
+
}
|
|
4922
|
+
function decodeImagePng(encoded, maximum = MAX_IMAGE_BYTES) {
|
|
4923
|
+
if (typeof encoded !== "string" || encoded.length === 0 || encoded.length > Math.ceil(maximum / 3) * 4 || encoded.length % 4 !== 0) throw new ImageRequestError("PackyCode returned invalid or oversized base64 image data");
|
|
4924
|
+
const padding = encoded.endsWith("==") ? 2 : encoded.endsWith("=") ? 1 : 0;
|
|
4925
|
+
for (let index = 0; index < encoded.length - padding; index += 1) {
|
|
4926
|
+
const code = encoded.charCodeAt(index);
|
|
4927
|
+
if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) throw new ImageRequestError("PackyCode returned invalid base64 image data");
|
|
4928
|
+
}
|
|
4929
|
+
const data = Buffer.from(encoded, "base64");
|
|
4930
|
+
if (data.length > maximum || data.toString("base64") !== encoded) throw new ImageRequestError("PackyCode returned invalid base64 image data");
|
|
4931
|
+
pngDimensions(data);
|
|
4932
|
+
return data;
|
|
4933
|
+
}
|
|
4934
|
+
function imageFailure(status, value) {
|
|
4935
|
+
if (value?.error?.code === "moderation_blocked") return "PackyCode image request was blocked by the image safety check; revise the prompt or references";
|
|
4936
|
+
if (status === 401 || status === 403) return "PackyCode image key was rejected; check the separate image key and its model permissions";
|
|
4937
|
+
if (status === 429) return "PackyCode image quota or rate limit is unavailable; check the separate image key";
|
|
4938
|
+
return `PackyCode image request failed (HTTP ${status}); check model, quality, and image key permissions`;
|
|
4939
|
+
}
|
|
4940
|
+
/** One paid attempt only. Invalid references never turn an edit into a generation. */
|
|
4941
|
+
function createPackyCodeImageClient({ resolveKey, getHost, fetchImpl = ambientFetch, timeoutMs = IMAGE_TIMEOUT_MS }) {
|
|
4942
|
+
return { async request(args, { signal, images, mask } = {}) {
|
|
4943
|
+
const options = validateImageOptions(args);
|
|
4944
|
+
const prompt = nonempty(args?.prompt);
|
|
4945
|
+
if (prompt === void 0 || prompt.length > 32e3) throw new ImageRequestError("PackyCode image prompt must contain 1-32000 characters");
|
|
4946
|
+
if (images !== void 0 && (!Array.isArray(images) || images.length === 0 || images.length > MAX_REFERENCE_IMAGES)) throw new ImageRequestError("PackyCode image edits require 1-16 reference images");
|
|
4947
|
+
if (mask !== void 0 && images === void 0) throw new ImageRequestError("PackyCode image mask requires a reference image");
|
|
4948
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4949
|
+
const requestSignal = signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
|
|
4950
|
+
let selected;
|
|
4951
|
+
try {
|
|
4952
|
+
selected = await waitForSharedRequest(Promise.resolve().then(resolveKey), requestSignal);
|
|
4953
|
+
} catch {
|
|
4954
|
+
if (signal?.aborted) throw new ImageRequestError("PackyCode image request was cancelled");
|
|
4955
|
+
if (timeout.aborted) throw new ImageRequestError("PackyCode image request timed out");
|
|
4956
|
+
throw new ImageRequestError("PackyCode image credentials could not be read");
|
|
4957
|
+
}
|
|
4958
|
+
if (typeof selected?.key !== "string" || selected.key === "") throw new ImageRequestError("PackyCode images need a separate image key; add one under Settings → PackyCode → Images");
|
|
4959
|
+
const fields = {
|
|
4960
|
+
...options,
|
|
4961
|
+
prompt,
|
|
4962
|
+
n: 1,
|
|
4963
|
+
output_format: "png"
|
|
4964
|
+
};
|
|
4965
|
+
let body;
|
|
4966
|
+
const headers = {
|
|
4967
|
+
authorization: `Bearer ${selected.key}`,
|
|
4968
|
+
accept: "application/json"
|
|
4969
|
+
};
|
|
4970
|
+
if (images === void 0) {
|
|
4971
|
+
headers["content-type"] = "application/json";
|
|
4972
|
+
body = JSON.stringify(fields);
|
|
4973
|
+
} else {
|
|
4974
|
+
body = new FormData();
|
|
4975
|
+
for (const [key, value] of Object.entries(fields)) body.append(key, String(value));
|
|
4976
|
+
for (const [index, image] of images.entries()) {
|
|
4977
|
+
if (!EDIT_MEDIA_TYPES.includes(image?.mediaType) || !(image.data instanceof Uint8Array) || image.data.byteLength === 0 || image.data.byteLength >= 50 * 1024 * 1024) throw new ImageRequestError("PackyCode reference images must be PNG, JPEG or WebP smaller than 50 MB");
|
|
4978
|
+
const extension = image.mediaType === "image/jpeg" ? "jpg" : image.mediaType.split("/")[1];
|
|
4979
|
+
body.append("image[]", new Blob([image.data], { type: image.mediaType }), `reference-${index + 1}.${extension}`);
|
|
4980
|
+
}
|
|
4981
|
+
if (mask !== void 0) {
|
|
4982
|
+
if (mask.mediaType !== "image/png" || !(mask.data instanceof Uint8Array) || mask.data.byteLength >= 4 * 1024 * 1024) throw new ImageRequestError("PackyCode image mask must be a PNG smaller than 4 MB");
|
|
4983
|
+
const dimensions = pngDimensions(mask.data);
|
|
4984
|
+
if (dimensions.width !== images[0].width || dimensions.height !== images[0].height) throw new ImageRequestError("PackyCode image mask must match the first reference image dimensions");
|
|
4985
|
+
body.append("mask", new Blob([mask.data], { type: "image/png" }), "mask.png");
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
try {
|
|
4989
|
+
requestSignal.throwIfAborted();
|
|
4990
|
+
const response = await fetchImpl(`${normalizeHost(getHost(), DEFAULT_API_HOST).replace(/\/v1$/u, "")}/v1/images/${images === void 0 ? "generations" : "edits"}`, {
|
|
4991
|
+
method: "POST",
|
|
4992
|
+
redirect: "error",
|
|
4993
|
+
headers,
|
|
4994
|
+
body,
|
|
4995
|
+
signal: requestSignal
|
|
4996
|
+
});
|
|
4997
|
+
if (!response.ok) {
|
|
4998
|
+
const value = await readJson(response, 64 * 1024, requestSignal).catch(() => void 0);
|
|
4999
|
+
requestSignal.throwIfAborted();
|
|
5000
|
+
throw new ImageRequestError(imageFailure(response.status, value));
|
|
5001
|
+
}
|
|
5002
|
+
const value = await readJson(response, Math.ceil(MAX_IMAGE_BYTES / 3) * 4 + ENVELOPE_BYTES, requestSignal);
|
|
5003
|
+
const data = decodeImagePng(value?.data?.[0]?.b64_json);
|
|
5004
|
+
return {
|
|
5005
|
+
data,
|
|
5006
|
+
...pngDimensions(data),
|
|
5007
|
+
requestedModel: options.model,
|
|
5008
|
+
requestedQuality: options.quality,
|
|
5009
|
+
requestedSize: options.size,
|
|
5010
|
+
...IMAGE_MODEL_IDS.includes(value?.model) ? { reportedModel: value.model } : {},
|
|
5011
|
+
...IMAGE_QUALITIES.includes(value?.quality) ? { quality: value.quality } : {}
|
|
5012
|
+
};
|
|
5013
|
+
} catch (error) {
|
|
5014
|
+
if (signal?.aborted) throw new ImageRequestError("PackyCode image request was cancelled");
|
|
5015
|
+
if (timeout.aborted) throw new ImageRequestError("PackyCode image request timed out");
|
|
5016
|
+
if (error instanceof ImageRequestError) throw error;
|
|
5017
|
+
throw new ImageRequestError("PackyCode image connection failed");
|
|
5018
|
+
}
|
|
5019
|
+
} };
|
|
5020
|
+
}
|
|
5021
|
+
const referenceSchema = {
|
|
5022
|
+
type: "object",
|
|
5023
|
+
additionalProperties: false,
|
|
5024
|
+
properties: {
|
|
5025
|
+
attachmentId: {
|
|
5026
|
+
type: "string",
|
|
5027
|
+
required: true,
|
|
5028
|
+
description: "Complete sha256:<64 lowercase hex> image reference from the current session or read_image. Never a path or filename."
|
|
5029
|
+
},
|
|
5030
|
+
mediaType: {
|
|
5031
|
+
type: "string",
|
|
5032
|
+
required: true
|
|
5033
|
+
},
|
|
5034
|
+
bytes: {
|
|
5035
|
+
type: "integer",
|
|
5036
|
+
required: true
|
|
5037
|
+
},
|
|
5038
|
+
width: {
|
|
5039
|
+
type: "integer",
|
|
5040
|
+
required: true
|
|
5041
|
+
},
|
|
5042
|
+
height: {
|
|
5043
|
+
type: "integer",
|
|
5044
|
+
required: true
|
|
5045
|
+
},
|
|
5046
|
+
name: { type: "string" },
|
|
5047
|
+
originalDimensions: {
|
|
5048
|
+
type: "object",
|
|
5049
|
+
additionalProperties: false,
|
|
5050
|
+
properties: {
|
|
5051
|
+
width: {
|
|
5052
|
+
type: "integer",
|
|
5053
|
+
required: true
|
|
5054
|
+
},
|
|
5055
|
+
height: {
|
|
5056
|
+
type: "integer",
|
|
5057
|
+
required: true
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
};
|
|
5063
|
+
/** Select only trusted references already present in this tool call's session. */
|
|
5064
|
+
async function readReferences(values, attachments, messages, signal) {
|
|
5065
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > MAX_REFERENCE_IMAGES) throw new ImageRequestError("PackyCode image edits require 1-16 reference images");
|
|
5066
|
+
const available = /* @__PURE__ */ new Map();
|
|
5067
|
+
const visit = (content) => {
|
|
5068
|
+
for (const block of content ?? []) {
|
|
5069
|
+
if (block?.type === "image" && ATTACHMENT_ID.test(block.attachment?.attachmentId)) available.set(block.attachment.attachmentId, block.attachment);
|
|
5070
|
+
if (block?.type === "tool-result" && Array.isArray(block.content)) visit(block.content);
|
|
5071
|
+
}
|
|
5072
|
+
};
|
|
5073
|
+
for (const message of messages ?? []) if (Array.isArray(message?.content)) visit(message.content);
|
|
5074
|
+
const ids = values.map((value) => value?.attachmentId);
|
|
5075
|
+
if (ids.some((id) => typeof id !== "string" || !ATTACHMENT_ID.test(id) || !available.has(id))) throw new ImageRequestError("PackyCode image reference is missing from this session; use read_image and copy its complete attachment reference");
|
|
5076
|
+
if (new Set(ids).size !== ids.length) throw new ImageRequestError("PackyCode image references must not contain duplicates");
|
|
5077
|
+
const images = [];
|
|
5078
|
+
let total = 0;
|
|
5079
|
+
for (const id of ids) {
|
|
5080
|
+
signal?.throwIfAborted();
|
|
5081
|
+
let stored;
|
|
5082
|
+
try {
|
|
5083
|
+
stored = await attachments.readImage(available.get(id), signal);
|
|
5084
|
+
} catch {
|
|
5085
|
+
throw new ImageRequestError("PackyCode could not read the selected image attachment");
|
|
5086
|
+
}
|
|
5087
|
+
const { data, ref } = stored;
|
|
5088
|
+
if (!(data instanceof Uint8Array) || !EDIT_MEDIA_TYPES.includes(ref?.mediaType)) throw new ImageRequestError("PackyCode image reference must be PNG, JPEG or WebP");
|
|
5089
|
+
total += data.byteLength;
|
|
5090
|
+
if (total > attachments.imageLimits.maxMessageImageBytes) throw new ImageRequestError("PackyCode image references exceed the DSH image limit");
|
|
5091
|
+
images.push({
|
|
5092
|
+
data,
|
|
5093
|
+
mediaType: ref.mediaType,
|
|
5094
|
+
width: ref.width,
|
|
5095
|
+
height: ref.height
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
return images;
|
|
5099
|
+
}
|
|
5100
|
+
function attachmentProjection(ref) {
|
|
5101
|
+
return Object.fromEntries([
|
|
5102
|
+
"attachmentId",
|
|
5103
|
+
"mediaType",
|
|
5104
|
+
"bytes",
|
|
5105
|
+
"width",
|
|
5106
|
+
"height",
|
|
5107
|
+
"name",
|
|
5108
|
+
"originalDimensions"
|
|
5109
|
+
].filter((key) => ref[key] !== void 0).map((key) => [key, ref[key]]));
|
|
5110
|
+
}
|
|
5111
|
+
const renderImage = (value) => [{
|
|
5112
|
+
type: "text",
|
|
5113
|
+
text: `PackyCode · ${value.requestedModel} · ${value.width} × ${value.height} · ${value.requestedQuality}\nOriginal PNG on the DSH host: ${value.localPath}`
|
|
5114
|
+
}, {
|
|
5115
|
+
type: "image",
|
|
5116
|
+
attachment: value.image
|
|
5117
|
+
}];
|
|
5118
|
+
function createPackyCodeImageTool({ client, attachments, getPreference, getSessionMessages, imageDirectory, onGenerated }) {
|
|
5119
|
+
return defineTool({
|
|
5120
|
+
name: IMAGE_TOOL_NAME,
|
|
5121
|
+
description: "Generate or edit images through PackyCode using the separately configured image API key. Use only when the user requests image output. This is independent of the conversation model and Codex subscription. For a new image omit referenceImages; for edits include only the intended images from this session or read_image. Never replace an edit with text-only generation, silently change models or downgrade quality. Return the resulting image to the user. Rendering may take several minutes.",
|
|
5122
|
+
parameters: {
|
|
5123
|
+
prompt: {
|
|
5124
|
+
type: "string",
|
|
5125
|
+
required: true,
|
|
5126
|
+
description: "Complete image description or precise editing instructions."
|
|
5127
|
+
},
|
|
5128
|
+
operation: {
|
|
5129
|
+
type: "string",
|
|
5130
|
+
enum: ["generate", "edit"],
|
|
5131
|
+
description: "Use generate for a new image; edit requires 1-16 real referenceImages. If omitted, nonempty references select editing, while an omitted or empty array selects generation."
|
|
5132
|
+
},
|
|
5133
|
+
model: {
|
|
5134
|
+
type: "string",
|
|
5135
|
+
enum: ["settings", ...IMAGE_MODEL_IDS],
|
|
5136
|
+
description: "Use settings (or omit) to follow the PackyCode image setting. Choose an explicit engine only when the user requests one."
|
|
5137
|
+
},
|
|
5138
|
+
quality: {
|
|
5139
|
+
type: "string",
|
|
5140
|
+
enum: ["settings", ...IMAGE_QUALITIES],
|
|
5141
|
+
description: "Use settings (or omit) to follow the user image setting. auto is an explicit API quality, not a request to follow settings. xhigh and max require a 2.5 model; never downgrade automatically."
|
|
5142
|
+
},
|
|
5143
|
+
size: {
|
|
5144
|
+
type: "string",
|
|
5145
|
+
description: "auto or widthxheight, e.g. 1024x1024; both edges multiples of 16, each <=3840, ratio <=3:1, pixels 655360..8294400."
|
|
5146
|
+
},
|
|
5147
|
+
background: {
|
|
5148
|
+
type: "string",
|
|
5149
|
+
enum: [
|
|
5150
|
+
"auto",
|
|
5151
|
+
"opaque",
|
|
5152
|
+
"transparent"
|
|
5153
|
+
]
|
|
5154
|
+
},
|
|
5155
|
+
referenceImages: {
|
|
5156
|
+
type: "array",
|
|
5157
|
+
items: referenceSchema,
|
|
5158
|
+
description: "1-16 complete attachment references for editing. Omit or use [] for a new image. Paths and unrelated images are not accepted; never drop real references after an edit fails."
|
|
5159
|
+
},
|
|
5160
|
+
maskImage: {
|
|
5161
|
+
...referenceSchema,
|
|
5162
|
+
description: "Omit for new images and edits without a mask. Never invent a reference. Optional PNG mask from this session, matching the first reference and under 4 MB; alpha=0 marks the area to edit."
|
|
5163
|
+
}
|
|
5164
|
+
},
|
|
5165
|
+
output: {
|
|
5166
|
+
schema: {
|
|
5167
|
+
type: "object",
|
|
5168
|
+
additionalProperties: false,
|
|
5169
|
+
properties: {
|
|
5170
|
+
image: {
|
|
5171
|
+
...referenceSchema,
|
|
5172
|
+
required: true
|
|
5173
|
+
},
|
|
5174
|
+
localPath: {
|
|
5175
|
+
type: "string",
|
|
5176
|
+
required: true
|
|
5177
|
+
},
|
|
5178
|
+
width: {
|
|
5179
|
+
type: "integer",
|
|
5180
|
+
required: true
|
|
5181
|
+
},
|
|
5182
|
+
height: {
|
|
5183
|
+
type: "integer",
|
|
5184
|
+
required: true
|
|
5185
|
+
},
|
|
5186
|
+
requestedModel: {
|
|
5187
|
+
type: "string",
|
|
5188
|
+
required: true
|
|
5189
|
+
},
|
|
5190
|
+
requestedQuality: {
|
|
5191
|
+
type: "string",
|
|
5192
|
+
required: true
|
|
5193
|
+
},
|
|
5194
|
+
requestedSize: {
|
|
5195
|
+
type: "string",
|
|
5196
|
+
required: true
|
|
5197
|
+
},
|
|
5198
|
+
reportedModel: { type: "string" },
|
|
5199
|
+
quality: { type: "string" }
|
|
5200
|
+
}
|
|
5201
|
+
},
|
|
5202
|
+
render: (_args, value) => renderImage(value),
|
|
5203
|
+
presentationMeta: (_args, value) => ({
|
|
5204
|
+
kind: "packycode-image",
|
|
5205
|
+
requestedModel: value.requestedModel,
|
|
5206
|
+
requestedQuality: value.requestedQuality,
|
|
5207
|
+
width: value.width,
|
|
5208
|
+
height: value.height
|
|
5209
|
+
})
|
|
5210
|
+
},
|
|
5211
|
+
timeoutMs: IMAGE_TIMEOUT_MS,
|
|
5212
|
+
isConcurrencySafe: () => false,
|
|
5213
|
+
async execute(args, exec) {
|
|
5214
|
+
const preference = getPreference();
|
|
5215
|
+
if (!preference.imageEnabled) throw new ImageRequestError("PackyCode images are disabled in settings");
|
|
5216
|
+
if (exec?.agent?.id === void 0) throw new ImageRequestError("PackyCode images require a session-owned tool call");
|
|
5217
|
+
const signal = exec.signal;
|
|
5218
|
+
const hasReferences = Array.isArray(args.referenceImages) && args.referenceImages.length > 0;
|
|
5219
|
+
if (args.operation === "generate" && hasReferences) throw new ImageRequestError("PackyCode image generation cannot include reference images; use edit");
|
|
5220
|
+
const editing = args.operation === "edit" || hasReferences;
|
|
5221
|
+
const maskImage = emptyMaskReference(args.maskImage) ? void 0 : args.maskImage;
|
|
5222
|
+
if (maskImage !== void 0 && !editing) throw new ImageRequestError("PackyCode image mask requires referenceImages");
|
|
5223
|
+
let messages;
|
|
5224
|
+
try {
|
|
5225
|
+
messages = editing ? await getSessionMessages(exec.agent.id) : void 0;
|
|
5226
|
+
} catch {
|
|
5227
|
+
throw new ImageRequestError("PackyCode could not read the current session image references");
|
|
5228
|
+
}
|
|
5229
|
+
const images = editing ? await readReferences(args.referenceImages, attachments, messages, signal) : void 0;
|
|
5230
|
+
const masks = maskImage === void 0 ? void 0 : await readReferences([maskImage], attachments, messages, signal);
|
|
5231
|
+
if (!getPreference().imageEnabled) throw new ImageRequestError("PackyCode images are disabled in settings");
|
|
5232
|
+
const result = await client.request({
|
|
5233
|
+
...args,
|
|
5234
|
+
model: args.model === "settings" ? preference.imageModel : args.model ?? preference.imageModel,
|
|
5235
|
+
quality: args.quality === "settings" ? preference.imageQuality : args.quality ?? preference.imageQuality
|
|
5236
|
+
}, {
|
|
5237
|
+
signal,
|
|
5238
|
+
images,
|
|
5239
|
+
mask: masks?.[0]
|
|
5240
|
+
});
|
|
5241
|
+
onGenerated?.();
|
|
5242
|
+
signal?.throwIfAborted();
|
|
5243
|
+
await mkdir(imageDirectory, {
|
|
5244
|
+
recursive: true,
|
|
5245
|
+
mode: 448
|
|
5246
|
+
});
|
|
5247
|
+
const localPath = join(imageDirectory, `${randomUUID()}.png`);
|
|
5248
|
+
let saved = false;
|
|
5249
|
+
try {
|
|
5250
|
+
const file = await open(localPath, "wx", 384);
|
|
5251
|
+
saved = true;
|
|
5252
|
+
try {
|
|
5253
|
+
await file.writeFile(result.data);
|
|
5254
|
+
} finally {
|
|
5255
|
+
await file.close();
|
|
5256
|
+
}
|
|
5257
|
+
const ref = await attachments.saveImage({
|
|
5258
|
+
data: result.data,
|
|
5259
|
+
mediaType: "image/png",
|
|
5260
|
+
name: "packycode-generated.png"
|
|
5261
|
+
});
|
|
5262
|
+
const { data: _data, ...metadata } = result;
|
|
5263
|
+
const value = {
|
|
5264
|
+
...metadata,
|
|
5265
|
+
image: attachmentProjection(ref),
|
|
5266
|
+
localPath
|
|
5267
|
+
};
|
|
5268
|
+
if (exec.parent !== void 0) exec.deferContext(createUserMessage({
|
|
5269
|
+
content: renderImage(value),
|
|
5270
|
+
source: {
|
|
5271
|
+
kind: "plugin",
|
|
5272
|
+
plugin: "packycode-api"
|
|
5273
|
+
}
|
|
5274
|
+
}));
|
|
5275
|
+
return value;
|
|
5276
|
+
} catch {
|
|
5277
|
+
if (saved) await rm(localPath, { force: true }).catch(() => void 0);
|
|
5278
|
+
throw new ImageRequestError("PackyCode could not save the generated image");
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
//#endregion
|
|
5284
|
+
//#region src/key-chooser.js
|
|
5285
|
+
/**
|
|
5286
|
+
* Which stored key a request runs with, and what happens when the gateway
|
|
5287
|
+
* rejects one.
|
|
5288
|
+
*
|
|
5289
|
+
* The ring's active key serves every request it can. A request for a model
|
|
5290
|
+
* the active key's groups cannot reach — as the last group reading shows —
|
|
5291
|
+
* runs on the next stored key that can; the active pointer itself does not
|
|
5292
|
+
* move. A key the gateway reports as exhausted or invalid is marked unusable
|
|
5293
|
+
* and the ring moves on, so the retry of the failed step resolves the next
|
|
5294
|
+
* key. Only keys stored in this plugin's ring are ever chosen.
|
|
5295
|
+
*
|
|
5296
|
+
* @module dsh-packycode-api/key-chooser
|
|
5297
|
+
*/
|
|
5298
|
+
/** How long a request with a choice of keys waits for the first group reading. */
|
|
5299
|
+
const DEFAULT_GROUP_WAIT_MS = 6e3;
|
|
5300
|
+
/**
|
|
5301
|
+
* @param options - the key ring, the group reader, what to do after a key
|
|
5302
|
+
* moved aside, a logger, and how long to wait for the first group reading.
|
|
5303
|
+
* @returns the chooser.
|
|
5304
|
+
*/
|
|
5305
|
+
function createKeyChooser({ ring, groups, onRotated, logger, waitMs = DEFAULT_GROUP_WAIT_MS }) {
|
|
5306
|
+
/**
|
|
5307
|
+
* Key material to ring id. A failure is reported from inside a stream, which
|
|
5308
|
+
* carries the request's key but not its id; this index is refreshed on every
|
|
5309
|
+
* resolution so the right key is the one that moves aside.
|
|
5310
|
+
*/
|
|
5311
|
+
const idByKey = /* @__PURE__ */ new Map();
|
|
5312
|
+
/** The ring as of the last resolution, for the synchronous per-request choice. */
|
|
5313
|
+
let snapshot = [];
|
|
5314
|
+
const remember = async () => {
|
|
5315
|
+
const entries = await ring.secrets();
|
|
5316
|
+
snapshot = entries;
|
|
5317
|
+
for (const entry of entries) idByKey.set(entry.key, entry.id);
|
|
5318
|
+
};
|
|
5319
|
+
const usable = () => snapshot.filter((entry) => entry.disabled !== true);
|
|
5320
|
+
const covers = (keyId, modelId) => groups.covers(keyId, modelId);
|
|
5321
|
+
/**
|
|
5322
|
+
* Keep the group reading fresh for the choice. It only matters with two
|
|
5323
|
+
* usable keys or more; the first reading is awaited for at most `waitMs`,
|
|
5324
|
+
* later ones refresh in the background.
|
|
5325
|
+
*/
|
|
5326
|
+
const warm = async () => {
|
|
5327
|
+
if (usable().length < 2) return;
|
|
5328
|
+
const pending = groups.read().catch(() => void 0);
|
|
5329
|
+
if (groups.hasReading()) return;
|
|
5330
|
+
let timer;
|
|
5331
|
+
await Promise.race([pending, new Promise((resolve) => {
|
|
5332
|
+
timer = setTimeout(resolve, waitMs);
|
|
5333
|
+
})]);
|
|
5334
|
+
clearTimeout(timer);
|
|
5335
|
+
};
|
|
5336
|
+
/** The active key, after refreshing the ring and, when it matters, the groups. */
|
|
5337
|
+
const active = async () => {
|
|
5338
|
+
await remember();
|
|
5339
|
+
const key = await ring.activeKey();
|
|
5340
|
+
if (key !== void 0) await warm();
|
|
5341
|
+
return key;
|
|
5342
|
+
};
|
|
5343
|
+
return Object.freeze({
|
|
5344
|
+
remember,
|
|
5345
|
+
covers,
|
|
4607
5346
|
active,
|
|
4608
5347
|
/**
|
|
4609
5348
|
* The key one HTTP request for `modelId` runs with, given the active key
|
|
@@ -4693,7 +5432,6 @@ function createKeyChooser({ ring, groups, onRotated, logger, waitMs = DEFAULT_GR
|
|
|
4693
5432
|
const LEGAL_KEY = /^[\x21-\x7E]+$/u;
|
|
4694
5433
|
const RECORD_VERSION = 1;
|
|
4695
5434
|
const MAX_KEYS = 32;
|
|
4696
|
-
const RECORD_KEY = typeof credentialKey === "function" ? credentialKey(PLUGIN_ID, KEY_RECORD_ID) : void 0;
|
|
4697
5435
|
const text$1 = (value) => typeof value === "string" ? value : "";
|
|
4698
5436
|
/** Validate a pasted key before it is stored; the gateway never sees a bad one from us. */
|
|
4699
5437
|
function assertApiKey(value) {
|
|
@@ -4757,16 +5495,18 @@ function projection(state) {
|
|
|
4757
5495
|
}
|
|
4758
5496
|
var PackyCodeKeyRing = class {
|
|
4759
5497
|
#credentials;
|
|
4760
|
-
|
|
4761
|
-
|
|
5498
|
+
#recordKey;
|
|
5499
|
+
constructor(credentials, { recordId = KEY_RECORD_ID } = {}) {
|
|
5500
|
+
if (typeof credentialKey !== "function") throw new Error("dsh-packycode-api requires @deepseek-ai/dsh-credentials with credentialKey()");
|
|
4762
5501
|
if (credentials === null || typeof credentials !== "object" || typeof credentials.readRecord !== "function" || typeof credentials.modifyRecord !== "function") throw new Error("dsh-packycode-api requires the DSH credential record API");
|
|
4763
5502
|
this.#credentials = credentials;
|
|
5503
|
+
this.#recordKey = credentialKey(PLUGIN_ID, recordId);
|
|
4764
5504
|
}
|
|
4765
5505
|
async #read() {
|
|
4766
|
-
return normalizeState(await this.#credentials.readRecord(
|
|
5506
|
+
return normalizeState(await this.#credentials.readRecord(this.#recordKey));
|
|
4767
5507
|
}
|
|
4768
5508
|
async #write(mutate) {
|
|
4769
|
-
return normalizeState(await this.#credentials.modifyRecord(
|
|
5509
|
+
return normalizeState(await this.#credentials.modifyRecord(this.#recordKey, async (current) => {
|
|
4770
5510
|
const updated = await mutate(normalizeState(current));
|
|
4771
5511
|
if (updated === void 0) return current;
|
|
4772
5512
|
return {
|
|
@@ -4916,95 +5656,6 @@ var PackyCodeKeyRing = class {
|
|
|
4916
5656
|
}
|
|
4917
5657
|
};
|
|
4918
5658
|
//#endregion
|
|
4919
|
-
//#region src/preferences.js
|
|
4920
|
-
/**
|
|
4921
|
-
* Where the plugin's preferences are read from and written to, on both
|
|
4922
|
-
* settings generations DSH has shipped.
|
|
4923
|
-
*
|
|
4924
|
-
* - DSH 0.1.5 installs a settings *section*: the section hands back a live
|
|
4925
|
-
* source and writes go through `settings.update(namespace, patch)`.
|
|
4926
|
-
* - DSH 0.2 has no sections: a preference is a *volatile* Config field of the
|
|
4927
|
-
* plugin's own row, read through a live reference, written through
|
|
4928
|
-
* `settings.update(rowId, patch)`, and announced by `loader/volatile-update`.
|
|
4929
|
-
* Only volatile fields are writable there, so the host overrides stay
|
|
4930
|
-
* read-only while web search and model visibility are editable.
|
|
4931
|
-
*
|
|
4932
|
-
* @module dsh-packycode-api/preferences
|
|
4933
|
-
*/
|
|
4934
|
-
/**
|
|
4935
|
-
* Mark a preference editable without restarting the plugin. DSH 0.2 stores
|
|
4936
|
-
* plugin preferences as volatile Config fields and writes only those, so the
|
|
4937
|
-
* schema version that implements `volatile()` is a peer requirement.
|
|
4938
|
-
*
|
|
4939
|
-
* Name the cause when it is missing instead of letting a bare
|
|
4940
|
-
* `schema.volatile is not a function` reach the host log: the Loader creates
|
|
4941
|
-
* no fiber for a plugin whose module throws while it is imported, so DSH shows
|
|
4942
|
-
* nothing at all — no settings section, no model route, no RPC channel — and
|
|
4943
|
-
* the only explanation is that one log line.
|
|
4944
|
-
*/
|
|
4945
|
-
const live = (schema) => {
|
|
4946
|
-
if (typeof schema?.volatile !== "function") throw new Error("PackyCode needs @deepseek-ai/schemastery ^3.18.4, which implements Schema.volatile(); the copy resolved for this plugin is older. A plugin installed from a registry has no node_modules of its own, so this usually means a stale dependency tree inside the installed package directory (for example <profile>/node_modules/dsh-packycode-api/node_modules, left by an earlier local-path install) is shadowing the DSH installation. Remove that directory, or uninstall the plugin and install it again, then restart DSH.");
|
|
4947
|
-
return schema.volatile();
|
|
4948
|
-
};
|
|
4949
|
-
/** Read a Config field that may arrive as a volatile reference. */
|
|
4950
|
-
const fieldValue = (value) => value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
4951
|
-
/** A preference target nothing can write to yet. */
|
|
4952
|
-
function unwritable(message) {
|
|
4953
|
-
return {
|
|
4954
|
-
writable: false,
|
|
4955
|
-
set: async () => {
|
|
4956
|
-
throw new Error(message);
|
|
4957
|
-
}
|
|
4958
|
-
};
|
|
4959
|
-
}
|
|
4960
|
-
/**
|
|
4961
|
-
* Connect the preferences to the settings service the host provides.
|
|
4962
|
-
* @param settingsCtx - the scope the `settings` inject handed over.
|
|
4963
|
-
* @param options - the plugin context and Config, the section namespace, the
|
|
4964
|
-
* current source and how to replace it, what to do when the section changes,
|
|
4965
|
-
* and the preference targets this fills in (host overrides, search, models).
|
|
4966
|
-
*/
|
|
4967
|
-
function connectSettings(settingsCtx, { ctx, Config, namespace, source, setSource, onChange, hostPreference, searchPreference, modelPreference }) {
|
|
4968
|
-
const settings = settingsCtx.settings;
|
|
4969
|
-
if (typeof settings?.installSection === "function") {
|
|
4970
|
-
try {
|
|
4971
|
-
settings.installSection(ctx, namespace, Config, source(), {
|
|
4972
|
-
setSource: (current) => setSource(typeof current === "function" ? () => current() ?? {} : () => current ?? {}),
|
|
4973
|
-
onChange
|
|
4974
|
-
});
|
|
4975
|
-
if (typeof settings.update === "function") {
|
|
4976
|
-
const write = (patch) => settings.update(namespace, patch);
|
|
4977
|
-
const target = {
|
|
4978
|
-
writable: settings.writable !== false,
|
|
4979
|
-
set: write
|
|
4980
|
-
};
|
|
4981
|
-
Object.assign(hostPreference, target);
|
|
4982
|
-
Object.assign(searchPreference, target);
|
|
4983
|
-
Object.assign(modelPreference, target);
|
|
4984
|
-
}
|
|
4985
|
-
} catch (error) {
|
|
4986
|
-
ctx.logger?.warn?.("packycode-api: host overrides unavailable, defaults stand: %s", error.message);
|
|
4987
|
-
}
|
|
4988
|
-
return;
|
|
4989
|
-
}
|
|
4990
|
-
if (typeof settings?.update !== "function") return;
|
|
4991
|
-
const rowId = ctx.fiber?.entry?.options?.id ?? namespace;
|
|
4992
|
-
const target = {
|
|
4993
|
-
writable: settings.writable !== false,
|
|
4994
|
-
set: (patch) => settings.update(rowId, patch)
|
|
4995
|
-
};
|
|
4996
|
-
Object.assign(searchPreference, target);
|
|
4997
|
-
Object.assign(modelPreference, target);
|
|
4998
|
-
if (typeof settings.configure === "function") {
|
|
4999
|
-
const scope = typeof settingsCtx.effect === "function" ? settingsCtx : ctx;
|
|
5000
|
-
try {
|
|
5001
|
-
scope.effect(() => settings.configure({ auto: false }, ctx.fiber), "packycode-api: settings page policy");
|
|
5002
|
-
} catch (error) {
|
|
5003
|
-
ctx.logger?.warn?.("packycode-api: could not hide the generated settings page: %s", error.message);
|
|
5004
|
-
}
|
|
5005
|
-
}
|
|
5006
|
-
}
|
|
5007
|
-
//#endregion
|
|
5008
5659
|
//#region src/model-selection.js
|
|
5009
5660
|
/** Profile preferences for the models advertised in PackyCode's picker. */
|
|
5010
5661
|
/** Store exclusions so new catalog models are selected by default as well. */
|
|
@@ -5375,13 +6026,119 @@ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = s
|
|
|
5375
6026
|
});
|
|
5376
6027
|
}
|
|
5377
6028
|
//#endregion
|
|
6029
|
+
//#region src/gemini-replay.js
|
|
6030
|
+
/** A bounded resend for the gateway's intermittent Flash signature refusal. */
|
|
6031
|
+
const waitForRetry = (ms, signal) => setTimeout$1(ms, void 0, { signal });
|
|
6032
|
+
/** Only a current-turn result for a signed same-model native call qualifies. */
|
|
6033
|
+
function hasSignedResult(model, context) {
|
|
6034
|
+
if (model?.id !== "gemini-3.8-flash" || model.api !== "google-generative-ai") return false;
|
|
6035
|
+
const messages = context?.messages ?? [];
|
|
6036
|
+
const calls = /* @__PURE__ */ new Set();
|
|
6037
|
+
for (const message of messages.slice(messages.findLastIndex((item) => item.role === "user") + 1)) {
|
|
6038
|
+
if (message.role === "assistant" && message.provider === model.provider && message.model === model.id && message.api === model.api) for (const block of Array.isArray(message.content) ? message.content : []) {
|
|
6039
|
+
const signature = block.thoughtSignature;
|
|
6040
|
+
if (block.type === "toolCall" && typeof block.id === "string" && typeof signature === "string" && signature.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/u.test(signature)) calls.add(block.id);
|
|
6041
|
+
}
|
|
6042
|
+
if (message.role === "toolResult" && calls.has(message.toolCallId)) return true;
|
|
6043
|
+
}
|
|
6044
|
+
return false;
|
|
6045
|
+
}
|
|
6046
|
+
function isSignatureRefusal(event) {
|
|
6047
|
+
if (event.type !== "error" || event.reason !== "error" || event.error.content?.length) return false;
|
|
6048
|
+
const text = event.error.errorMessage ?? "";
|
|
6049
|
+
return /\b400\b/u.test(text) && /\b(?:Corrupted|Invalid) thought signature\b/iu.test(text) && !/\b(?:401|403|408|409|429|5\d\d)\b/u.test(text) && classifyGatewayFailure(text) === void 0;
|
|
6050
|
+
}
|
|
6051
|
+
/**
|
|
6052
|
+
* The caller closes over one resolved model/context/key/session. Reopening it
|
|
6053
|
+
* resends that request once; it never chooses a different key or changes the
|
|
6054
|
+
* signature. SDK retries remain disabled by the host. Its ordinary retry
|
|
6055
|
+
* policy still receives any terminal failure after this one compatibility
|
|
6056
|
+
* resend. Never replay after even a start event has reached the consumer.
|
|
6057
|
+
*/
|
|
6058
|
+
function guardGeminiReplay(open, { model, context, signal, wait = waitForRetry } = {}) {
|
|
6059
|
+
const initial = open();
|
|
6060
|
+
if (!hasSignedResult(model, context) || signal?.aborted) return initial;
|
|
6061
|
+
let finalMessage;
|
|
6062
|
+
return lazyStream(model, async () => ({
|
|
6063
|
+
async *[Symbol.asyncIterator]() {
|
|
6064
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
6065
|
+
const source = attempt === 0 ? initial : open();
|
|
6066
|
+
let observed = false;
|
|
6067
|
+
let refused;
|
|
6068
|
+
for await (const event of source) if (attempt === 0 && !observed && isSignatureRefusal(event)) refused = event;
|
|
6069
|
+
else {
|
|
6070
|
+
observed = true;
|
|
6071
|
+
yield event;
|
|
6072
|
+
}
|
|
6073
|
+
finalMessage = await source.result();
|
|
6074
|
+
if (refused === void 0) return;
|
|
6075
|
+
try {
|
|
6076
|
+
await wait(1e3, signal);
|
|
6077
|
+
signal?.throwIfAborted();
|
|
6078
|
+
} catch (error) {
|
|
6079
|
+
if (!signal?.aborted) throw error;
|
|
6080
|
+
finalMessage = {
|
|
6081
|
+
...refused.error,
|
|
6082
|
+
stopReason: "aborted",
|
|
6083
|
+
errorMessage: "Request aborted"
|
|
6084
|
+
};
|
|
6085
|
+
yield {
|
|
6086
|
+
type: "error",
|
|
6087
|
+
reason: "aborted",
|
|
6088
|
+
error: finalMessage
|
|
6089
|
+
};
|
|
6090
|
+
return;
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
},
|
|
6094
|
+
result: async () => finalMessage
|
|
6095
|
+
}));
|
|
6096
|
+
}
|
|
6097
|
+
//#endregion
|
|
6098
|
+
//#region src/gemini-api.js
|
|
6099
|
+
/** Gemini's native API, including level-based thinking on older pi-ai hosts. */
|
|
6100
|
+
/**
|
|
6101
|
+
* pi-ai 0.85 treats Argon as a budget-based model, and defaults Flash to
|
|
6102
|
+
* MINIMAL when reasoning is omitted. Both models support LOW/MEDIUM/HIGH,
|
|
6103
|
+
* default to MEDIUM and cannot disable thinking. Keep the native SDK stream,
|
|
6104
|
+
* credentials, tools and replay signatures; correct only that config.
|
|
6105
|
+
*/
|
|
6106
|
+
function createGeminiApi() {
|
|
6107
|
+
const api = googleGenerativeAIApi();
|
|
6108
|
+
return {
|
|
6109
|
+
...api,
|
|
6110
|
+
streamSimple(model, context, options) {
|
|
6111
|
+
if (!["gemini-4-argon", "gemini-3.8-flash"].includes(model.id)) return api.streamSimple(model, context, options);
|
|
6112
|
+
const level = options?.reasoning === void 0 ? "medium" : clampThinkingLevel(model, options.reasoning);
|
|
6113
|
+
const wireLevel = (model.thinkingLevelMap?.[level] ?? level).toUpperCase();
|
|
6114
|
+
return api.streamSimple(model, context, {
|
|
6115
|
+
...options,
|
|
6116
|
+
onPayload: async (payload, descriptor) => {
|
|
6117
|
+
const { thinkingBudget: _budget, ...thinking } = payload.config?.thinkingConfig ?? {};
|
|
6118
|
+
const normalized = {
|
|
6119
|
+
...payload,
|
|
6120
|
+
config: {
|
|
6121
|
+
...payload.config,
|
|
6122
|
+
thinkingConfig: {
|
|
6123
|
+
...thinking,
|
|
6124
|
+
thinkingLevel: wireLevel
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
};
|
|
6128
|
+
return await options?.onPayload?.(normalized, descriptor) ?? normalized;
|
|
6129
|
+
}
|
|
6130
|
+
});
|
|
6131
|
+
}
|
|
6132
|
+
};
|
|
6133
|
+
}
|
|
6134
|
+
//#endregion
|
|
5378
6135
|
//#region src/provider.js
|
|
5379
6136
|
/**
|
|
5380
6137
|
* The PackyCode pi-ai provider, built in code so the user configures no route.
|
|
5381
6138
|
*
|
|
5382
6139
|
* `createProvider` dispatches on `model.api`, which is what lets one route
|
|
5383
|
-
* carry
|
|
5384
|
-
*
|
|
6140
|
+
* carry Chat Completions, Responses, Anthropic Messages and native Gemini
|
|
6141
|
+
* models — the gateway refuses some of them over other protocols.
|
|
5385
6142
|
*
|
|
5386
6143
|
* @module dsh-packycode-api/provider
|
|
5387
6144
|
*/
|
|
@@ -5413,7 +6170,8 @@ function createPackyCodeProvider() {
|
|
|
5413
6170
|
api: {
|
|
5414
6171
|
"openai-completions": openAICompletionsApi(),
|
|
5415
6172
|
"openai-responses": openAIResponsesApi(),
|
|
5416
|
-
"anthropic-messages": anthropicMessagesApi()
|
|
6173
|
+
"anthropic-messages": anthropicMessagesApi(),
|
|
6174
|
+
"google-generative-ai": createGeminiApi()
|
|
5417
6175
|
}
|
|
5418
6176
|
});
|
|
5419
6177
|
const requestOptions = (model, options) => model.api === "openai-responses" && !model.id.startsWith("gpt-") ? {
|
|
@@ -5505,7 +6263,7 @@ function guardKeyRotation(stream, { onKeyFailure, key } = {}) {
|
|
|
5505
6263
|
//#region src/route.js
|
|
5506
6264
|
/**
|
|
5507
6265
|
* The HTTP model route: the pi-ai provider profile and the adapter that
|
|
5508
|
-
* serves the catalog's HTTP models (DeepSeek, GPT and
|
|
6266
|
+
* serves the catalog's HTTP models (DeepSeek, GPT, Grok and Gemini) with the ring's keys.
|
|
5509
6267
|
*
|
|
5510
6268
|
* @module dsh-packycode-api/route
|
|
5511
6269
|
*/
|
|
@@ -5617,7 +6375,11 @@ function createRouteAdapter({ ring, keys, fallbackSessionId, resolveAttachments,
|
|
|
5617
6375
|
};
|
|
5618
6376
|
const guarded = (method) => (model, context, options) => {
|
|
5619
6377
|
const routed = requestOptions(model, options);
|
|
5620
|
-
return guardKeyRotation(raw[method](model, context, routed), {
|
|
6378
|
+
return guardKeyRotation(guardGeminiReplay(() => raw[method](model, context, routed), {
|
|
6379
|
+
model,
|
|
6380
|
+
context,
|
|
6381
|
+
signal: routed?.signal
|
|
6382
|
+
}), {
|
|
5621
6383
|
key: routed?.apiKey,
|
|
5622
6384
|
onKeyFailure: keys.onKeyFailure
|
|
5623
6385
|
});
|
|
@@ -5683,7 +6445,7 @@ const keyId = (payload) => String(payload?.id ?? "");
|
|
|
5683
6445
|
* web search controller (without one, its endpoints refuse).
|
|
5684
6446
|
* @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
|
|
5685
6447
|
*/
|
|
5686
|
-
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection }) {
|
|
6448
|
+
function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCli, pluginManager, search, modelSelection, images }) {
|
|
5687
6449
|
const need = (service, message) => {
|
|
5688
6450
|
if (service === void 0) throw new Error(message);
|
|
5689
6451
|
return service;
|
|
@@ -5693,6 +6455,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
5693
6455
|
const webSearch = () => need(search, "PackyCode web search is unavailable in this runtime");
|
|
5694
6456
|
const groupReader = () => need(groups, "PackyCode key groups are unavailable in this runtime");
|
|
5695
6457
|
const models = () => need(modelSelection, "PackyCode model selection is unavailable in this runtime");
|
|
6458
|
+
const imageSupport = () => need(images, "PackyCode images are unavailable in this runtime");
|
|
5696
6459
|
const status = async () => ({
|
|
5697
6460
|
...await ring.list(),
|
|
5698
6461
|
hosts: hosts(),
|
|
@@ -5741,6 +6504,7 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
5741
6504
|
});
|
|
5742
6505
|
balance.invalidate();
|
|
5743
6506
|
groups?.invalidate();
|
|
6507
|
+
images?.invalidateBalance?.();
|
|
5744
6508
|
return status();
|
|
5745
6509
|
},
|
|
5746
6510
|
"claude-cli/status": (payload) => cli().status({ force: payload?.force === true }),
|
|
@@ -5759,7 +6523,20 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
|
|
|
5759
6523
|
searchBackend: payload?.searchBackend
|
|
5760
6524
|
}),
|
|
5761
6525
|
"models/status": () => models().status(),
|
|
5762
|
-
"models/update": (payload) => models().update({ selectedIds: payload?.selectedIds })
|
|
6526
|
+
"models/update": (payload) => models().update({ selectedIds: payload?.selectedIds }),
|
|
6527
|
+
"images/status": () => imageSupport().status(),
|
|
6528
|
+
"images/balance": (payload, signal) => imageSupport().readBalance({
|
|
6529
|
+
force: payload?.force === true,
|
|
6530
|
+
signal
|
|
6531
|
+
}),
|
|
6532
|
+
"images/update": (payload) => imageSupport().update(payload),
|
|
6533
|
+
"images/keys/add": (payload) => imageSupport().mutate("add", {
|
|
6534
|
+
key: payload?.key,
|
|
6535
|
+
label: payload?.label
|
|
6536
|
+
}),
|
|
6537
|
+
"images/keys/remove": (payload) => imageSupport().mutate("remove", { id: payload?.id }),
|
|
6538
|
+
"images/keys/select": (payload) => imageSupport().mutate("select", { id: payload?.id }),
|
|
6539
|
+
"images/keys/enable": (payload) => imageSupport().mutate("enable", { id: payload?.id })
|
|
5763
6540
|
};
|
|
5764
6541
|
return async (endpoint, payload, signal) => {
|
|
5765
6542
|
try {
|
|
@@ -6694,16 +7471,23 @@ const Config = z.object({
|
|
|
6694
7471
|
/** Which PackyCode model runs a search: `deepseek` or `gpt`. */
|
|
6695
7472
|
searchBackend: live(z.union([...SEARCH_BACKENDS]).default(DEFAULT_SEARCH_BACKEND)),
|
|
6696
7473
|
/** Only the picker is filtered; an existing session can keep its model. */
|
|
6697
|
-
hiddenModels: live(z.array(z.string()).default([]))
|
|
7474
|
+
hiddenModels: live(z.array(z.string()).default([])),
|
|
7475
|
+
imageEnabled: live(z.boolean().default(true)),
|
|
7476
|
+
imageModel: live(z.union([...IMAGE_MODEL_IDS]).default(DEFAULT_IMAGE_MODEL)),
|
|
7477
|
+
imageQuality: live(z.union([...IMAGE_QUALITIES]).default("auto"))
|
|
6698
7478
|
});
|
|
6699
7479
|
function apply(ctx, config) {
|
|
6700
7480
|
const ring = new PackyCodeKeyRing(ctx.credentials);
|
|
7481
|
+
const imageRing = new PackyCodeKeyRing(ctx.credentials, { recordId: IMAGE_KEY_RECORD_ID });
|
|
6701
7482
|
let source = () => ({
|
|
6702
7483
|
apiHost: normalizeHost(config?.apiHost, DEFAULT_API_HOST),
|
|
6703
7484
|
statusHost: normalizeHost(config?.statusHost, DEFAULT_STATUS_HOST),
|
|
6704
7485
|
searchProvider: fieldValue(config?.searchProvider),
|
|
6705
7486
|
searchBackend: fieldValue(config?.searchBackend),
|
|
6706
|
-
hiddenModels: fieldValue(config?.hiddenModels)
|
|
7487
|
+
hiddenModels: fieldValue(config?.hiddenModels),
|
|
7488
|
+
imageEnabled: fieldValue(config?.imageEnabled),
|
|
7489
|
+
imageModel: fieldValue(config?.imageModel),
|
|
7490
|
+
imageQuality: fieldValue(config?.imageQuality)
|
|
6707
7491
|
});
|
|
6708
7492
|
const hosts = () => {
|
|
6709
7493
|
const current = source() ?? {};
|
|
@@ -6716,6 +7500,11 @@ function apply(ctx, config) {
|
|
|
6716
7500
|
listKeys: () => ring.secrets(),
|
|
6717
7501
|
getHosts: hosts
|
|
6718
7502
|
});
|
|
7503
|
+
const imageBalance = createBalanceReader({
|
|
7504
|
+
listKeys: () => imageRing.secrets(),
|
|
7505
|
+
getHosts: hosts,
|
|
7506
|
+
includeAccount: false
|
|
7507
|
+
});
|
|
6719
7508
|
/**
|
|
6720
7509
|
* Which token groups each key is in, and so which models it can reach.
|
|
6721
7510
|
* Nothing reads it at boot: the settings panel asks for it, and a request
|
|
@@ -6850,7 +7639,22 @@ function apply(ctx, config) {
|
|
|
6850
7639
|
writable: false,
|
|
6851
7640
|
set: void 0
|
|
6852
7641
|
};
|
|
7642
|
+
const imagePreference = {
|
|
7643
|
+
writable: false,
|
|
7644
|
+
set: void 0
|
|
7645
|
+
};
|
|
6853
7646
|
const dshHome = typeof process.env.DSH_HOME === "string" && process.env.DSH_HOME !== "" ? process.env.DSH_HOME : join(homedir(), ".dsh");
|
|
7647
|
+
const images = createImageController({
|
|
7648
|
+
ring: imageRing,
|
|
7649
|
+
balance: imageBalance,
|
|
7650
|
+
readPreference: () => source() ?? {},
|
|
7651
|
+
writePreference: (patch) => imagePreference.set(patch),
|
|
7652
|
+
isWritable: () => imagePreference.writable === true && typeof imagePreference.set === "function"
|
|
7653
|
+
});
|
|
7654
|
+
const imageClient = createPackyCodeImageClient({
|
|
7655
|
+
resolveKey: () => imageRing.activeKey(),
|
|
7656
|
+
getHost: () => hosts().apiHost
|
|
7657
|
+
});
|
|
6854
7658
|
const search = createSearchController({
|
|
6855
7659
|
ring,
|
|
6856
7660
|
hosts,
|
|
@@ -6889,16 +7693,20 @@ function apply(ctx, config) {
|
|
|
6889
7693
|
},
|
|
6890
7694
|
onChange: () => {
|
|
6891
7695
|
balance.invalidate();
|
|
7696
|
+
imageBalance.invalidate();
|
|
6892
7697
|
groups.invalidate();
|
|
6893
7698
|
modelSelection.reconcile();
|
|
7699
|
+
images.reconcile().catch(() => void 0);
|
|
6894
7700
|
search.reconcile();
|
|
6895
7701
|
},
|
|
6896
7702
|
hostPreference,
|
|
6897
7703
|
searchPreference,
|
|
6898
|
-
modelPreference
|
|
7704
|
+
modelPreference,
|
|
7705
|
+
imagePreference
|
|
6899
7706
|
}));
|
|
6900
7707
|
ctx.on?.("loader/volatile-update", () => {
|
|
6901
7708
|
modelSelection.reconcile();
|
|
7709
|
+
images.reconcile().catch(() => void 0);
|
|
6902
7710
|
search.reconcile();
|
|
6903
7711
|
});
|
|
6904
7712
|
/**
|
|
@@ -6921,7 +7729,8 @@ function apply(ctx, config) {
|
|
|
6921
7729
|
claudeCli,
|
|
6922
7730
|
pluginManager: createPackyCodePluginManager(),
|
|
6923
7731
|
search,
|
|
6924
|
-
modelSelection
|
|
7732
|
+
modelSelection,
|
|
7733
|
+
images
|
|
6925
7734
|
});
|
|
6926
7735
|
/**
|
|
6927
7736
|
* The shipped Connection getter retains its own service context, whose row
|
|
@@ -6942,6 +7751,18 @@ function apply(ctx, config) {
|
|
|
6942
7751
|
planActive: (session, agent) => planModeForToolExecution(ctx, session, agent)
|
|
6943
7752
|
})), "packycode-api: claude_code tool");
|
|
6944
7753
|
});
|
|
7754
|
+
ctx.inject(["tools", "attachments"], (imageCtx) => {
|
|
7755
|
+
const store = imageCtx.attachments;
|
|
7756
|
+
if (typeof store?.saveImage !== "function" || typeof store?.readImage !== "function") return;
|
|
7757
|
+
(typeof imageCtx.effect === "function" ? imageCtx : ctx).effect(() => images.mount(() => imageCtx.tools.register(createPackyCodeImageTool({
|
|
7758
|
+
client: imageClient,
|
|
7759
|
+
attachments: store,
|
|
7760
|
+
getPreference: images.preference,
|
|
7761
|
+
onGenerated: () => imageBalance.invalidate(),
|
|
7762
|
+
getSessionMessages: (id) => ctx.get?.("sessions")?.get?.(id)?.deriveMessages?.() ?? [],
|
|
7763
|
+
imageDirectory: join(dshHome, "packycode-api", "images")
|
|
7764
|
+
}))), "packycode-api: image tool availability");
|
|
7765
|
+
});
|
|
6945
7766
|
}
|
|
6946
7767
|
//#endregion
|
|
6947
|
-
export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, LEGACY_STATUS_HOSTS, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, planModeForToolExecution, readClaudeStreamEvents, readHiddenModels, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse };
|
|
7768
|
+
export { AUTO_CLAUDE_MODE, CHANNEL, CLAUDE_CLI_MODES, CLAUDE_INSTALL_COMMAND, CLAUDE_MISSING_MESSAGE, CLAUDE_NPM_PACKAGE, CLAUDE_TOOL_INSTRUCTIONS, CLI_BACKED_MODEL_IDS, CLI_IMAGE_LIMITS, Config, DEFAULT_API_HOST, DEFAULT_APPEND_SYSTEM_PROMPT, DEFAULT_IMAGE_MODEL, DEFAULT_SEARCH_BACKEND, DEFAULT_STATUS_HOST, DISPLAY_NAME, IMAGE_MODELS, IMAGE_MODEL_IDS, IMAGE_QUALITIES, IMAGE_TIMEOUT_MS, IMAGE_TOOL_NAME, LEGACY_STATUS_HOSTS, MAX_IMAGE_BYTES, PACKYCODE_AUTO_SEARCH_PROVIDER_ID, PACKYCODE_MODELS, PACKYCODE_MODEL_IDS, PACKYCODE_SEARCH_PROVIDER_ID, PLUGIN_ID, PROVIDER, PackyCodeKeyRing, SEARCH_BACKENDS, SEARCH_BACKEND_MODELS, SEARCH_MODES, SETTINGS_NAMESPACE, TITLE_MODEL_ID, TITLE_MODEL_ROUTES, accountHostOrder, apply, assertApiKey, buildClaudeArgs, buildClaudeEnvironment, buildClaudeInput, buildDelegatedContent, buildSearchRequest, capClaudeMode, catalogCoverage, classifyClientPolicy, classifyGatewayFailure, claudeBinaryCandidates, claudeModeFor, claudeModeForSession, cliSearchInstructions, createBalanceReader, createClaudeCliAdapter, createClaudeCliManager, createClaudeCodeTool, createClaudeRunner, createClaudeSearchBridge, createGroupReader, createImageController, createKeyChooser, createModelSelectionController, createPackyCodeAutoSearchProvider, createPackyCodeImageClient, createPackyCodeImageTool, createPackyCodeProfile, createPackyCodeProvider, createPackyCodeRpc, createPackyCodeSearchProvider, createProfileSearchComposition, createRouteAdapter, createSearchComposition, createSearchController, decodeImagePng, delegationInstructions, describeRunKey, explainCliClientPolicyFailure, explainCliGroupFailure, explainClientPolicyFailure, explainNpmFailure, findClaudeBinary, guardKeyRotation, inject, keyNote, locateClaudeBinary, locateNpm, name, normalizeFailureMessage, normalizeHost, npmCandidates, parseBillingSubscription, parseBillingUsage, parseClaudeResult, parseClaudeVersion, parseMessagesSearchResponse, parsePricing, parseResponsesSearchResponse, parseSelectedGroups, parseTokenUsage, pickKeyForModel, planModeForSession, planModeForToolExecution, pngDimensions, readClaudeStreamEvents, readHiddenModels, readImagePreference, readSearchChoice, renderAuxiliaryPrompt, renderDelegatedPrompt, resolveSpawnCommand, statusHostOrder, summarizeToolUse, validateImageOptions };
|