dsh-image-gen 0.1.7 → 0.2.1

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
@@ -2,9 +2,9 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
3
  import { defineTool } from "@deepseek-ai/dsh-tools";
4
4
  import z from "@deepseek-ai/schemastery";
5
- import { randomUUID } from "node:crypto";
6
- import { mkdir, realpath, rename, unlink, writeFile } from "node:fs/promises";
5
+ import { mkdir, readFile, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
7
6
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
8
  //#region lib/types/shared.js
9
9
  /** Values shared by the Host and browser Bundle faces. */
10
10
  /** Browser route used by the generated-image card. */
@@ -27,7 +27,7 @@ const DEFAULT_DASHSCOPE_ENDPOINT = "https://dashscope.aliyuncs.com/api/v1";
27
27
  const DEFAULT_GOOGLE_MODEL = "gemini-3.1-flash-image";
28
28
  const DEFAULT_OPENAI_MODEL = "gpt-image-2";
29
29
  const DEFAULT_SEEDREAM_MODEL = "doubao-seedream-5-0-260128";
30
- const DEFAULT_DASHSCOPE_MODEL = "wanx2.1-t2i-turbo";
30
+ const DEFAULT_DASHSCOPE_MODEL = "qwen-image-3.0";
31
31
  //#endregion
32
32
  //#region lib/types/config.js
33
33
  /** User-facing configuration for supported image providers. */
@@ -75,7 +75,7 @@ function resolveProvider(config) {
75
75
  case "dashscope": return {
76
76
  provider: "dashscope",
77
77
  apiKeyEnv: DASHSCOPE_API_KEY_ENV,
78
- model: config.dashscopeModel ?? "wanx2.1-t2i-turbo",
78
+ model: config.dashscopeModel ?? "qwen-image-3.0",
79
79
  endpoint: config.dashscopeEndpoint ?? "https://dashscope.aliyuncs.com/api/v1",
80
80
  imageSize: "1024*1024"
81
81
  };
@@ -91,83 +91,75 @@ function resolveProvider(config) {
91
91
  }
92
92
  //#endregion
93
93
  //#region lib/types/dashscope.js
94
- /** Call DashScope image synthesis/generation API and handle both synchronous output & async task polling. */
95
94
  async function generateDashScopeImage(options) {
95
+ assertQwenImageModel(options.model);
96
+ const formattedSize = formatSize(options.size);
97
+ return requestQwenImage({
98
+ ...options,
99
+ requestBody: {
100
+ model: options.model,
101
+ input: { messages: [{
102
+ role: "user",
103
+ content: [{ text: options.prompt }]
104
+ }] },
105
+ parameters: { ...formattedSize === void 0 ? {} : { size: formattedSize } }
106
+ },
107
+ operation: "generation"
108
+ });
109
+ }
110
+ async function editDashScopeImage(options) {
111
+ assertQwenImageModel(options.model);
112
+ const formattedSize = formatSize(options.size);
113
+ return requestQwenImage({
114
+ ...options,
115
+ requestBody: {
116
+ model: options.model,
117
+ input: { messages: [{
118
+ role: "user",
119
+ content: [...options.sourceImages.map((sourceImage) => ({ image: toDataUrl$1(sourceImage) })), { text: options.prompt }]
120
+ }] },
121
+ parameters: {
122
+ prompt_extend: true,
123
+ ...formattedSize === void 0 ? {} : { size: formattedSize }
124
+ }
125
+ },
126
+ operation: "editing"
127
+ });
128
+ }
129
+ function assertQwenImageModel(model) {
130
+ if (!model.toLowerCase().startsWith("qwen-image")) throw new Error(`Unsupported DashScope image model ${model}. Configure a qwen-image model.`);
131
+ }
132
+ function formatSize(size) {
133
+ if (size === void 0 || size.length === 0) return void 0;
134
+ return size.replace("x", "*");
135
+ }
136
+ function toDataUrl$1(image) {
137
+ return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
138
+ }
139
+ async function requestQwenImage(options) {
96
140
  const base = options.endpoint.replace(/\/+$/, "");
97
- const formattedSize = (options.size ?? "1024*1024").replace("x", "*");
98
- const isQwenImage = options.model.toLowerCase().startsWith("qwen-image");
99
- const submitUrl = isQwenImage ? `${base}/services/aigc/multimodal-generation/generation` : `${base}/services/aigc/text2image/image-synthesis`;
100
- const requestBody = isQwenImage ? {
101
- model: options.model,
102
- input: { messages: [{
103
- role: "user",
104
- content: [{ text: options.prompt }]
105
- }] },
106
- parameters: { size: formattedSize }
107
- } : {
108
- model: options.model,
109
- input: { prompt: options.prompt },
110
- parameters: {
111
- size: formattedSize,
112
- n: 1
113
- }
114
- };
115
- const submitResponse = await fetch(submitUrl, {
141
+ const response = await fetch(`${base}/services/aigc/multimodal-generation/generation`, {
116
142
  method: "POST",
117
143
  ...options.signal ? { signal: options.signal } : {},
118
144
  headers: {
119
145
  "content-type": "application/json",
120
- authorization: `Bearer ${options.apiKey}`,
121
- "X-DashScope-Async": "enable"
146
+ authorization: `Bearer ${options.apiKey}`
122
147
  },
123
- body: JSON.stringify(requestBody)
148
+ body: JSON.stringify(options.requestBody)
124
149
  });
125
- if (!submitResponse.ok) {
126
- const errorText = await submitResponse.text();
127
- throw new Error(`DashScope image generation failed (${String(submitResponse.status)}): ${errorText}`);
128
- }
129
- const submitResult = await submitResponse.json();
130
- const directImageUrl = extractImageUrl(submitResult);
131
- if (directImageUrl !== void 0) return downloadImageBlob(directImageUrl, options);
132
- const taskId = submitResult.output?.task_id;
133
- if (taskId === void 0 || taskId.length === 0) throw new Error(`DashScope did not return an image URL or task_id: ${submitResult.message ?? JSON.stringify(submitResult)}`);
134
- const taskQueryUrl = `${base}/tasks/${taskId}`;
135
- const startTime = Date.now();
136
- const timeoutMs = 6e4;
137
- while (Date.now() - startTime < timeoutMs) {
138
- if (options.signal?.aborted) throw new Error("DashScope image task polling aborted");
139
- await new Promise((resolve, reject) => {
140
- const timer = setTimeout(resolve, 1500);
141
- if (options.signal !== void 0) options.signal.addEventListener("abort", () => {
142
- clearTimeout(timer);
143
- reject(/* @__PURE__ */ new Error("DashScope image generation aborted"));
144
- }, { once: true });
145
- });
146
- const taskResponse = await fetch(taskQueryUrl, {
147
- method: "GET",
148
- ...options.signal ? { signal: options.signal } : {},
149
- headers: { authorization: `Bearer ${options.apiKey}` }
150
- });
151
- if (!taskResponse.ok) {
152
- const errorText = await taskResponse.text();
153
- throw new Error(`DashScope task query failed (${String(taskResponse.status)}): ${errorText}`);
154
- }
155
- const taskResult = await taskResponse.json();
156
- const status = taskResult.output?.task_status;
157
- if (status === "SUCCEEDED") {
158
- const imageUrl = extractImageUrl(taskResult);
159
- if (imageUrl === void 0 || imageUrl.length === 0) throw new Error("DashScope task succeeded but returned no image URL");
160
- return downloadImageBlob(imageUrl, options);
161
- }
162
- if (status === "FAILED") throw new Error(`DashScope image generation failed: ${taskResult.output?.message ?? taskResult.message ?? "Unknown error"}`);
150
+ if (!response.ok) {
151
+ const errorText = await response.text();
152
+ throw new Error(`DashScope image ${options.operation} failed (${String(response.status)}): ${errorText}`);
163
153
  }
164
- throw new Error(`DashScope image generation timed out after ${String(timeoutMs / 1e3)} seconds`);
154
+ const payload = await response.json();
155
+ const imageUrl = extractImageUrl(payload);
156
+ if (imageUrl === void 0) throw new Error(`DashScope image ${options.operation} returned no image URL: ${payload.message ?? JSON.stringify(payload)}`);
157
+ return downloadImageBlob(imageUrl, options);
165
158
  }
166
159
  function extractImageUrl(response) {
167
- const resultUrl = response.output?.results?.[0]?.url;
168
- if (resultUrl !== void 0 && resultUrl.length > 0) return resultUrl;
169
160
  const contents = response.output?.choices?.[0]?.message?.content;
170
- if (Array.isArray(contents)) for (const item of contents) {
161
+ if (!Array.isArray(contents)) return void 0;
162
+ for (const item of contents) {
171
163
  if (item.image !== void 0 && item.image.length > 0) return item.image;
172
164
  if (item.image_url !== void 0 && item.image_url.length > 0) return item.image_url;
173
165
  if (item.url !== void 0 && item.url.length > 0) return item.url;
@@ -187,10 +179,34 @@ async function downloadImageBlob(imageUrl, options) {
187
179
  }
188
180
  //#endregion
189
181
  //#region lib/types/google.js
190
- const ERROR_LIMIT$1 = 4096;
182
+ const ERROR_LIMIT$2 = 4096;
191
183
  const REQUESTED_MEDIA_TYPE = "image/jpeg";
192
- /** Send one native Google image request and validate its inline image response. */
193
- async function generateGoogleImage(input) {
184
+ /** Send one native Google text-to-image request. */
185
+ function generateGoogleImage(input) {
186
+ return requestGoogleImage({
187
+ ...input,
188
+ operation: "generation",
189
+ interactionInput: input.prompt
190
+ });
191
+ }
192
+ /** Send one native Google image-editing request using already-resolved bytes. */
193
+ function editGoogleImage(input) {
194
+ return requestGoogleImage({
195
+ ...input,
196
+ operation: "editing",
197
+ interactionInput: [{
198
+ type: "text",
199
+ text: input.prompt
200
+ }, ...input.sourceImages.map((sourceImage) => ({
201
+ type: "image",
202
+ mime_type: sourceImage.mediaType,
203
+ data: Buffer.from(sourceImage.data).toString("base64")
204
+ }))]
205
+ });
206
+ }
207
+ /** Shared Google request, response parsing, decoding, and size enforcement. */
208
+ async function requestGoogleImage(input) {
209
+ const label = `Google image ${input.operation}`;
194
210
  const response = await fetch(input.endpoint, {
195
211
  method: "POST",
196
212
  redirect: "error",
@@ -201,7 +217,7 @@ async function generateGoogleImage(input) {
201
217
  },
202
218
  body: JSON.stringify({
203
219
  model: input.model,
204
- input: input.prompt,
220
+ input: input.interactionInput,
205
221
  response_format: {
206
222
  type: "image",
207
223
  mime_type: REQUESTED_MEDIA_TYPE,
@@ -210,33 +226,33 @@ async function generateGoogleImage(input) {
210
226
  }
211
227
  })
212
228
  });
213
- const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
214
- if (!response.ok) throw new Error(`Google image generation failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
229
+ const text = await readBoundedText$2(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$2, label);
230
+ if (!response.ok) throw new Error(`${label} failed (${response.status}): ${text.slice(0, ERROR_LIMIT$2)}`);
215
231
  let payload;
216
232
  try {
217
233
  payload = JSON.parse(text);
218
234
  } catch {
219
- throw new Error("Google image generation returned invalid JSON");
235
+ throw new Error(`${label} returned invalid JSON`);
220
236
  }
221
237
  const image = outputImage(payload);
222
- if (image === void 0) throw new Error(`Google image generation returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
238
+ if (image === void 0) throw new Error(`${label} returned no image: ${text.slice(0, ERROR_LIMIT$2)}`);
223
239
  const mediaType = mediaTypeOf(image.mime_type ?? REQUESTED_MEDIA_TYPE);
224
- if (mediaType === void 0) throw new Error(`Google image generation returned unsupported media type ${JSON.stringify(image.mime_type)}`);
225
- const data = decodeBase64$1(image.data);
226
- if (data.byteLength > input.maxBytes) throw new Error(`Google image generation exceeded the ${String(input.maxBytes)} byte image limit`);
240
+ if (mediaType === void 0) throw new Error(`${label} returned unsupported media type ${JSON.stringify(image.mime_type)}`);
241
+ const data = decodeBase64$2(image.data, label);
242
+ if (data.byteLength > input.maxBytes) throw new Error(`${label} exceeded the ${String(input.maxBytes)} byte image limit`);
227
243
  return {
228
244
  data,
229
245
  mediaType
230
246
  };
231
247
  }
232
248
  function outputImage(value) {
233
- const interaction = record$1(value);
249
+ const interaction = record$2(value);
234
250
  if (interaction === void 0) return void 0;
235
251
  const direct = imageContent(interaction.output_image, false);
236
252
  if (direct !== void 0) return direct;
237
253
  if (!Array.isArray(interaction.steps)) return void 0;
238
254
  for (const step of interaction.steps) {
239
- const modelOutput = record$1(step);
255
+ const modelOutput = record$2(step);
240
256
  if (modelOutput?.type !== "model_output" || !Array.isArray(modelOutput.content)) continue;
241
257
  for (const content of modelOutput.content) {
242
258
  const image = imageContent(content, true);
@@ -245,27 +261,27 @@ function outputImage(value) {
245
261
  }
246
262
  }
247
263
  function imageContent(value, requiresImageType) {
248
- const image = record$1(value);
264
+ const image = record$2(value);
249
265
  if (image === void 0 || requiresImageType && image.type !== "image" || typeof image.data !== "string") return void 0;
250
266
  return {
251
267
  data: image.data,
252
268
  mime_type: image.mime_type
253
269
  };
254
270
  }
255
- function record$1(value) {
271
+ function record$2(value) {
256
272
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
257
273
  }
258
274
  function mediaTypeOf(value) {
259
275
  return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif" ? value : void 0;
260
276
  }
261
- function decodeBase64$1(data) {
277
+ function decodeBase64$2(data, label) {
262
278
  const clean = data.replace(/\s+/g, "");
263
- if (clean.length === 0) throw new Error("Google image generation returned invalid base64 image data");
279
+ if (clean.length === 0) throw new Error(`${label} returned invalid base64 image data`);
264
280
  const decoded = Buffer.from(clean, "base64");
265
- if (decoded.length === 0) throw new Error("Google image generation returned invalid base64 image data");
281
+ if (decoded.length === 0) throw new Error(`${label} returned invalid base64 image data`);
266
282
  return new Uint8Array(decoded);
267
283
  }
268
- async function readBoundedText$1(response, maxBytes) {
284
+ async function readBoundedText$2(response, maxBytes, label) {
269
285
  if (response.body === null) return "";
270
286
  const reader = response.body.getReader();
271
287
  const chunks = [];
@@ -275,7 +291,7 @@ async function readBoundedText$1(response, maxBytes) {
275
291
  const next = await reader.read();
276
292
  if (next.done) break;
277
293
  bytes += next.value.byteLength;
278
- if (bytes > maxBytes) throw new Error(`Google image generation response exceeded the ${String(maxBytes)} byte limit`);
294
+ if (bytes > maxBytes) throw new Error(`${label} response exceeded the ${String(maxBytes)} byte limit`);
279
295
  chunks.push(next.value);
280
296
  }
281
297
  } finally {
@@ -290,6 +306,220 @@ async function readBoundedText$1(response, maxBytes) {
290
306
  return new TextDecoder().decode(joined);
291
307
  }
292
308
  //#endregion
309
+ //#region lib/types/reference-image.js
310
+ /** Latest-DSH compatibility boundary for resolving conversation image references. */
311
+ /**
312
+ * Resolve one or more edit references while keeping every DSH-specific detail
313
+ * behind this compatibility boundary. Explicit selectors preserve caller order;
314
+ * without selectors, all images in the newest image-bearing message are used.
315
+ */
316
+ async function resolveReferenceImages(input) {
317
+ const sourceAttachmentIds = mergeSelectors({
318
+ single: input.sourceAttachmentId,
319
+ multiple: input.sourceAttachmentIds,
320
+ singleName: "source_attachment_id",
321
+ multipleName: "source_attachment_ids",
322
+ equal: attachmentIdsEqual
323
+ });
324
+ const sourcePaths = mergeSelectors({
325
+ single: input.sourcePath,
326
+ multiple: input.sourcePaths,
327
+ singleName: "source_path",
328
+ multipleName: "source_paths",
329
+ equal: (left, right) => left.trim() === right.trim()
330
+ });
331
+ if (sourceAttachmentIds !== void 0 && sourcePaths !== void 0) throw new Error("edit_image accepts only one of source_attachment_id, source_attachment_ids, source_path, or source_paths");
332
+ if (sourcePaths !== void 0) return Promise.all(sourcePaths.map((sourcePath) => readWorkspaceReferenceImage({
333
+ sourcePath,
334
+ ...input.agent?.session.header?.cwd === void 0 ? {} : { workspaceRoot: input.agent.session.header.cwd },
335
+ ...input.maxBytes === void 0 ? {} : { maxBytes: input.maxBytes },
336
+ signal: input.signal
337
+ })));
338
+ if (input.agent === void 0) throw new Error("edit_image requires an active DSH agent session to resolve a reference image");
339
+ const refs = findReferenceImages(input.agent.session.deriveMessages(), sourceAttachmentIds);
340
+ if (refs.length === 0) {
341
+ if (sourceAttachmentIds !== void 0) throw new Error(`edit_image could not find image attachment ${sourceAttachmentIds[0]} in the current conversation`);
342
+ throw new Error("edit_image requires an image in the current conversation; upload or generate an image first");
343
+ }
344
+ if (sourceAttachmentIds !== void 0 && refs.length !== sourceAttachmentIds.length) {
345
+ const missing = sourceAttachmentIds.find((id) => !refs.some((ref) => attachmentIdsEqual(String(ref.attachmentId), id)));
346
+ throw new Error(`edit_image could not find image attachment ${missing ?? "unknown"} in the current conversation`);
347
+ }
348
+ return Promise.all(refs.map(async (ref) => {
349
+ const stored = await input.attachments.readImage(ref, input.signal);
350
+ if (input.maxBytes !== void 0 && stored.data.byteLength > input.maxBytes) throw new Error(`edit_image source image is too large (${stored.data.byteLength} bytes; maximum ${input.maxBytes})`);
351
+ return {
352
+ data: stored.data,
353
+ mediaType: stored.ref.mediaType
354
+ };
355
+ }));
356
+ }
357
+ /** Find images in caller order, or every image in the newest image-bearing message. */
358
+ function findReferenceImages(messages, sourceAttachmentIds) {
359
+ if (sourceAttachmentIds !== void 0) return sourceAttachmentIds.flatMap((id) => {
360
+ const ref = findReferenceImage(messages, id);
361
+ return ref === void 0 ? [] : [ref];
362
+ });
363
+ const latestHumanMessage = [...messages].reverse().find((message) => message.source?.kind === "user");
364
+ if (latestHumanMessage !== void 0) {
365
+ const refs = collectInBlocks(latestHumanMessage.content);
366
+ if (refs.length > 0) return refs;
367
+ }
368
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
369
+ const refs = collectInBlocks(messages[index]?.content ?? []);
370
+ if (refs.length > 0) return refs;
371
+ }
372
+ return [];
373
+ }
374
+ /**
375
+ * Read an explicitly named workspace image without exposing filesystem or DSH
376
+ * details to provider adapters. Both lexical and real-path containment are
377
+ * enforced so absolute paths, parent traversal, and symlink escapes fail.
378
+ */
379
+ async function readWorkspaceReferenceImage(input) {
380
+ const requested = input.sourcePath.trim();
381
+ if (requested.length === 0) throw new Error("edit_image source_path must not be empty");
382
+ if (input.workspaceRoot === void 0) throw new Error("edit_image source_path requires an active DSH session workspace");
383
+ const root = resolve(input.workspaceRoot);
384
+ const candidate = isAbsolute(requested) ? resolve(requested) : resolve(root, requested);
385
+ if (!containsPath$1(root, candidate)) throw new Error("edit_image source_path must stay inside the session workspace: " + requested);
386
+ let realRoot;
387
+ let realCandidate;
388
+ try {
389
+ [realRoot, realCandidate] = await Promise.all([realpath(root), realpath(candidate)]);
390
+ } catch (error) {
391
+ if (error.code === "ENOENT") throw new Error("edit_image could not find workspace image: " + requested);
392
+ throw error;
393
+ }
394
+ if (!containsPath$1(realRoot, realCandidate)) throw new Error("edit_image source_path resolves outside the session workspace: " + requested);
395
+ const file = await stat(realCandidate);
396
+ if (!file.isFile()) throw new Error("edit_image source_path is not a file: " + requested);
397
+ if (input.maxBytes !== void 0 && file.size > input.maxBytes) throw new Error("edit_image source image is too large (" + file.size + " bytes; maximum " + input.maxBytes + ")");
398
+ const data = await readFile(realCandidate, { signal: input.signal });
399
+ const mediaType = detectImageMediaType(data);
400
+ if (mediaType === void 0) throw new Error("edit_image source_path is not a supported PNG, JPEG, WebP, or GIF image: " + requested);
401
+ return {
402
+ data: new Uint8Array(data),
403
+ mediaType
404
+ };
405
+ }
406
+ /** Find the newest matching image in the effective, replacement-aware history. */
407
+ function findReferenceImage(messages, sourceAttachmentId) {
408
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
409
+ const found = findInBlocks(messages[index]?.content ?? [], sourceAttachmentId);
410
+ if (found !== void 0) return found;
411
+ }
412
+ }
413
+ /** Validate an untrusted serialized image reference at an HTTP/UI boundary. */
414
+ function parseImageAttachmentRef(value) {
415
+ const ref = record$1(value);
416
+ if (ref === void 0) return void 0;
417
+ if (typeof ref.attachmentId !== "string" || !imageMediaType$2(ref.mediaType)) return void 0;
418
+ if (!nonNegativeInteger(ref.bytes) || !positiveInteger(ref.width) || !positiveInteger(ref.height)) return void 0;
419
+ if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
420
+ const originalDimensions = ref.originalDimensions === void 0 ? void 0 : parseDimensions(ref.originalDimensions);
421
+ if (ref.originalDimensions !== void 0 && originalDimensions === void 0) return void 0;
422
+ return {
423
+ attachmentId: ref.attachmentId,
424
+ mediaType: ref.mediaType,
425
+ bytes: ref.bytes,
426
+ width: ref.width,
427
+ height: ref.height,
428
+ ...typeof ref.name === "string" ? { name: ref.name } : {},
429
+ ...originalDimensions === void 0 ? {} : { originalDimensions }
430
+ };
431
+ }
432
+ function findInBlocks(blocks, sourceAttachmentId) {
433
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
434
+ const block = blocks[index];
435
+ if (block === void 0) continue;
436
+ if (block.type === "image") {
437
+ if (sourceAttachmentId === void 0 || attachmentIdsEqual(String(block.attachment.attachmentId), sourceAttachmentId)) return block.attachment;
438
+ continue;
439
+ }
440
+ if (block.type === "tool-result") {
441
+ const nested = findInBlocks(block.content, sourceAttachmentId);
442
+ if (nested !== void 0) return nested;
443
+ }
444
+ }
445
+ }
446
+ function parseDimensions(value) {
447
+ const dimensions = record$1(value);
448
+ if (dimensions === void 0 || !positiveInteger(dimensions.width) || !positiveInteger(dimensions.height)) return void 0;
449
+ return {
450
+ width: dimensions.width,
451
+ height: dimensions.height
452
+ };
453
+ }
454
+ function imageMediaType$2(value) {
455
+ return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
456
+ }
457
+ function collectInBlocks(blocks) {
458
+ const refs = [];
459
+ for (const block of blocks) {
460
+ if (block.type === "image") refs.push(block.attachment);
461
+ if (block.type === "tool-result") refs.push(...collectInBlocks(block.content));
462
+ }
463
+ return refs;
464
+ }
465
+ function attachmentIdsEqual(actual, requested) {
466
+ if (actual === requested) return true;
467
+ const actualDigest = sha256Digest(actual);
468
+ const requestedDigest = sha256Digest(requested);
469
+ return actualDigest !== void 0 && actualDigest === requestedDigest;
470
+ }
471
+ function sha256Digest(value) {
472
+ return /^(?:sha256:)?([0-9a-f]{64})$/i.exec(value.trim())?.[1]?.toLowerCase();
473
+ }
474
+ function mergeSelectors(input) {
475
+ if (input.multiple === void 0) return input.single === void 0 ? void 0 : [input.single];
476
+ if (input.multiple.length === 0) {
477
+ if (input.single !== void 0) return [input.single];
478
+ throw new Error(`edit_image ${input.multipleName} must not be empty`);
479
+ }
480
+ const single = input.single;
481
+ if (single !== void 0 && !input.multiple.some((value) => input.equal(single, value))) throw new Error(`edit_image ${input.singleName} must also appear in ${input.multipleName} when both are provided`);
482
+ return input.multiple;
483
+ }
484
+ function detectImageMediaType(data) {
485
+ if (startsWith(data, [
486
+ 137,
487
+ 80,
488
+ 78,
489
+ 71,
490
+ 13,
491
+ 10,
492
+ 26,
493
+ 10
494
+ ])) return "image/png";
495
+ if (startsWith(data, [
496
+ 255,
497
+ 216,
498
+ 255
499
+ ])) return "image/jpeg";
500
+ if (ascii(data, 0, 6) === "GIF87a" || ascii(data, 0, 6) === "GIF89a") return "image/gif";
501
+ if (ascii(data, 0, 4) === "RIFF" && ascii(data, 8, 4) === "WEBP") return "image/webp";
502
+ }
503
+ function startsWith(data, signature) {
504
+ return signature.every((byte, index) => data[index] === byte);
505
+ }
506
+ function ascii(data, offset, length) {
507
+ return String.fromCharCode(...data.subarray(offset, offset + length));
508
+ }
509
+ function containsPath$1(parent, child) {
510
+ const rel = relative(parent, child);
511
+ return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && !isAbsolute(rel);
512
+ }
513
+ function positiveInteger(value) {
514
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
515
+ }
516
+ function nonNegativeInteger(value) {
517
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
518
+ }
519
+ function record$1(value) {
520
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
521
+ }
522
+ //#endregion
293
523
  //#region lib/types/image-route.js
294
524
  const MAX_BODY_BYTES = 4096;
295
525
  /** Serve one verified durable image reference to a same-origin browser request. */
@@ -324,20 +554,10 @@ async function serveImage(req, res, deps) {
324
554
  function imageAttachmentFromMeta(meta) {
325
555
  const value = record(meta);
326
556
  if (value?.kind !== "dsh-image-gen") return void 0;
327
- return imageAttachment(value.attachment);
557
+ return parseImageAttachmentRef(value.attachment);
328
558
  }
329
559
  function attachmentFromRequest(value) {
330
- return imageAttachment(record(value)?.attachment);
331
- }
332
- function imageAttachment(value) {
333
- const ref = record(value);
334
- if (ref === void 0) return void 0;
335
- if (typeof ref.attachmentId !== "string" || !mediaType(ref.mediaType) || typeof ref.bytes !== "number" || typeof ref.width !== "number" || typeof ref.height !== "number") return void 0;
336
- if (ref.name !== void 0 && typeof ref.name !== "string") return void 0;
337
- return ref;
338
- }
339
- function mediaType(value) {
340
- return value === "image/png" || value === "image/jpeg" || value === "image/webp" || value === "image/gif";
560
+ return parseImageAttachmentRef(record(value)?.attachment);
341
561
  }
342
562
  function record(value) {
343
563
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
@@ -362,10 +582,9 @@ function jsonError(res, status, code) {
362
582
  }
363
583
  //#endregion
364
584
  //#region lib/types/openai-compatible.js
365
- const ERROR_LIMIT = 4096;
366
- /** Generate an image from an OpenAI-compatible endpoint. */
585
+ const ERROR_LIMIT$1 = 4096;
367
586
  async function generateOpenAICompatibleImage(input) {
368
- const response = await fetch(imageEndpoint(input.baseURL), {
587
+ return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "generations"), {
369
588
  method: "POST",
370
589
  redirect: "error",
371
590
  signal: input.signal,
@@ -379,20 +598,177 @@ async function generateOpenAICompatibleImage(input) {
379
598
  size: input.size,
380
599
  ...input.provider === "seedream" ? { response_format: "url" } : {}
381
600
  })
601
+ }), input.provider, input);
602
+ }
603
+ async function editOpenAICompatibleImage(input) {
604
+ const form = new FormData();
605
+ const imageField = input.sourceImages.length === 1 ? "image" : "image[]";
606
+ input.sourceImages.forEach((sourceImage, index) => {
607
+ const uploadBytes = new Uint8Array(sourceImage.data);
608
+ form.append(imageField, new Blob([uploadBytes], { type: sourceImage.mediaType }), `reference-${index + 1}.${extensionOf(sourceImage.mediaType)}`);
609
+ });
610
+ form.append("prompt", input.prompt);
611
+ form.append("model", input.model);
612
+ if (input.size !== void 0 && input.size.length > 0) form.append("size", input.size);
613
+ return parseImageResponse(await fetch(imageEndpoint$1(input.baseURL, "edits"), {
614
+ method: "POST",
615
+ redirect: "error",
616
+ signal: input.signal,
617
+ headers: { authorization: `Bearer ${input.apiKey}` },
618
+ body: form
619
+ }), "openai", input);
620
+ }
621
+ async function parseImageResponse(response, provider, input) {
622
+ const text = await readBoundedText$1(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT$1);
623
+ if (!response.ok) throw new Error(`${provider} image request failed (${response.status}): ${text.slice(0, ERROR_LIMIT$1)}`);
624
+ let payload;
625
+ try {
626
+ payload = JSON.parse(text);
627
+ } catch {
628
+ throw new Error(`${provider} image request returned invalid JSON`);
629
+ }
630
+ const image = firstImage$1(payload);
631
+ if (image === void 0) throw new Error(`${provider} image request returned no image: ${text.slice(0, ERROR_LIMIT$1)}`);
632
+ if (image.b64_json !== void 0) return {
633
+ data: decodeBase64$1(image.b64_json, provider),
634
+ mediaType: imageMediaType$1(image.mime_type) ?? "image/png"
635
+ };
636
+ return downloadImage$1(image.url, provider, input);
637
+ }
638
+ function imageEndpoint$1(baseURL, operation) {
639
+ try {
640
+ return new URL(`images/${operation}`, baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
641
+ } catch {
642
+ throw new Error("Image endpoint must be an absolute URL");
643
+ }
644
+ }
645
+ function firstImage$1(value) {
646
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
647
+ const record = value;
648
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
649
+ if (data === void 0 || data.length === 0) return void 0;
650
+ const candidate = data[0];
651
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
652
+ const item = candidate;
653
+ const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
654
+ return typeof item.b64_json === "string" ? {
655
+ b64_json: item.b64_json,
656
+ ...mime === void 0 ? {} : { mime_type: mime }
657
+ } : typeof item.url === "string" ? {
658
+ url: item.url,
659
+ ...mime === void 0 ? {} : { mime_type: mime }
660
+ } : void 0;
661
+ }
662
+ async function downloadImage$1(url, provider, input) {
663
+ if (url === void 0) throw new Error(`${provider} image request returned no image data`);
664
+ if (url.startsWith("data:")) {
665
+ const parsed = parseDataUrl(url);
666
+ if (parsed === void 0) throw new Error(`${provider} image request returned invalid data URL`);
667
+ return {
668
+ data: decodeBase64$1(parsed.base64, provider),
669
+ mediaType: imageMediaType$1(parsed.mediaType) ?? "image/png"
670
+ };
671
+ }
672
+ const response = await fetch(url, {
673
+ redirect: "follow",
674
+ signal: input.signal,
675
+ ...input.apiKey === void 0 ? {} : { headers: { authorization: `Bearer ${input.apiKey}` } }
676
+ });
677
+ if (!response.ok) throw new Error(`${provider} image download failed (${response.status})`);
678
+ const mediaType = imageMediaType$1(response.headers.get("content-type"));
679
+ if (mediaType === void 0) throw new Error(`${provider} image download returned unsupported content type`);
680
+ return {
681
+ data: await readBoundedBytes$1(response, input.maxBytes),
682
+ mediaType
683
+ };
684
+ }
685
+ function parseDataUrl(value) {
686
+ const match = /^data:([^;,]+);base64,(.*)$/s.exec(value.trim());
687
+ return match?.[1] !== void 0 && match[2] !== void 0 ? {
688
+ mediaType: match[1],
689
+ base64: match[2]
690
+ } : void 0;
691
+ }
692
+ function extensionOf(mediaType) {
693
+ switch (mediaType) {
694
+ case "image/jpeg": return "jpg";
695
+ case "image/webp": return "webp";
696
+ case "image/gif": return "gif";
697
+ case "image/png": return "png";
698
+ }
699
+ }
700
+ function decodeBase64$1(value, provider) {
701
+ const clean = (parseDataUrl(value)?.base64 ?? value).replace(/\s+/g, "");
702
+ if (clean.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
703
+ const decoded = Buffer.from(clean, "base64");
704
+ if (decoded.length === 0) throw new Error(`${provider} image request returned invalid base64 image data`);
705
+ return new Uint8Array(decoded);
706
+ }
707
+ function imageMediaType$1(value) {
708
+ const mediaType = value?.split(";", 1)[0]?.trim().toLowerCase();
709
+ return mediaType === "image/png" || mediaType === "image/jpeg" || mediaType === "image/webp" || mediaType === "image/gif" ? mediaType : void 0;
710
+ }
711
+ async function readBoundedText$1(response, maxBytes) {
712
+ return new TextDecoder().decode(await readBoundedBytes$1(response, maxBytes));
713
+ }
714
+ async function readBoundedBytes$1(response, maxBytes) {
715
+ if (response.body === null) return /* @__PURE__ */ new Uint8Array();
716
+ const reader = response.body.getReader();
717
+ const chunks = [];
718
+ let bytes = 0;
719
+ try {
720
+ for (;;) {
721
+ const next = await reader.read();
722
+ if (next.done) break;
723
+ bytes += next.value.byteLength;
724
+ if (bytes > maxBytes) throw new Error(`Image response exceeded the ${String(maxBytes)} byte limit`);
725
+ chunks.push(next.value);
726
+ }
727
+ } finally {
728
+ reader.releaseLock();
729
+ }
730
+ const joined = new Uint8Array(bytes);
731
+ let offset = 0;
732
+ for (const chunk of chunks) {
733
+ joined.set(chunk, offset);
734
+ offset += chunk.byteLength;
735
+ }
736
+ return joined;
737
+ }
738
+ //#endregion
739
+ //#region lib/types/seedream.js
740
+ const ERROR_LIMIT = 4096;
741
+ /** Edit one image through Ark ImageGenerations using a data-URL reference. */
742
+ async function editSeedreamImage(input) {
743
+ const response = await fetch(imageEndpoint(input.baseURL), {
744
+ method: "POST",
745
+ redirect: "error",
746
+ signal: input.signal,
747
+ headers: {
748
+ authorization: `Bearer ${input.apiKey}`,
749
+ "content-type": "application/json"
750
+ },
751
+ body: JSON.stringify({
752
+ model: input.model,
753
+ prompt: input.prompt,
754
+ image: input.sourceImages.map(toDataUrl),
755
+ ...input.size === void 0 || input.size.length === 0 ? {} : { size: input.size },
756
+ response_format: "b64_json"
757
+ })
382
758
  });
383
759
  const text = await readBoundedText(response, Math.ceil(input.maxBytes * 1.4) + ERROR_LIMIT);
384
- if (!response.ok) throw new Error(`${input.provider} image generation failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
760
+ if (!response.ok) throw new Error(`seedream image editing failed (${response.status}): ${text.slice(0, ERROR_LIMIT)}`);
385
761
  let payload;
386
762
  try {
387
763
  payload = JSON.parse(text);
388
764
  } catch {
389
- throw new Error(`${input.provider} image generation returned invalid JSON`);
765
+ throw new Error("seedream image editing returned invalid JSON");
390
766
  }
391
767
  const image = firstImage(payload);
392
- if (image === void 0) throw new Error(`${input.provider} image generation returned no image: ${text.slice(0, ERROR_LIMIT)}`);
768
+ if (image === void 0) throw new Error(`seedream image editing returned no image: ${text.slice(0, ERROR_LIMIT)}`);
393
769
  if (image.b64_json !== void 0) return {
394
- data: decodeBase64(image.b64_json, input.provider),
395
- mediaType: "image/png"
770
+ data: decodeBase64(image.b64_json),
771
+ mediaType: imageMediaType(image.mime_type) ?? "image/png"
396
772
  };
397
773
  return downloadImage(image.url, input);
398
774
  }
@@ -400,37 +776,48 @@ function imageEndpoint(baseURL) {
400
776
  try {
401
777
  return new URL("images/generations", baseURL.endsWith("/") ? baseURL : `${baseURL}/`).toString();
402
778
  } catch {
403
- throw new Error("Image generation endpoint must be an absolute URL");
779
+ throw new Error("Seedream image endpoint must be an absolute URL");
404
780
  }
405
781
  }
782
+ function toDataUrl(image) {
783
+ return `data:${image.mediaType};base64,${Buffer.from(image.data).toString("base64")}`;
784
+ }
406
785
  function firstImage(value) {
407
786
  if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
408
- const data = value.data;
409
- if (!Array.isArray(data) || data.length === 0) return void 0;
787
+ const record = value;
788
+ const data = Array.isArray(record.data) ? record.data : Array.isArray(record.images) ? record.images : Array.isArray(record.output) ? record.output : void 0;
789
+ if (data === void 0 || data.length === 0) return void 0;
410
790
  const candidate = data[0];
411
791
  if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) return void 0;
412
792
  const item = candidate;
413
- return typeof item.b64_json === "string" ? { b64_json: item.b64_json } : typeof item.url === "string" ? { url: item.url } : void 0;
793
+ const mime = typeof item.mime_type === "string" ? item.mime_type : typeof item.mime === "string" ? item.mime : void 0;
794
+ return typeof item.b64_json === "string" ? {
795
+ b64_json: item.b64_json,
796
+ ...mime === void 0 ? {} : { mime_type: mime }
797
+ } : typeof item.url === "string" ? {
798
+ url: item.url,
799
+ ...mime === void 0 ? {} : { mime_type: mime }
800
+ } : void 0;
414
801
  }
415
802
  async function downloadImage(url, input) {
416
- if (url === void 0) throw new Error(`${input.provider} image generation returned no image data`);
803
+ if (url === void 0) throw new Error("seedream image editing returned no image data");
417
804
  const response = await fetch(url, {
418
805
  redirect: "follow",
419
806
  signal: input.signal
420
807
  });
421
- if (!response.ok) throw new Error(`${input.provider} image download failed (${response.status})`);
808
+ if (!response.ok) throw new Error(`seedream image download failed (${response.status})`);
422
809
  const mediaType = imageMediaType(response.headers.get("content-type"));
423
- if (mediaType === void 0) throw new Error(`${input.provider} image download returned unsupported content type`);
810
+ if (mediaType === void 0) throw new Error("seedream image download returned unsupported content type");
424
811
  return {
425
812
  data: await readBoundedBytes(response, input.maxBytes),
426
813
  mediaType
427
814
  };
428
815
  }
429
- function decodeBase64(value, provider) {
816
+ function decodeBase64(value) {
430
817
  const clean = value.replace(/\s+/g, "");
431
- if (clean.length === 0) throw new Error(`${provider} image generation returned invalid base64 image data`);
818
+ if (clean.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
432
819
  const decoded = Buffer.from(clean, "base64");
433
- if (decoded.length === 0) throw new Error(`${provider} image generation returned invalid base64 image data`);
820
+ if (decoded.length === 0) throw new Error("seedream image editing returned invalid base64 image data");
434
821
  return new Uint8Array(decoded);
435
822
  }
436
823
  function imageMediaType(value) {
@@ -582,16 +969,13 @@ async function saveImageToWorkspace(options) {
582
969
  }
583
970
  //#endregion
584
971
  //#region lib/types/index.js
585
- /** Cordis plugin name. */
586
972
  const name = "dsh-image-gen";
587
- /** Host services required by the Bundle. */
588
973
  const inject = [
589
974
  "tools",
590
975
  "attachments",
591
976
  "credentials",
592
977
  "webServer"
593
978
  ];
594
- /** Register settings, the image route, and the model-callable tool. */
595
979
  function apply(ctx, config = {}) {
596
980
  let current = () => config;
597
981
  installSettingsSection(ctx, settingsNamespace(IMAGE_GENERATION_NAMESPACE), Config, config, {
@@ -607,7 +991,7 @@ function apply(ctx, config = {}) {
607
991
  }), "dsh-image-gen: image route");
608
992
  ctx.tools.register(defineTool({
609
993
  name: "generate_image",
610
- description: "Generate one image with the configured image provider. Use when the user explicitly asks to create or draw an image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is already attached directly to the conversation; with workspace saving enabled (the default) it is also written as a file under the session workspace, and the result's savedTo field carries that absolute file path. Do not call read, glob, or other tools to locate or verify the image.",
994
+ description: "Generate a new image with the configured provider. Use when the user asks to create or draw a new image; use edit_image instead when they want to change an existing image. Give a complete visual prompt including subject, composition, style, lighting, and any exact text that should appear. A successful image is attached directly to the conversation and may also be saved under the session workspace. Do not call read, glob, or other tools to locate or verify the image.",
611
995
  parameters: {
612
996
  prompt: {
613
997
  type: "string",
@@ -641,72 +1025,7 @@ function apply(ctx, config = {}) {
641
1025
  description: "Optional dimensions or size tier for OpenAI, Seedream, or DashScope."
642
1026
  }
643
1027
  },
644
- output: {
645
- schema: {
646
- type: "object",
647
- additionalProperties: false,
648
- properties: {
649
- attachment: {
650
- type: "object",
651
- required: true,
652
- additionalProperties: false,
653
- properties: {
654
- attachmentId: {
655
- type: "string",
656
- required: true
657
- },
658
- mediaType: {
659
- type: "string",
660
- required: true
661
- },
662
- bytes: {
663
- type: "integer",
664
- required: true
665
- },
666
- width: {
667
- type: "integer",
668
- required: true
669
- },
670
- height: {
671
- type: "integer",
672
- required: true
673
- },
674
- name: { type: "string" }
675
- }
676
- },
677
- provider: {
678
- type: "string",
679
- required: true
680
- },
681
- model: {
682
- type: "string",
683
- required: true
684
- },
685
- output: {
686
- type: "string",
687
- required: true
688
- },
689
- savedTo: { type: "string" },
690
- saveError: { type: "string" }
691
- }
692
- },
693
- render: (_args, value) => {
694
- const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
695
- return [{
696
- type: "text",
697
- text: `Generated one image with ${value.provider}/${value.model} (${value.output}). It is already attached to the conversation.${saved} Respond to the user without reading or searching for the image.`
698
- }];
699
- },
700
- presentationMeta: (args, value) => ({
701
- kind: "dsh-image-gen",
702
- attachment: value.attachment,
703
- provider: value.provider,
704
- model: value.model,
705
- output: value.output,
706
- ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
707
- prompt: args.prompt
708
- })
709
- },
1028
+ output: imageOutput("Generated"),
710
1029
  async execute(args, exec) {
711
1030
  const active = resolveProvider(current());
712
1031
  const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
@@ -751,13 +1070,226 @@ function apply(ctx, config = {}) {
751
1070
  },
752
1071
  presentResult: (_args, result) => imagePresentation(result)
753
1072
  }));
1073
+ ctx.tools.register(defineTool({
1074
+ name: "edit_image",
1075
+ description: "Edit, combine, or restyle existing images with the configured provider. Images attached inline to the latest human message are already readable DSH attachments even when no workspace file exists. In that case, call edit_image immediately with prompt only; NEVER call read_image, glob, or shell to locate them, and NEVER invent @ paths. All inline images will be used in upload order. For specific older conversation images use source_attachment_id or source_attachment_ids; both canonical sha256: IDs and full bare SHA-256 digests are accepted. For files the user explicitly names in the workspace use source_path or source_paths. Provide exactly one selector field. Without a selector, images from the latest human message take priority; only when that message has no images does editing fall back to the newest conversation image.",
1076
+ parameters: {
1077
+ prompt: {
1078
+ type: "string",
1079
+ required: true,
1080
+ description: "Describe the changes to make while preserving everything else that should remain."
1081
+ },
1082
+ source_attachment_id: {
1083
+ type: "string",
1084
+ description: "Optional attachment id of a specific image already present in the current conversation."
1085
+ },
1086
+ source_attachment_ids: {
1087
+ type: "array",
1088
+ items: { type: "string" },
1089
+ description: "Optional ordered attachment ids of multiple images already present in the current conversation. Prompt references such as image 1 and image 2 follow this order."
1090
+ },
1091
+ source_path: {
1092
+ type: "string",
1093
+ description: "Optional absolute or workspace-relative path of a specific image file inside the active session workspace. Prefer this when the user names a saved file."
1094
+ },
1095
+ source_paths: {
1096
+ type: "array",
1097
+ items: { type: "string" },
1098
+ description: "Optional ordered absolute or workspace-relative paths of multiple image files inside the active session workspace."
1099
+ },
1100
+ aspect_ratio: {
1101
+ type: "string",
1102
+ enum: [
1103
+ "1:1",
1104
+ "3:2",
1105
+ "2:3",
1106
+ "4:3",
1107
+ "3:4",
1108
+ "16:9",
1109
+ "9:16"
1110
+ ],
1111
+ description: "Optional output aspect ratio for Google Gemini."
1112
+ },
1113
+ image_size: {
1114
+ type: "string",
1115
+ enum: [
1116
+ "1K",
1117
+ "2K",
1118
+ "4K"
1119
+ ],
1120
+ description: "Optional output resolution for Google Gemini."
1121
+ },
1122
+ size: {
1123
+ type: "string",
1124
+ description: "Optional output size for OpenAI, Seedream, or DashScope."
1125
+ }
1126
+ },
1127
+ output: imageOutput("Edited"),
1128
+ async execute(args, exec) {
1129
+ const active = resolveProvider(current());
1130
+ const credential = await ctx.credentials.resolve(credentialRef(active.apiKeyEnv));
1131
+ if (credential === void 0 || credential.value.length === 0) throw new Error(`edit_image requires the ${active.apiKeyEnv} credential; configure it in Settings > Plugins > Image generation.`);
1132
+ const sourceImages = await resolveReferenceImages({
1133
+ ...exec.agent === void 0 ? {} : { agent: exec.agent },
1134
+ attachments: ctx.attachments,
1135
+ ...typeof args.source_attachment_id === "string" ? { sourceAttachmentId: args.source_attachment_id } : {},
1136
+ ...Array.isArray(args.source_attachment_ids) ? { sourceAttachmentIds: args.source_attachment_ids } : {},
1137
+ ...typeof args.source_path === "string" ? { sourcePath: args.source_path } : {},
1138
+ ...Array.isArray(args.source_paths) ? { sourcePaths: args.source_paths } : {},
1139
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1140
+ signal: exec.signal
1141
+ });
1142
+ if (active.provider === "google") {
1143
+ const aspectRatio = args.aspect_ratio ?? active.aspectRatio;
1144
+ const imageSize = args.image_size ?? active.imageSize;
1145
+ return saveGenerated(ctx, await editGoogleImage({
1146
+ apiKey: credential.value,
1147
+ endpoint: active.endpoint,
1148
+ model: active.model,
1149
+ prompt: args.prompt,
1150
+ sourceImages,
1151
+ aspectRatio,
1152
+ imageSize,
1153
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1154
+ signal: exec.signal
1155
+ }), active.provider, active.model, `${aspectRatio}, ${imageSize}`, current(), exec);
1156
+ }
1157
+ const size = args.size ?? active.imageSize;
1158
+ if (active.provider === "openai") return saveGenerated(ctx, await editOpenAICompatibleImage({
1159
+ apiKey: credential.value,
1160
+ baseURL: active.baseURL,
1161
+ model: active.model,
1162
+ prompt: args.prompt,
1163
+ sourceImages,
1164
+ size,
1165
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1166
+ signal: exec.signal
1167
+ }), active.provider, active.model, size, current(), exec);
1168
+ if (active.provider === "seedream") return saveGenerated(ctx, await editSeedreamImage({
1169
+ apiKey: credential.value,
1170
+ baseURL: active.baseURL,
1171
+ model: active.model,
1172
+ prompt: args.prompt,
1173
+ sourceImages,
1174
+ size,
1175
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1176
+ signal: exec.signal
1177
+ }), active.provider, active.model, size, current(), exec);
1178
+ return saveGenerated(ctx, await editDashScopeImage({
1179
+ apiKey: credential.value,
1180
+ endpoint: active.endpoint,
1181
+ model: active.model,
1182
+ prompt: args.prompt,
1183
+ sourceImages,
1184
+ size,
1185
+ maxBytes: ctx.attachments.imageLimits.maxImageBytes,
1186
+ signal: exec.signal
1187
+ }), active.provider, active.model, size, current(), exec);
1188
+ },
1189
+ presentResult: (_args, result) => imagePresentation(result)
1190
+ }));
1191
+ }
1192
+ function imageOutput(verb) {
1193
+ return {
1194
+ schema: {
1195
+ type: "object",
1196
+ additionalProperties: false,
1197
+ properties: {
1198
+ attachment: {
1199
+ type: "object",
1200
+ required: true,
1201
+ additionalProperties: false,
1202
+ properties: {
1203
+ attachmentId: {
1204
+ type: "string",
1205
+ required: true
1206
+ },
1207
+ mediaType: {
1208
+ type: "string",
1209
+ required: true
1210
+ },
1211
+ bytes: {
1212
+ type: "integer",
1213
+ required: true
1214
+ },
1215
+ width: {
1216
+ type: "integer",
1217
+ required: true
1218
+ },
1219
+ height: {
1220
+ type: "integer",
1221
+ required: true
1222
+ },
1223
+ name: { type: "string" },
1224
+ originalDimensions: {
1225
+ type: "object",
1226
+ additionalProperties: false,
1227
+ properties: {
1228
+ width: {
1229
+ type: "integer",
1230
+ required: true
1231
+ },
1232
+ height: {
1233
+ type: "integer",
1234
+ required: true
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ },
1240
+ provider: {
1241
+ type: "string",
1242
+ required: true
1243
+ },
1244
+ model: {
1245
+ type: "string",
1246
+ required: true
1247
+ },
1248
+ output: {
1249
+ type: "string",
1250
+ required: true
1251
+ },
1252
+ savedTo: { type: "string" },
1253
+ saveError: { type: "string" }
1254
+ }
1255
+ },
1256
+ render: (_args, value) => {
1257
+ const saved = typeof value.savedTo === "string" ? ` It was also saved to the workspace as ${value.savedTo}.` : typeof value.saveError === "string" ? ` Saving it to the workspace failed: ${value.saveError}.` : " It has no local file path.";
1258
+ const action = verb === "Generated" ? "It is already attached to the conversation." : "The edited image is attached to the conversation.";
1259
+ return [{
1260
+ type: "text",
1261
+ text: `${verb} one image with ${value.provider}/${value.model} (${value.output}). Attachment ID: ${String(value.attachment.attachmentId)}. ${action}${saved} Respond to the user without reading or searching for the image.`
1262
+ }, {
1263
+ type: "image",
1264
+ attachment: value.attachment
1265
+ }];
1266
+ },
1267
+ presentationMeta: (args, value) => ({
1268
+ kind: "dsh-image-gen",
1269
+ attachment: attachmentMeta(value.attachment),
1270
+ provider: value.provider,
1271
+ model: value.model,
1272
+ output: value.output,
1273
+ ...verb === "Edited" ? { operation: "edit" } : {},
1274
+ ...typeof value.savedTo === "string" ? { savedTo: value.savedTo } : {},
1275
+ prompt: args.prompt
1276
+ })
1277
+ };
1278
+ }
1279
+ function attachmentMeta(ref) {
1280
+ return {
1281
+ attachmentId: String(ref.attachmentId),
1282
+ mediaType: ref.mediaType,
1283
+ bytes: ref.bytes,
1284
+ width: ref.width,
1285
+ height: ref.height,
1286
+ ...ref.name === void 0 ? {} : { name: ref.name },
1287
+ ...ref.originalDimensions === void 0 ? {} : { originalDimensions: {
1288
+ width: ref.originalDimensions.width,
1289
+ height: ref.originalDimensions.height
1290
+ } }
1291
+ };
754
1292
  }
755
- /**
756
- * Persist the generated image as a durable attachment, then — when workspace
757
- * saving is enabled — also write it as a file under the calling agent's
758
- * session workspace. A workspace write failure never discards the generated
759
- * attachment: it is reported through `saveError` instead.
760
- */
761
1293
  async function saveGenerated(ctx, generated, provider, model, output, config, exec) {
762
1294
  if (!ctx.attachments.imageLimits.mediaTypes.includes(generated.mediaType)) throw new Error(`This DSH deployment does not accept ${generated.mediaType} generated images`);
763
1295
  const attachment = await ctx.attachments.saveImage({