@dickpy/dsh-imagegen 1.1.0 → 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/lib/index.js CHANGED
@@ -6,6 +6,8 @@ import { homedir } from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { spawn } from "node:child_process";
9
+ import { createUserMessage } from "@deepseek-ai/dsh-llm/message";
10
+ import { defineTool } from "@deepseek-ai/dsh-tools";
9
11
  //#region src/protocol.ts
10
12
  /**
11
13
  * Wire contract shared by the host and client halves of dsh-imagegen: the
@@ -15,7 +17,7 @@ import { spawn } from "node:child_process";
15
17
  /** Settings namespace this plugin owns (host settings seam + bridge). */
16
18
  const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
17
19
  /** Published package version shared by the host updater and the client UI. */
18
- const PLUGIN_VERSION = "1.1.0";
20
+ const PLUGIN_VERSION = "1.2.0";
19
21
  /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
20
22
  const SETTINGS_API = {
21
23
  describe: "/api/dsh-imagegen/settings/describe",
@@ -28,6 +30,8 @@ const PROMPT_ENHANCE_API = {
28
30
  models: "/api/dsh-imagegen/prompt-enhance/models",
29
31
  enhance: "/api/dsh-imagegen/prompt-enhance"
30
32
  };
33
+ /** Host-mediated candidate discovery for the configured image API. */
34
+ const IMAGE_MODEL_API = { models: "/api/dsh-imagegen/image-models" };
31
35
  /** Host-resident generation queue endpoints. */
32
36
  const TASK_API = {
33
37
  submit: "/api/dsh-imagegen/tasks/submit",
@@ -78,6 +82,78 @@ const TEMPLATES_API = {
78
82
  image: "/api/dsh-imagegen/templates/image"
79
83
  };
80
84
  //#endregion
85
+ //#region src/prompt-enhancer.ts
86
+ function endpoint(base, suffix) {
87
+ return `${base.replace(/\/+$/, "")}${suffix}`;
88
+ }
89
+ function headers(apiKey) {
90
+ return {
91
+ "content-type": "application/json",
92
+ ...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
93
+ };
94
+ }
95
+ async function responseJson(response) {
96
+ const body = await response.json().catch(() => void 0);
97
+ if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
98
+ const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
99
+ throw new Error(message);
100
+ }
101
+ return body;
102
+ }
103
+ /** List candidates exposed by an OpenAI-compatible endpoint. */
104
+ async function listOpenAIModels(config) {
105
+ if (config.apiUrl.trim() === "") throw new Error("API URL is required");
106
+ const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
107
+ const data = Array.isArray(body.data) ? body.data : [];
108
+ return [...new Set(data.flatMap((item) => item !== null && typeof item === "object" && typeof item.id === "string" ? [item.id.trim()] : []).filter(Boolean))].sort((a, b) => a.localeCompare(b));
109
+ }
110
+ /** List chat models exposed by an OpenAI-compatible endpoint. */
111
+ async function listPromptModels(config) {
112
+ return listOpenAIModels(config);
113
+ }
114
+ /** Expand a concise image request into a production-ready image prompt. */
115
+ async function enhancePrompt(config, prompt) {
116
+ if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
117
+ const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
118
+ method: "POST",
119
+ headers: headers(config.apiKey),
120
+ body: JSON.stringify({
121
+ model: config.model.trim(),
122
+ temperature: .7,
123
+ messages: [{
124
+ role: "system",
125
+ content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
126
+ }, {
127
+ role: "user",
128
+ content: prompt
129
+ }]
130
+ })
131
+ }));
132
+ const choices = Array.isArray(body.choices) ? body.choices : [];
133
+ const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
134
+ if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
135
+ return content.trim();
136
+ }
137
+ //#endregion
138
+ //#region src/image-models.ts
139
+ /**
140
+ * Image-model configuration shared by the host, panel, and Agent tools.
141
+ * `/models` exposes candidates only: the configured list is the explicit
142
+ * allow-list because OpenAI-compatible gateways rarely advertise modalities.
143
+ */
144
+ const DEFAULT_IMAGE_MODELS = ["gpt-image-2", "grok-imagine-image"];
145
+ /** Normalize user-entered model identifiers and retain a usable legacy default. */
146
+ function normalizeImageModels(value) {
147
+ const candidates = Array.isArray(value) ? value : [];
148
+ const unique = /* @__PURE__ */ new Set();
149
+ for (const candidate of candidates) {
150
+ if (typeof candidate !== "string") continue;
151
+ const model = candidate.trim();
152
+ if (model !== "") unique.add(model);
153
+ }
154
+ return unique.size > 0 ? [...unique] : [...DEFAULT_IMAGE_MODELS];
155
+ }
156
+ //#endregion
81
157
  //#region src/engine.ts
82
158
  /** A generation failure with a user-presentable message. */
83
159
  var ImageGenError = class extends Error {
@@ -123,6 +199,30 @@ const OPENAI_SIZE_BY_RATIO = {
123
199
  /** Panel ratios that need renaming for a model's vocabulary. Grok documents
124
200
  * 20:9 as its ultra-wide ratio, so the panel's 21:9 label is sent as 20:9. */
125
201
  const GROK_ASPECT_ALIASES = { "21:9": "20:9" };
202
+ /**
203
+ * One request-scoped timeout that is cleared as soon as its fetch settles.
204
+ * AbortSignal.timeout() cannot be disposed early; using it inside a long-lived
205
+ * task queue leaves an otherwise idle Node process holding every timeout.
206
+ */
207
+ function requestSignal(source, timeoutMs) {
208
+ const controller = new AbortController();
209
+ const abortFromSource = () => {
210
+ controller.abort(source?.reason);
211
+ };
212
+ if (source?.aborted === true) abortFromSource();
213
+ else source?.addEventListener("abort", abortFromSource, { once: true });
214
+ const timeout = setTimeout(() => {
215
+ controller.abort(new DOMException("The operation timed out.", "TimeoutError"));
216
+ }, timeoutMs);
217
+ timeout.unref();
218
+ return {
219
+ signal: controller.signal,
220
+ dispose: () => {
221
+ clearTimeout(timeout);
222
+ source?.removeEventListener("abort", abortFromSource);
223
+ }
224
+ };
225
+ }
126
226
  /** Content-type extension hints for URL-fetched images. */
127
227
  function mimeOfExtension(path) {
128
228
  const match = /\.([a-z0-9]+)$/i.exec(path);
@@ -207,14 +307,17 @@ async function normalizeItem(item, upstream) {
207
307
  revisedPrompt
208
308
  };
209
309
  }
310
+ const budget = requestSignal(void 0, IMAGE_FETCH_TIMEOUT_MS);
210
311
  let response;
211
312
  try {
212
313
  response = await fetch(url, {
213
314
  headers: { ...upstream.apiKey === "" ? {} : { authorization: `Bearer ${upstream.apiKey}` } },
214
- signal: AbortSignal.timeout(IMAGE_FETCH_TIMEOUT_MS)
315
+ signal: budget.signal
215
316
  });
216
317
  } catch (error) {
217
318
  throw new ImageGenError(`failed to fetch the generated image url: ${error instanceof Error ? error.message : String(error)}`);
319
+ } finally {
320
+ budget.dispose();
218
321
  }
219
322
  if (!response.ok) throw new ImageGenError(`failed to fetch the generated image url: HTTP ${response.status}`);
220
323
  const buffer = Buffer.from(await response.arrayBuffer());
@@ -273,18 +376,21 @@ async function requestOneImage(baseUrl, upstream, request, params, signal) {
273
376
  ...params
274
377
  });
275
378
  }
379
+ const budget = requestSignal(signal, UPSTREAM_TIMEOUT_MS);
276
380
  let response;
277
381
  try {
278
382
  response = await fetch(`${baseUrl}/images/${request.mode === "edit" ? "edits" : "generations"}`, {
279
383
  method: "POST",
280
384
  headers,
281
385
  body,
282
- signal: signal === void 0 ? AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) : AbortSignal.any([signal, AbortSignal.timeout(UPSTREAM_TIMEOUT_MS)])
386
+ signal: budget.signal
283
387
  });
284
388
  } catch (error) {
285
389
  const message = error instanceof Error ? error.message : String(error);
286
390
  if (/aborter/i.test(message) || /timeout/i.test(message)) throw new ImageGenError("上游接口响应超时(240 秒)", "upstream-timeout");
287
391
  throw new ImageGenError(`无法连接上游接口:${message}`, "upstream-unreachable");
392
+ } finally {
393
+ budget.dispose();
288
394
  }
289
395
  let payload;
290
396
  try {
@@ -340,128 +446,6 @@ function extensionOf$2(mime) {
340
446
  }
341
447
  }
342
448
  //#endregion
343
- //#region src/prompt-enhancer.ts
344
- function endpoint(base, suffix) {
345
- return `${base.replace(/\/+$/, "")}${suffix}`;
346
- }
347
- function headers(apiKey) {
348
- return {
349
- "content-type": "application/json",
350
- ...apiKey.trim() === "" ? {} : { authorization: `Bearer ${apiKey.trim()}` }
351
- };
352
- }
353
- async function responseJson(response) {
354
- const body = await response.json().catch(() => void 0);
355
- if (!response.ok || body === void 0 || body === null || typeof body !== "object") {
356
- const message = body !== null && typeof body === "object" && typeof body.error?.message === "string" ? body.error.message : `HTTP ${response.status}`;
357
- throw new Error(message);
358
- }
359
- return body;
360
- }
361
- /** List chat models exposed by an OpenAI-compatible endpoint. */
362
- async function listPromptModels(config) {
363
- if (config.apiUrl.trim() === "") throw new Error("prompt enhancement API URL is required");
364
- const body = await responseJson(await fetch(endpoint(config.apiUrl, "/models"), { headers: headers(config.apiKey) }));
365
- return (Array.isArray(body.data) ? body.data : []).flatMap((item) => item !== null && typeof item === "object" && typeof item.id === "string" ? [item.id] : []).sort((a, b) => a.localeCompare(b));
366
- }
367
- /** Expand a concise image request into a production-ready image prompt. */
368
- async function enhancePrompt(config, prompt) {
369
- if (config.apiUrl.trim() === "" || config.model.trim() === "") throw new Error("prompt enhancement model is not configured");
370
- const body = await responseJson(await fetch(endpoint(config.apiUrl, "/chat/completions"), {
371
- method: "POST",
372
- headers: headers(config.apiKey),
373
- body: JSON.stringify({
374
- model: config.model.trim(),
375
- temperature: .7,
376
- messages: [{
377
- role: "system",
378
- content: "You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown."
379
- }, {
380
- role: "user",
381
- content: prompt
382
- }]
383
- })
384
- }));
385
- const choices = Array.isArray(body.choices) ? body.choices : [];
386
- const content = choices[0] !== null && typeof choices[0] === "object" ? choices[0].message?.content : void 0;
387
- if (typeof content !== "string" || content.trim() === "") throw new Error("chat model returned an empty prompt");
388
- return content.trim();
389
- }
390
- //#endregion
391
- //#region src/task-queue.ts
392
- /** In-memory, host-resident image generation queue. */
393
- var GenerationTaskQueue = class {
394
- run;
395
- tasks = [];
396
- controllers = /* @__PURE__ */ new Map();
397
- running = false;
398
- constructor(run) {
399
- this.run = run;
400
- }
401
- list() {
402
- return this.tasks.map((task) => ({
403
- ...task,
404
- request: { ...task.request },
405
- ...task.result === void 0 ? {} : { result: task.result }
406
- }));
407
- }
408
- submit(request) {
409
- const task = {
410
- id: randomUUID(),
411
- request: { ...request },
412
- status: "queued",
413
- createdAt: Date.now()
414
- };
415
- this.tasks.unshift(task);
416
- this.drain();
417
- return task;
418
- }
419
- cancel(id) {
420
- const task = this.tasks.find((item) => item.id === id);
421
- if (task === void 0 || task.status === "completed" || task.status === "failed" || task.status === "cancelled") return task;
422
- task.status = "cancelled";
423
- task.finishedAt = Date.now();
424
- this.controllers.get(id)?.abort();
425
- return task;
426
- }
427
- retry(id) {
428
- const previous = this.tasks.find((item) => item.id === id);
429
- return previous === void 0 ? void 0 : this.submit(previous.request);
430
- }
431
- async drain() {
432
- if (this.running) return;
433
- this.running = true;
434
- try {
435
- for (;;) {
436
- const task = this.tasks.find((item) => item.status === "queued");
437
- if (task === void 0) return;
438
- task.status = "running";
439
- task.startedAt = Date.now();
440
- const controller = new AbortController();
441
- this.controllers.set(task.id, controller);
442
- try {
443
- const result = await this.run(task.request, controller.signal);
444
- if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
445
- task.status = "completed";
446
- task.result = result;
447
- task.finishedAt = Date.now();
448
- }
449
- } catch (error) {
450
- if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
451
- task.status = "failed";
452
- task.error = error instanceof Error ? error.message : String(error);
453
- task.finishedAt = Date.now();
454
- }
455
- } finally {
456
- this.controllers.delete(task.id);
457
- }
458
- }
459
- } finally {
460
- this.running = false;
461
- }
462
- }
463
- };
464
- //#endregion
465
449
  //#region src/history-store.ts
466
450
  /**
467
451
  * Host-persisted generation history: images are stored as individual files
@@ -643,6 +627,146 @@ async function readHistoryImage(file) {
643
627
  }
644
628
  }
645
629
  //#endregion
630
+ //#region src/task-queue.ts
631
+ /** In-memory, host-resident image generation queue. */
632
+ var GenerationTaskQueue = class {
633
+ run;
634
+ tasks = [];
635
+ controllers = /* @__PURE__ */ new Map();
636
+ listeners = /* @__PURE__ */ new Set();
637
+ running = false;
638
+ constructor(run) {
639
+ this.run = run;
640
+ }
641
+ list() {
642
+ return this.tasks.map((task) => this.snapshot(task));
643
+ }
644
+ /** Observe queue state changes. Listener failures never disrupt generation. */
645
+ subscribe(listener) {
646
+ this.listeners.add(listener);
647
+ return () => {
648
+ this.listeners.delete(listener);
649
+ };
650
+ }
651
+ submit(request) {
652
+ const task = {
653
+ id: randomUUID(),
654
+ request: { ...request },
655
+ status: "queued",
656
+ createdAt: Date.now()
657
+ };
658
+ this.tasks.unshift(task);
659
+ this.publish(task);
660
+ this.drain();
661
+ return this.snapshot(task);
662
+ }
663
+ cancel(id) {
664
+ const task = this.tasks.find((item) => item.id === id);
665
+ if (task === void 0 || task.status === "completed" || task.status === "failed" || task.status === "cancelled") return task;
666
+ task.status = "cancelled";
667
+ task.finishedAt = Date.now();
668
+ this.controllers.get(id)?.abort();
669
+ this.publish(task);
670
+ return this.snapshot(task);
671
+ }
672
+ retry(id) {
673
+ const previous = this.tasks.find((item) => item.id === id);
674
+ return previous === void 0 ? void 0 : this.submit(previous.request);
675
+ }
676
+ async drain() {
677
+ if (this.running) return;
678
+ this.running = true;
679
+ try {
680
+ for (;;) {
681
+ const task = this.tasks.find((item) => item.status === "queued");
682
+ if (task === void 0) return;
683
+ task.status = "running";
684
+ task.startedAt = Date.now();
685
+ this.publish(task);
686
+ const controller = new AbortController();
687
+ this.controllers.set(task.id, controller);
688
+ try {
689
+ const result = await this.run(task.request, controller.signal);
690
+ if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
691
+ task.status = "completed";
692
+ task.result = result;
693
+ task.finishedAt = Date.now();
694
+ this.publish(task);
695
+ }
696
+ } catch (error) {
697
+ if (this.tasks.find((item) => item.id === task.id)?.status !== "cancelled") {
698
+ task.status = "failed";
699
+ task.error = error instanceof Error ? error.message : String(error);
700
+ task.finishedAt = Date.now();
701
+ this.publish(task);
702
+ }
703
+ } finally {
704
+ this.controllers.delete(task.id);
705
+ }
706
+ }
707
+ } finally {
708
+ this.running = false;
709
+ }
710
+ }
711
+ publish(task) {
712
+ const snapshot = this.snapshot(task);
713
+ for (const listener of this.listeners) try {
714
+ listener(snapshot);
715
+ } catch {}
716
+ }
717
+ snapshot(task) {
718
+ return {
719
+ ...task,
720
+ request: { ...task.request },
721
+ ...task.result === void 0 ? {} : { result: task.result }
722
+ };
723
+ }
724
+ };
725
+ //#endregion
726
+ //#region src/generation-runtime.ts
727
+ /**
728
+ * Shared host-side generation runtime. Both the browser routes and Agent tools
729
+ * submit to this one queue so persisted history and cancellation semantics stay
730
+ * identical regardless of where a request originated.
731
+ */
732
+ var ImageGenerationRuntime = class {
733
+ resolve;
734
+ history;
735
+ queue;
736
+ constructor(resolve, history = { append: appendHistory }) {
737
+ this.resolve = resolve;
738
+ this.history = history;
739
+ this.queue = new GenerationTaskQueue((request, signal) => this.run(request, signal));
740
+ }
741
+ async run(request, signal) {
742
+ const result = await generateImage(this.resolve(), request, { signal });
743
+ try {
744
+ const history = await this.history.append({
745
+ id: randomUUID(),
746
+ createdAt: Date.now(),
747
+ mode: request.mode,
748
+ model: request.model,
749
+ prompt: request.prompt,
750
+ size: request.size,
751
+ quality: request.quality,
752
+ detail: request.detail,
753
+ n: request.n,
754
+ images: result.images,
755
+ ...request.refName === void 0 ? {} : { refName: request.refName }
756
+ });
757
+ return {
758
+ ...result,
759
+ history
760
+ };
761
+ } catch (error) {
762
+ return {
763
+ ...result,
764
+ historyError: error instanceof Error ? error.message : String(error)
765
+ };
766
+ }
767
+ }
768
+ };
769
+ //#endregion
646
770
  //#region src/gallery-store.ts
647
771
  /**
648
772
  * Host-persisted gallery (user-curated favorites): mirrors the history store
@@ -1286,7 +1410,7 @@ function parseGenerateRequest(body) {
1286
1410
  if (prompt === "") return void 0;
1287
1411
  return {
1288
1412
  mode: body.mode === "edit" ? "edit" : "text",
1289
- model: typeof body.model === "string" ? body.model : "gpt-image-2",
1413
+ model: typeof body.model === "string" ? body.model : "",
1290
1414
  prompt,
1291
1415
  size: typeof body.size === "string" ? body.size : "auto",
1292
1416
  quality: typeof body.quality === "string" ? body.quality : "auto",
@@ -1403,34 +1527,19 @@ function makeRoutes(deps) {
1403
1527
  apiKey: "",
1404
1528
  model: ""
1405
1529
  }));
1406
- const runGeneration = async (request, signal) => {
1407
- const result = await generateImage(deps.resolve(), request, { signal });
1408
- try {
1409
- const entries = await history.append({
1410
- id: randomUUID(),
1411
- createdAt: Date.now(),
1412
- mode: request.mode,
1413
- model: request.model,
1414
- prompt: request.prompt,
1415
- size: request.size,
1416
- quality: request.quality,
1417
- detail: request.detail,
1418
- n: request.n,
1419
- images: result.images,
1420
- ...request.refName === void 0 ? {} : { refName: request.refName }
1421
- });
1422
- return {
1423
- ...result,
1424
- history: entries
1425
- };
1426
- } catch (error) {
1427
- return {
1428
- ...result,
1429
- historyError: messageOf(error)
1430
- };
1431
- }
1530
+ const resolveImageModels = deps.resolveImageModels ?? (() => normalizeImageModels(void 0));
1531
+ const parseConfiguredRequest = (body) => {
1532
+ const request = parseGenerateRequest(body);
1533
+ if (request === void 0) return void 0;
1534
+ const models = normalizeImageModels(resolveImageModels());
1535
+ const model = request.model.trim() === "" ? models[0] : request.model.trim();
1536
+ if (!models.includes(model)) throw new Error(`image model "${model}" is not configured; choose one of: ${models.join(", ")}`);
1537
+ return {
1538
+ ...request,
1539
+ model
1540
+ };
1432
1541
  };
1433
- const taskQueue = new GenerationTaskQueue((request, signal) => runGeneration(request, signal));
1542
+ const runtime = deps.runtime ?? new ImageGenerationRuntime(deps.resolve, history);
1434
1543
  const guard = (req, res, method) => {
1435
1544
  if (!isLoopbackRequest(req)) {
1436
1545
  writeJson(res, 403, { error: "forbidden: loopback-only" });
@@ -1443,6 +1552,25 @@ function makeRoutes(deps) {
1443
1552
  return true;
1444
1553
  };
1445
1554
  return [
1555
+ {
1556
+ kind: "exact",
1557
+ path: IMAGE_MODEL_API.models,
1558
+ handler: async (req, res) => {
1559
+ if (!guard(req, res, "POST")) return;
1560
+ try {
1561
+ writeJson(res, 200, {
1562
+ ok: true,
1563
+ models: await listOpenAIModels(deps.resolve())
1564
+ });
1565
+ } catch (error) {
1566
+ writeJson(res, 200, {
1567
+ ok: false,
1568
+ code: "image-models-failed",
1569
+ message: messageOf(error)
1570
+ });
1571
+ }
1572
+ }
1573
+ },
1446
1574
  {
1447
1575
  kind: "exact",
1448
1576
  path: PROMPT_ENHANCE_API.models,
@@ -1565,7 +1693,17 @@ function makeRoutes(deps) {
1565
1693
  });
1566
1694
  return;
1567
1695
  }
1568
- const request = parseGenerateRequest(body);
1696
+ let request;
1697
+ try {
1698
+ request = parseConfiguredRequest(body);
1699
+ } catch (error) {
1700
+ writeJson(res, 200, {
1701
+ ok: false,
1702
+ code: "image-model-not-configured",
1703
+ message: messageOf(error)
1704
+ });
1705
+ return;
1706
+ }
1569
1707
  if (request === void 0) {
1570
1708
  writeJson(res, 200, {
1571
1709
  ok: false,
@@ -1577,7 +1715,7 @@ function makeRoutes(deps) {
1577
1715
  try {
1578
1716
  writeJson(res, 200, {
1579
1717
  ok: true,
1580
- ...await runGeneration(request)
1718
+ ...await runtime.run(request)
1581
1719
  });
1582
1720
  } catch (error) {
1583
1721
  const message = error instanceof Error ? error.message : String(error);
@@ -1595,7 +1733,17 @@ function makeRoutes(deps) {
1595
1733
  handler: async (req, res) => {
1596
1734
  if (!guard(req, res, "POST")) return;
1597
1735
  const body = await readJsonBody(req);
1598
- const request = body === void 0 ? void 0 : parseGenerateRequest(body);
1736
+ let request;
1737
+ try {
1738
+ request = body === void 0 ? void 0 : parseConfiguredRequest(body);
1739
+ } catch (error) {
1740
+ writeJson(res, 200, {
1741
+ ok: false,
1742
+ code: "image-model-not-configured",
1743
+ message: messageOf(error)
1744
+ });
1745
+ return;
1746
+ }
1599
1747
  if (request === void 0) {
1600
1748
  writeJson(res, 200, {
1601
1749
  ok: false,
@@ -1606,7 +1754,7 @@ function makeRoutes(deps) {
1606
1754
  }
1607
1755
  writeJson(res, 200, {
1608
1756
  ok: true,
1609
- task: taskQueue.submit(request)
1757
+ task: runtime.queue.submit(request)
1610
1758
  });
1611
1759
  }
1612
1760
  },
@@ -1617,7 +1765,7 @@ function makeRoutes(deps) {
1617
1765
  if (!guard(req, res, "POST")) return;
1618
1766
  writeJson(res, 200, {
1619
1767
  ok: true,
1620
- tasks: taskQueue.list()
1768
+ tasks: runtime.queue.list()
1621
1769
  });
1622
1770
  }
1623
1771
  },
@@ -1627,7 +1775,7 @@ function makeRoutes(deps) {
1627
1775
  handler: async (req, res) => {
1628
1776
  if (!guard(req, res, "POST")) return;
1629
1777
  const body = await readJsonBody(req);
1630
- const task = typeof body?.id === "string" ? taskQueue.cancel(body.id) : void 0;
1778
+ const task = typeof body?.id === "string" ? runtime.queue.cancel(body.id) : void 0;
1631
1779
  if (task === void 0) {
1632
1780
  writeJson(res, 200, {
1633
1781
  ok: false,
@@ -1648,7 +1796,7 @@ function makeRoutes(deps) {
1648
1796
  handler: async (req, res) => {
1649
1797
  if (!guard(req, res, "POST")) return;
1650
1798
  const body = await readJsonBody(req);
1651
- const task = typeof body?.id === "string" ? taskQueue.retry(body.id) : void 0;
1799
+ const task = typeof body?.id === "string" ? runtime.queue.retry(body.id) : void 0;
1652
1800
  if (task === void 0) {
1653
1801
  writeJson(res, 200, {
1654
1802
  ok: false,
@@ -2097,6 +2245,328 @@ function makeRoutes(deps) {
2097
2245
  ];
2098
2246
  }
2099
2247
  //#endregion
2248
+ //#region src/agent-image-tools.ts
2249
+ const imageRefSchema = {
2250
+ type: "object",
2251
+ additionalProperties: false,
2252
+ properties: {
2253
+ attachment_id: {
2254
+ type: "string",
2255
+ required: true
2256
+ },
2257
+ media_type: {
2258
+ type: "string",
2259
+ required: true
2260
+ },
2261
+ bytes: {
2262
+ type: "integer",
2263
+ required: true
2264
+ },
2265
+ width: {
2266
+ type: "integer",
2267
+ required: true
2268
+ },
2269
+ height: {
2270
+ type: "integer",
2271
+ required: true
2272
+ },
2273
+ name: { type: "string" }
2274
+ }
2275
+ };
2276
+ const taskResultSchema = {
2277
+ type: "object",
2278
+ additionalProperties: false,
2279
+ properties: {
2280
+ task_id: {
2281
+ type: "string",
2282
+ required: true
2283
+ },
2284
+ status: {
2285
+ type: "string",
2286
+ required: true
2287
+ },
2288
+ message: {
2289
+ type: "string",
2290
+ required: true
2291
+ },
2292
+ error: { type: "string" },
2293
+ images: {
2294
+ type: "array",
2295
+ required: true,
2296
+ items: imageRefSchema
2297
+ }
2298
+ }
2299
+ };
2300
+ function acceptedMediaType(value) {
2301
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
2302
+ }
2303
+ function projectRef(ref) {
2304
+ return {
2305
+ attachment_id: String(ref.attachmentId),
2306
+ media_type: ref.mediaType,
2307
+ bytes: ref.bytes,
2308
+ width: ref.width,
2309
+ height: ref.height,
2310
+ ...ref.name === void 0 ? {} : { name: ref.name }
2311
+ };
2312
+ }
2313
+ function restoreRef(value) {
2314
+ if (!acceptedMediaType(value.media_type)) throw new ImageGenError("source_image.media_type is not a supported image type", "bad-reference-image");
2315
+ if (!Number.isInteger(value.bytes) || value.bytes < 1 || !Number.isInteger(value.width) || value.width < 1 || !Number.isInteger(value.height) || value.height < 1) throw new ImageGenError("source_image metadata is invalid", "bad-reference-image");
2316
+ return {
2317
+ attachmentId: value.attachment_id,
2318
+ mediaType: value.media_type,
2319
+ bytes: value.bytes,
2320
+ width: value.width,
2321
+ height: value.height,
2322
+ ...value.name === void 0 ? {} : { name: value.name }
2323
+ };
2324
+ }
2325
+ function imageDataUrl(image) {
2326
+ return `data:${image.ref.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
2327
+ }
2328
+ function renderTaskResult(value) {
2329
+ return [{
2330
+ type: "text",
2331
+ text: JSON.stringify(value)
2332
+ }, ...value.images.map((image) => ({
2333
+ type: "image",
2334
+ attachment: restoreRef(image)
2335
+ }))];
2336
+ }
2337
+ /** Register the global Agent tools and unregister them with the plugin lifecycle. */
2338
+ function registerAgentImageTools(ctx, runtime, resolve) {
2339
+ const attachmentRefs = /* @__PURE__ */ new Map();
2340
+ const taskSubscriptions = /* @__PURE__ */ new Set();
2341
+ const ensureConfigured = () => {
2342
+ const config = resolve();
2343
+ if (!config.enabled) throw new ImageGenError("AI image generation is disabled. Open Settings > Plugins > AI Image and enable it.", "plugin-disabled");
2344
+ if (!config.allowAgentImageGeneration) throw new ImageGenError("Agent image generation is disabled in Settings > Plugins > AI Image.", "agent-generation-disabled");
2345
+ if (config.apiUrl.trim() === "" || config.apiKey.trim() === "") throw new ImageGenError("Image API credentials are not configured. Open Settings > Plugins > AI Image and fill in API URL and API key.", "image-api-not-configured");
2346
+ };
2347
+ const selectedModel = (requested) => {
2348
+ const models = normalizeImageModels(resolve().imageModels);
2349
+ const model = typeof requested === "string" && requested.trim() !== "" ? requested.trim() : models[0];
2350
+ if (!models.includes(model)) throw new ImageGenError(`Image model "${model}" is not configured. Choose one of: ${models.join(", ")}.`, "image-model-not-configured");
2351
+ return model;
2352
+ };
2353
+ const materializeTaskImages = (task) => {
2354
+ if (task.status !== "completed") return Promise.resolve([]);
2355
+ const existing = attachmentRefs.get(task.id);
2356
+ if (existing !== void 0) return existing;
2357
+ const pending = ctx.attachments.saveImages((task.result?.images ?? []).map((image, index) => toSaveImage(image, task.id, index))).then((refs) => refs.map(projectRef));
2358
+ attachmentRefs.set(task.id, pending);
2359
+ pending.catch(() => {
2360
+ if (attachmentRefs.get(task.id) === pending) attachmentRefs.delete(task.id);
2361
+ });
2362
+ return pending;
2363
+ };
2364
+ const taskResult = async (task) => {
2365
+ const images = await materializeTaskImages(task);
2366
+ return {
2367
+ task_id: task.id,
2368
+ status: task.status,
2369
+ message: task.status === "completed" ? "Generation completed. The images are attached below and can be reused as source_image in edit_image." : task.status === "failed" ? "Generation failed." : task.status === "cancelled" ? "Generation was cancelled." : "Generation is still running. Completion will be delivered to the conversation automatically with image attachments.",
2370
+ ...task.error === void 0 ? {} : { error: task.error },
2371
+ images
2372
+ };
2373
+ };
2374
+ const findTask = (id) => {
2375
+ const task = runtime.queue.list().find((candidate) => candidate.id === id);
2376
+ if (task === void 0) throw new ImageGenError(`Image generation task ${id} was not found.`, "task-not-found");
2377
+ return task;
2378
+ };
2379
+ const notifyCompletion = async (agent, task) => {
2380
+ const result = await taskResult(task);
2381
+ const completed = task.status === "completed";
2382
+ const text = completed ? `图像生成任务已完成(${task.id})。图片已附在这条消息中,可以直接查看、下载或作为后续图生图的参考。` : task.status === "cancelled" ? `图像生成任务已取消(${task.id})。` : `图像生成任务失败(${task.id}):${task.error ?? "未知错误"}`;
2383
+ agent.send(createUserMessage({
2384
+ content: [{
2385
+ type: "text",
2386
+ text
2387
+ }, ...completed ? result.images.map((image) => ({
2388
+ type: "image",
2389
+ attachment: restoreRef(image)
2390
+ })) : []],
2391
+ source: { kind: "user" }
2392
+ }), "next-turn", true);
2393
+ };
2394
+ const watchTask = (task, agent) => {
2395
+ if (agent === void 0) return;
2396
+ let dispose;
2397
+ const onChange = (updated) => {
2398
+ if (updated.id !== task.id || !isFinalTask(updated)) return;
2399
+ dispose?.();
2400
+ if (dispose !== void 0) taskSubscriptions.delete(dispose);
2401
+ notifyCompletion(agent, updated).catch(() => {});
2402
+ };
2403
+ dispose = runtime.queue.subscribe(onChange);
2404
+ taskSubscriptions.add(dispose);
2405
+ const current = findTask(task.id);
2406
+ if (isFinalTask(current)) onChange(current);
2407
+ };
2408
+ const disposers = [
2409
+ ctx.tools.register(defineTool({
2410
+ name: "generate_image",
2411
+ description: "Queue a text-to-image generation request. This returns immediately with a task id. When it finishes, the conversation automatically receives a visible image-attachment notification; do not repeatedly poll. Only use models configured for this plugin; omit model to use the first configured image model. Use get_image_generation_task only for an explicit status check or recovery.",
2412
+ parameters: {
2413
+ prompt: {
2414
+ type: "string",
2415
+ required: true,
2416
+ description: "Detailed image-generation prompt."
2417
+ },
2418
+ model: {
2419
+ type: "string",
2420
+ description: "One of the configured image models. Defaults to the first configured model."
2421
+ },
2422
+ size: {
2423
+ type: "string",
2424
+ description: "Aspect ratio such as 1:1, 16:9, 9:16, or auto."
2425
+ },
2426
+ quality: {
2427
+ type: "string",
2428
+ description: "auto, 1k, 2k, or 4k."
2429
+ },
2430
+ count: {
2431
+ type: "integer",
2432
+ description: "Number of images, 1 to 4. Defaults to 1."
2433
+ },
2434
+ detail: {
2435
+ type: "string",
2436
+ description: "Optional provider detail value, for example standard or high."
2437
+ }
2438
+ },
2439
+ output: {
2440
+ schema: taskResultSchema,
2441
+ render: (_args, value) => renderTaskResult(value)
2442
+ },
2443
+ async execute(args, exec) {
2444
+ ensureConfigured();
2445
+ const task = runtime.queue.submit({
2446
+ mode: "text",
2447
+ model: selectedModel(args.model),
2448
+ prompt: args.prompt.trim(),
2449
+ size: args.size ?? "auto",
2450
+ quality: args.quality ?? "auto",
2451
+ n: Math.min(4, Math.max(1, args.count ?? 1)),
2452
+ detail: args.detail ?? ""
2453
+ });
2454
+ watchTask(task, exec.agent);
2455
+ return taskResult(task);
2456
+ }
2457
+ })),
2458
+ ctx.tools.register(defineTool({
2459
+ name: "edit_image",
2460
+ description: "Queue an image-to-image edit. source_image must be an image reference returned by a completed generation notification or get_image_generation_task; pass that entire object unchanged. Only configured image models are allowed; omit model to use the first configured model. Completion is automatically delivered to the conversation with visible image attachments; do not repeatedly poll.",
2461
+ parameters: {
2462
+ prompt: {
2463
+ type: "string",
2464
+ required: true,
2465
+ description: "How to transform the source image."
2466
+ },
2467
+ source_image: {
2468
+ ...imageRefSchema,
2469
+ required: true,
2470
+ description: "Image reference returned by get_image_generation_task."
2471
+ },
2472
+ model: {
2473
+ type: "string",
2474
+ description: "One of the configured image models. Defaults to the first configured model."
2475
+ },
2476
+ size: {
2477
+ type: "string",
2478
+ description: "Aspect ratio such as 1:1, 16:9, 9:16, or auto."
2479
+ },
2480
+ quality: {
2481
+ type: "string",
2482
+ description: "auto, 1k, 2k, or 4k."
2483
+ },
2484
+ count: {
2485
+ type: "integer",
2486
+ description: "Number of images, 1 to 4. Defaults to 1."
2487
+ },
2488
+ detail: {
2489
+ type: "string",
2490
+ description: "Optional provider detail value."
2491
+ }
2492
+ },
2493
+ output: {
2494
+ schema: taskResultSchema,
2495
+ render: (_args, value) => renderTaskResult(value)
2496
+ },
2497
+ async execute(args, exec) {
2498
+ ensureConfigured();
2499
+ const reference = await ctx.attachments.readImage(restoreRef(args.source_image), exec.signal);
2500
+ const task = runtime.queue.submit({
2501
+ mode: "edit",
2502
+ model: selectedModel(args.model),
2503
+ prompt: args.prompt.trim(),
2504
+ size: args.size ?? "auto",
2505
+ quality: args.quality ?? "auto",
2506
+ n: Math.min(4, Math.max(1, args.count ?? 1)),
2507
+ detail: args.detail ?? "",
2508
+ image: imageDataUrl(reference),
2509
+ ...reference.ref.name === void 0 ? {} : { refName: reference.ref.name }
2510
+ });
2511
+ watchTask(task, exec.agent);
2512
+ return taskResult(task);
2513
+ }
2514
+ })),
2515
+ ctx.tools.register(defineTool({
2516
+ name: "get_image_generation_task",
2517
+ description: "Optionally check an image-generation task status. Completed tasks return image references for edit_image, but the conversation already receives a visible completion notification automatically; do not poll repeatedly.",
2518
+ parameters: { task_id: {
2519
+ type: "string",
2520
+ required: true,
2521
+ description: "Task id returned by generate_image or edit_image."
2522
+ } },
2523
+ output: {
2524
+ schema: taskResultSchema,
2525
+ render: (_args, value) => renderTaskResult(value)
2526
+ },
2527
+ async execute(args) {
2528
+ ensureConfigured();
2529
+ return taskResult(findTask(args.task_id));
2530
+ }
2531
+ })),
2532
+ ctx.tools.register(defineTool({
2533
+ name: "cancel_image_generation_task",
2534
+ description: "Cancel a queued or running image generation task.",
2535
+ parameters: { task_id: {
2536
+ type: "string",
2537
+ required: true,
2538
+ description: "Task id returned by generate_image or edit_image."
2539
+ } },
2540
+ output: {
2541
+ schema: taskResultSchema,
2542
+ render: (_args, value) => renderTaskResult(value)
2543
+ },
2544
+ async execute(args) {
2545
+ ensureConfigured();
2546
+ const task = runtime.queue.cancel(args.task_id);
2547
+ if (task === void 0) throw new ImageGenError(`Image generation task ${args.task_id} was not found.`, "task-not-found");
2548
+ return taskResult(task);
2549
+ }
2550
+ }))
2551
+ ];
2552
+ return () => {
2553
+ for (const dispose of taskSubscriptions) dispose();
2554
+ taskSubscriptions.clear();
2555
+ for (const dispose of disposers) dispose();
2556
+ };
2557
+ }
2558
+ function isFinalTask(task) {
2559
+ return task.status === "completed" || task.status === "failed" || task.status === "cancelled";
2560
+ }
2561
+ function toSaveImage(image, taskId, index) {
2562
+ const mediaType = acceptedMediaType(image.mime) ? image.mime : "image/png";
2563
+ return {
2564
+ data: Buffer.from(image.b64, "base64"),
2565
+ mediaType,
2566
+ name: `imagegen-${taskId}-${index + 1}.${mediaType === "image/jpeg" ? "jpg" : mediaType.slice(6)}`
2567
+ };
2568
+ }
2569
+ //#endregion
2100
2570
  //#region src/index.ts
2101
2571
  /** Stable cordis plugin name. */
2102
2572
  const name = "imagegen";
@@ -2107,8 +2577,10 @@ const ImageGenSettingsNamespace = settingsNamespace(IMAGEGEN_SETTINGS_NAMESPACE)
2107
2577
  const Config = z.object({
2108
2578
  enabled: z.boolean().default(true),
2109
2579
  announceToAgent: z.boolean().default(true),
2580
+ allowAgentImageGeneration: z.boolean().default(true),
2110
2581
  apiUrl: z.string().default(""),
2111
2582
  apiKey: z.string().role("secret").default(""),
2583
+ imageModels: z.array(z.string()).default([...DEFAULT_IMAGE_MODELS]),
2112
2584
  promptApiUrl: z.string().default(""),
2113
2585
  promptApiKey: z.string().role("secret").default(""),
2114
2586
  promptModel: z.string().default("")
@@ -2116,10 +2588,15 @@ const Config = z.object({
2116
2588
  /** Schema defaults, re-read for hand-built contexts (the loader applies them normally). */
2117
2589
  const DEFAULT_ENABLED = true;
2118
2590
  const DEFAULT_ANNOUNCE = true;
2591
+ const DEFAULT_ALLOW_AGENT_IMAGE_GENERATION = true;
2119
2592
  /** Order of the announcement section within the tool-guidance band. */
2120
2593
  const SECTION_ORDER = 150;
2121
2594
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
2122
- const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API(模型 gpt-image-2 / grok-imagine-image),支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok 模型按官方 JSON image_url 协议发送);API 地址与密钥在 GUI「设置 插件 → 可配置」中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载;可一键把满意的图片加入「画廊」(工作台顶部 文生图 / 图生图 / 画廊 三个标签页,画廊在右侧展示,收藏持久化在本地 ~/.dsh/dsh-imagegen/gallery/)。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条 gpt-image-2 提示词案例(含中文标题、分类、参考图,可搜索/按分类筛选),参考图经宿主代理按需缓存到本地;用户可一键把模板提示词填入提示词框再生成。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / gpt-image-2 / grok-imagine-image / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
2595
+ const IMAGEGEN_GUIDANCE = "本机已安装 dsh-imagegen 插件(DSH AI 生图):侧边栏「AI 生图」入口。能力:对接 OpenAI 兼容图像生成 API,模型由用户在「设置 插件 → AI 生图」中检测或手动配置的生图模型列表决定;支持文生图(/images/generations)与图生图(/images/edits,上传参考图,grok-imagine 模型按官方 JSON image_url 协议发送)。API 地址与密钥在 GUI 设置中配置,密钥仅存于本机设置文档;生成请求由本地宿主代理转发,结果以 base64 返回面板,可预览与下载。模型只能使用已配置的生图模型;模型出现在 /models 中不等于其网关原生支持生图协议,遇到 Qwen、Gemini 等非 OpenAI 生图协议时应如实说明上游兼容性。可一键把满意的图片加入「画廊」。内置「提示词模板库」(面板提示词框左下角「模板库」按钮):打包 awesome-gpt-image-2 的数百条提示词案例,可搜索、筛选与复用。Agent 可直接调用 `generate_image` 提交文生图,也可用 `edit_image` 图生图;任务后台异步执行,完成后插件会自动唤醒原对话,并以可直接查看和复用的图片附件回贴结果,因此不要反复轮询。仅在用户明确要求进度或需要恢复任务时,才使用 `get_image_generation_task` 查询状态。限制:生成消耗上游 API 额度;图片内容由上游模型生成,可能不符合预期或包含不适宜内容;api_key 以明文存储在设置文档中;参考图会发送至所配置的 API 服务;模板库在线刷新与参考图首次加载需要访问 vibeui.top。用户提到「生图 / 绘画 / 生成图片 / 文生图 / 图生图 / 画廊 / 提示词模板」时即指本插件,请据此协作。";
2596
+ /** Add the live allow-list so an Agent can honor a user's model choice. */
2597
+ function guidanceFor(imageModels) {
2598
+ return `${IMAGEGEN_GUIDANCE} 当前允许调用的生图模型:${imageModels.join("、")}。用户指定其中某个模型时,工具参数 model 必须使用该精确名称;未指定时使用列表中的第一个。`;
2599
+ }
2123
2600
  /**
2124
2601
  * Mount the settings section, routes, and announcement.
2125
2602
  * @param ctx - host plugin context carrying webServer/systemPrompt.
@@ -2132,13 +2609,22 @@ function apply(ctx, config) {
2132
2609
  return {
2133
2610
  enabled: value.enabled ?? DEFAULT_ENABLED,
2134
2611
  announceToAgent: value.announceToAgent ?? DEFAULT_ANNOUNCE,
2612
+ allowAgentImageGeneration: value.allowAgentImageGeneration ?? DEFAULT_ALLOW_AGENT_IMAGE_GENERATION,
2135
2613
  apiUrl: value.apiUrl ?? "",
2136
2614
  apiKey: value.apiKey ?? "",
2615
+ imageModels: normalizeImageModels(value.imageModels),
2137
2616
  promptApiUrl: value.promptApiUrl ?? "",
2138
2617
  promptApiKey: value.promptApiKey ?? "",
2139
2618
  promptModel: value.promptModel ?? ""
2140
2619
  };
2141
2620
  };
2621
+ const runtime = new ImageGenerationRuntime(() => {
2622
+ const value = resolve();
2623
+ return {
2624
+ apiUrl: value.apiUrl,
2625
+ apiKey: value.apiKey
2626
+ };
2627
+ });
2142
2628
  ctx.inject(["settings"], (sctx) => {
2143
2629
  const seam = sctx.get("settings");
2144
2630
  sctx.effect(() => {
@@ -2158,13 +2644,27 @@ function apply(ctx, config) {
2158
2644
  apiKey: value.promptApiKey.trim() || value.apiKey,
2159
2645
  model: value.promptModel
2160
2646
  };
2161
- }
2647
+ },
2648
+ resolveImageModels: () => resolve().imageModels,
2649
+ runtime
2162
2650
  }).map((route) => ctx.webServer.register(route));
2163
2651
  return () => {
2164
2652
  for (const dispose of disposers) dispose();
2165
2653
  };
2166
2654
  }, "dsh-imagegen: routes");
2167
2655
  });
2656
+ ctx.inject(["tools", "attachments"], (tctx) => {
2657
+ tctx.effect(() => registerAgentImageTools(tctx, runtime, () => {
2658
+ const value = resolve();
2659
+ return {
2660
+ enabled: value.enabled,
2661
+ allowAgentImageGeneration: value.allowAgentImageGeneration,
2662
+ apiUrl: value.apiUrl,
2663
+ apiKey: value.apiKey,
2664
+ imageModels: value.imageModels
2665
+ };
2666
+ }), "dsh-imagegen: agent image tools");
2667
+ });
2168
2668
  let disposeSection;
2169
2669
  const sync = () => {
2170
2670
  if (disposeSection !== void 0) {
@@ -2176,7 +2676,7 @@ function apply(ctx, config) {
2176
2676
  disposeSection = ctx.systemPrompt.section({
2177
2677
  name: "plugin:dsh-imagegen",
2178
2678
  order: SECTION_ORDER,
2179
- text: IMAGEGEN_GUIDANCE
2679
+ text: guidanceFor(value.imageModels)
2180
2680
  });
2181
2681
  };
2182
2682
  installSettingsSection(ctx, ImageGenSettingsNamespace, Config, config ?? {}, {
@@ -2189,4 +2689,4 @@ function apply(ctx, config) {
2189
2689
  sync();
2190
2690
  }
2191
2691
  //#endregion
2192
- export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, removeGallery, updateGalleryTags };
2692
+ export { CURRENT_VERSION, Config, IMAGEGEN_GUIDANCE, ImageGenError, ImageGenSettingsNamespace, ImageGenerationRuntime, appendGallery, apply, checkForUpdate, clearGallery, clearTemplateMemo, clearUpdateCache, compareVersions, generateImage, inject, installUpdate, listGallery, listTemplates, makeRoutes, name, profileFromProcess, readGalleryImage, readTemplateImage, refreshTemplates, registerAgentImageTools, removeGallery, updateGalleryTags };