@goodandready/dsh-image-gen 0.11.17 → 0.11.19
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/frontend-assets.js +14 -0
- package/lib/generation-helpers.js +250 -0
- package/lib/provider-utils.js +4 -2
- package/lib/resolve-image.js +14 -4
- package/lib/resolve-source.js +13 -3
- package/lib/tools/anchor.js +5 -0
- package/lib/tools/character-sheet.js +1 -0
- package/lib/tools/diagram.js +1 -0
- package/lib/tools/generation-pack.js +1 -0
- package/lib/tools/generation.js +496 -651
- package/lib/tools/inspect.js +20 -11
- package/lib/tools/style-matrix.js +1 -0
- package/lib/tools/theme-pair.js +1 -0
- package/lib/tools/ui-asset.js +1 -0
- package/lib/tools/vision-ocr.js +1 -0
- package/lib/updater.js +2 -2
- package/package.json +1 -1
package/lib/tools/generation.js
CHANGED
|
@@ -1,651 +1,496 @@
|
|
|
1
|
-
import { registerGenerationPackTool } from './generation-pack.js'
|
|
2
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
-
import {
|
|
6
|
-
computeGenerationHash,
|
|
7
|
-
IMAGE_SIZES,
|
|
8
|
-
OUTPUT_FORMATS,
|
|
9
|
-
PROVIDER_KEYS,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from '../
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
type: 'string',
|
|
71
|
-
|
|
72
|
-
description:
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
type: 'string',
|
|
76
|
-
description:
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
type: 'string',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
type: 'string',
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
description: 'Optional
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
type: '
|
|
111
|
-
description: '
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
type: '
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
type: '
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
meta: { provider, model: cfg.model || cfg.customModel, prompt: promptArg, seed: gen.seed },
|
|
498
|
-
})
|
|
499
|
-
|
|
500
|
-
return {
|
|
501
|
-
path: filePath,
|
|
502
|
-
url: deliverAs === 'image' && gen.sourceUrl ? gen.sourceUrl : localUrl,
|
|
503
|
-
width: gen.width || attachment.width,
|
|
504
|
-
height: gen.height || attachment.height,
|
|
505
|
-
seed: gen.seed,
|
|
506
|
-
prompt: promptArg,
|
|
507
|
-
...(enhanced.enhanced ? { originalPrompt: args.prompt } : {}),
|
|
508
|
-
cost: gen.cost,
|
|
509
|
-
format: mediaType.replace('image/', ''),
|
|
510
|
-
attachment: {
|
|
511
|
-
attachmentId: attachment.attachmentId,
|
|
512
|
-
mediaType: attachment.mediaType,
|
|
513
|
-
bytes: attachment.bytes,
|
|
514
|
-
width: attachment.width,
|
|
515
|
-
height: attachment.height,
|
|
516
|
-
name: attachment.name,
|
|
517
|
-
},
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
|
|
522
|
-
// Fallback cascade: evaluate configured provider, falling back to alternates
|
|
523
|
-
// (fal -> custom -> codex -> grok), accumulating failure diagnostics.
|
|
524
|
-
const order = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
525
|
-
const qgEnabled = args.quality_gate ?? cfg.qualityGate
|
|
526
|
-
const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [
|
|
527
|
-
k,
|
|
528
|
-
async (s, p) => {
|
|
529
|
-
const { generated, qualityReport } = await executeWithQualityGate(
|
|
530
|
-
(seedVal) => one(seedVal, k, p),
|
|
531
|
-
{
|
|
532
|
-
initialSeed: s,
|
|
533
|
-
enabled: qgEnabled,
|
|
534
|
-
ctx,
|
|
535
|
-
prompt: p,
|
|
536
|
-
}
|
|
537
|
-
)
|
|
538
|
-
return { ...generated, qualityReport }
|
|
539
|
-
}
|
|
540
|
-
]))
|
|
541
|
-
const images = []
|
|
542
|
-
const historyEntries = (cfg.cacheBySeed || cfg.cacheByPrompt) ? await readHistory() : []
|
|
543
|
-
const checkCache = async (seedVal, promptVal) => {
|
|
544
|
-
const h = computeGenerationHash({
|
|
545
|
-
provider,
|
|
546
|
-
model: cfg.model || cfg.customModel,
|
|
547
|
-
prompt: promptVal,
|
|
548
|
-
seed: seedVal,
|
|
549
|
-
size: args.image_size || cfg.defaultSize,
|
|
550
|
-
style: args.style || cfg.stylePreset,
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
// #170: Content-addressed disk cache check (<50ms return)
|
|
554
|
-
if (cfg.diskCache !== false && !args.force) {
|
|
555
|
-
const diskHit = getCachedGeneration(h, { force: args.force })
|
|
556
|
-
if (diskHit) {
|
|
557
|
-
const extension = diskHit.mediaType === 'image/jpeg' ? 'jpg' : diskHit.mediaType === 'image/webp' ? 'webp' : 'png'
|
|
558
|
-
const stem = `${slugify(args.output_name || promptVal)}-${Date.now().toString(36)}-${seedVal}`
|
|
559
|
-
const name = `${stem}.${extension}`
|
|
560
|
-
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: diskHit.bytes, mediaType: diskHit.mediaType, name })
|
|
561
|
-
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
562
|
-
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
563
|
-
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
564
|
-
await mkdir(outDir, { recursive: true })
|
|
565
|
-
const filePath = path.join(outDir, name)
|
|
566
|
-
await writeFile(filePath, diskHit.bytes)
|
|
567
|
-
|
|
568
|
-
return {
|
|
569
|
-
path: filePath,
|
|
570
|
-
url: localUrl,
|
|
571
|
-
width: diskHit.width || attachment.width,
|
|
572
|
-
height: diskHit.height || attachment.height,
|
|
573
|
-
seed: seedVal,
|
|
574
|
-
prompt: promptVal,
|
|
575
|
-
cost: 0,
|
|
576
|
-
fromCache: true,
|
|
577
|
-
format: diskHit.mediaType.replace('image/', ''),
|
|
578
|
-
attachment: {
|
|
579
|
-
attachmentId: attachment.attachmentId,
|
|
580
|
-
mediaType: attachment.mediaType,
|
|
581
|
-
bytes: attachment.bytes,
|
|
582
|
-
width: attachment.width,
|
|
583
|
-
height: attachment.height,
|
|
584
|
-
name: attachment.name,
|
|
585
|
-
},
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (!cfg.cacheBySeed && !cfg.cacheByPrompt) return undefined
|
|
591
|
-
const hPrompt = computeGenerationHash({
|
|
592
|
-
provider,
|
|
593
|
-
model: cfg.model,
|
|
594
|
-
prompt: promptVal,
|
|
595
|
-
seed: seedVal,
|
|
596
|
-
size: args.image_size || cfg.defaultSize,
|
|
597
|
-
style: args.style || cfg.stylePreset,
|
|
598
|
-
})
|
|
599
|
-
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
600
|
-
if (found) return await cachedResult(found)
|
|
601
|
-
if (cfg.cacheBySeed) {
|
|
602
|
-
const bySeed = await findCached(historyEntries, seedVal, promptVal)
|
|
603
|
-
if (bySeed) return await cachedResult(bySeed)
|
|
604
|
-
}
|
|
605
|
-
if (cfg.cacheByPrompt) {
|
|
606
|
-
const byPrompt = await findCachedByPrompt(historyEntries, promptVal)
|
|
607
|
-
if (byPrompt) return await cachedResult(byPrompt)
|
|
608
|
-
}
|
|
609
|
-
return undefined
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const batchPrompts = Array.isArray(args.prompts) && args.prompts.length ? args.prompts : null
|
|
613
|
-
if (batchPrompts) {
|
|
614
|
-
const tasks = batchPrompts.map((item, i) => async () => {
|
|
615
|
-
const text = typeof item === 'string' ? item : (item && item.text) || ''
|
|
616
|
-
const cached = await checkCache(seedBase + i, text)
|
|
617
|
-
return cached || await executeWithFallback(generators, order, seedBase + i, text, { logger: ctx.logger })
|
|
618
|
-
})
|
|
619
|
-
const parallelResults = await asyncPool(tasks, 3)
|
|
620
|
-
images.push(...parallelResults)
|
|
621
|
-
} else {
|
|
622
|
-
const count = normalizeCount(args.count)
|
|
623
|
-
const tasks = Array.from({ length: count }, (_, i) => async () => {
|
|
624
|
-
const cached = await checkCache(seedBase + i, effectivePrompt)
|
|
625
|
-
return cached || await executeWithFallback(generators, order, seedBase + i, effectivePrompt, { logger: ctx.logger })
|
|
626
|
-
})
|
|
627
|
-
const parallelResults = await asyncPool(tasks, 3)
|
|
628
|
-
images.push(...parallelResults)
|
|
629
|
-
}
|
|
630
|
-
const first = images[0]
|
|
631
|
-
const dualOutputSummary = buildDualOutputMarkdown({
|
|
632
|
-
action: 'generated',
|
|
633
|
-
filePath: first.path,
|
|
634
|
-
width: first.width,
|
|
635
|
-
height: first.height,
|
|
636
|
-
mediaType: first.mediaType || ('image/' + (first.format || 'png')),
|
|
637
|
-
seed: first.seed,
|
|
638
|
-
provider,
|
|
639
|
-
model: cfg.model || cfg.customModel || 'default',
|
|
640
|
-
cost: first.cost,
|
|
641
|
-
attachmentId: first.attachment?.attachmentId || 'N/A',
|
|
642
|
-
})
|
|
643
|
-
return toLosslessJson({ summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, _fallback: first._fallback, anchor: activeAnchor ? { label: activeAnchor.label, mode: activeAnchor.mode } : undefined, ...first, images })
|
|
644
|
-
},
|
|
645
|
-
}),
|
|
646
|
-
)
|
|
647
|
-
}, 'dsh-image-gen: tool generate_image')
|
|
648
|
-
if (!options.coreOnly) {
|
|
649
|
-
registerGenerationPackTool(ctx, deps)
|
|
650
|
-
}
|
|
651
|
-
}
|
|
1
|
+
import { registerGenerationPackTool } from './generation-pack.js'
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
5
|
+
import {
|
|
6
|
+
computeGenerationHash,
|
|
7
|
+
IMAGE_SIZES,
|
|
8
|
+
OUTPUT_FORMATS,
|
|
9
|
+
PROVIDER_KEYS,
|
|
10
|
+
makeProviders,
|
|
11
|
+
normalizeCount,
|
|
12
|
+
toLosslessJson,
|
|
13
|
+
saveAttachmentSafe,
|
|
14
|
+
} from '../providers.js'
|
|
15
|
+
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
16
|
+
import { executeWithQualityGate } from '../quality-gate.js'
|
|
17
|
+
import { calculateGenerationCost, assertBudgetAvailable } from '../cost-meter.js'
|
|
18
|
+
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
19
|
+
import { getCachedGeneration } from '../generation-cache.js'
|
|
20
|
+
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
21
|
+
import {
|
|
22
|
+
readHistory,
|
|
23
|
+
findCachedGeneration,
|
|
24
|
+
findCached,
|
|
25
|
+
findCachedByPrompt,
|
|
26
|
+
cachedResult,
|
|
27
|
+
} from '../history.js'
|
|
28
|
+
import {
|
|
29
|
+
asyncPool,
|
|
30
|
+
validateGenerationParams,
|
|
31
|
+
prepareGenerationPrompt,
|
|
32
|
+
saveGeneratedImageArtifact,
|
|
33
|
+
} from '../generation-helpers.js'
|
|
34
|
+
|
|
35
|
+
// generation — image-gen tools (generate_image, generate_image_pack). Extracted from apply() (#216).
|
|
36
|
+
|
|
37
|
+
export function registerGenerationTools(ctx, deps, options = {}) {
|
|
38
|
+
const {
|
|
39
|
+
config,
|
|
40
|
+
live,
|
|
41
|
+
resolveSource,
|
|
42
|
+
slugify,
|
|
43
|
+
resolveApiKey,
|
|
44
|
+
} = deps
|
|
45
|
+
ctx.effect(() => {
|
|
46
|
+
ctx.tools.register(
|
|
47
|
+
defineTool({
|
|
48
|
+
name: 'generate_image',
|
|
49
|
+
description: 'Generate an image from prompt. Saves the image to workspace and conversation. Pass count (1-4) for variations.',
|
|
50
|
+
parameters: {
|
|
51
|
+
prompt: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
required: true,
|
|
54
|
+
description: 'Detailed description of the image to generate.',
|
|
55
|
+
},
|
|
56
|
+
image_size: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: `One of: ${IMAGE_SIZES.join(', ')}. Default: ${config?.defaultSize || 'square_hd'}.`,
|
|
59
|
+
},
|
|
60
|
+
aspect_ratio: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
|
|
63
|
+
description: 'Optional aspect ratio (e.g. 16:9, 1:1), overrides image_size.',
|
|
64
|
+
},
|
|
65
|
+
seed: {
|
|
66
|
+
type: 'integer',
|
|
67
|
+
description: 'Optional seed for reproducible output.',
|
|
68
|
+
},
|
|
69
|
+
output_format: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
enum: OUTPUT_FORMATS,
|
|
72
|
+
description: `Output format. Default: ${config?.defaultFormat || 'png'}.`,
|
|
73
|
+
},
|
|
74
|
+
output_name: {
|
|
75
|
+
type: 'string',
|
|
76
|
+
description: 'Optional file name stem for the saved image.',
|
|
77
|
+
},
|
|
78
|
+
output_dir: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Optional custom directory to write images into.',
|
|
81
|
+
},
|
|
82
|
+
style_preset: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Optional style preset: cinematic, photorealistic, anime, minimalist_vector, isometric_3d, analog_film, cyberpunk, claymation.',
|
|
85
|
+
},
|
|
86
|
+
palette_colors: {
|
|
87
|
+
type: 'array',
|
|
88
|
+
items: { type: 'string' },
|
|
89
|
+
description: 'Optional hex color codes (#1e1b4b) to harmonize palette.',
|
|
90
|
+
},
|
|
91
|
+
count: {
|
|
92
|
+
type: 'integer',
|
|
93
|
+
description: 'Variations count (1-4, default 1).',
|
|
94
|
+
},
|
|
95
|
+
prompts: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
items: {
|
|
98
|
+
oneOf: [
|
|
99
|
+
{ type: 'string' },
|
|
100
|
+
{ type: 'object', additionalProperties: false, properties: { text: { type: 'string' } } },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
description: 'Optional list of prompts for batch generation.',
|
|
104
|
+
},
|
|
105
|
+
negative_prompt: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Optional text describing what NOT to draw.',
|
|
108
|
+
},
|
|
109
|
+
guidance_scale: {
|
|
110
|
+
type: 'number',
|
|
111
|
+
description: 'Optional prompt adherence (1-20).',
|
|
112
|
+
},
|
|
113
|
+
quality: {
|
|
114
|
+
type: 'string',
|
|
115
|
+
enum: ['auto', 'low', 'medium', 'high'],
|
|
116
|
+
description: 'Optional quality for supported providers.',
|
|
117
|
+
},
|
|
118
|
+
style: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'Optional style preset for supported providers.',
|
|
121
|
+
},
|
|
122
|
+
source_image: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Optional source image path or attachment ID for img2img.',
|
|
125
|
+
},
|
|
126
|
+
mask: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Optional inpaint mask path or attachment ID.',
|
|
129
|
+
},
|
|
130
|
+
strength: {
|
|
131
|
+
type: 'number',
|
|
132
|
+
description: 'Optional edit strength (0-1).',
|
|
133
|
+
},
|
|
134
|
+
quality_gate: {
|
|
135
|
+
type: 'boolean',
|
|
136
|
+
description: 'Optional automated quality gate override.',
|
|
137
|
+
},
|
|
138
|
+
force: {
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
description: 'Bypass cache and force new generation.',
|
|
141
|
+
},
|
|
142
|
+
reference_image: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Optional reference image path or URL for consistency.',
|
|
145
|
+
},
|
|
146
|
+
reference_strength: {
|
|
147
|
+
type: 'number',
|
|
148
|
+
description: 'Strength of reference influence (0.1-1.0).',
|
|
149
|
+
},
|
|
150
|
+
face_reference: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
description: 'Portrait reference image for facial identity preservation.',
|
|
153
|
+
},
|
|
154
|
+
face_strength: {
|
|
155
|
+
type: 'number',
|
|
156
|
+
description: 'Facial identity similarity weight (0.1-1.0).',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
output: {
|
|
160
|
+
schema: {
|
|
161
|
+
type: 'object',
|
|
162
|
+
additionalProperties: true,
|
|
163
|
+
properties: {
|
|
164
|
+
summary: { type: 'string' },
|
|
165
|
+
channel: { type: 'string' },
|
|
166
|
+
provider: { type: 'string' },
|
|
167
|
+
model: { type: 'string' },
|
|
168
|
+
_fallback: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
additionalProperties: true,
|
|
171
|
+
properties: {
|
|
172
|
+
triggered: { type: 'boolean' },
|
|
173
|
+
primaryProvider: { type: 'string' },
|
|
174
|
+
providerUsed: { type: 'string' },
|
|
175
|
+
attempts: {
|
|
176
|
+
type: 'array',
|
|
177
|
+
items: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
additionalProperties: true,
|
|
180
|
+
properties: {
|
|
181
|
+
provider: { type: 'string' },
|
|
182
|
+
durationMs: { type: 'number' },
|
|
183
|
+
success: { type: 'boolean' },
|
|
184
|
+
error: { type: 'string' },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
path: { type: 'string' },
|
|
191
|
+
url: { type: 'string' },
|
|
192
|
+
width: { type: 'integer' },
|
|
193
|
+
height: { type: 'integer' },
|
|
194
|
+
seed: { type: 'integer' },
|
|
195
|
+
prompt: { type: 'string' },
|
|
196
|
+
originalPrompt: { type: 'string' },
|
|
197
|
+
format: { type: 'string' },
|
|
198
|
+
cost: { type: 'number' },
|
|
199
|
+
fromCache: { type: 'boolean' },
|
|
200
|
+
qualityReport: { type: 'object', additionalProperties: true },
|
|
201
|
+
anchor: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
additionalProperties: true,
|
|
204
|
+
properties: {
|
|
205
|
+
label: { type: 'string' },
|
|
206
|
+
mode: { type: 'string' },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
attachment: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
additionalProperties: true,
|
|
212
|
+
properties: {
|
|
213
|
+
attachmentId: { type: 'string' },
|
|
214
|
+
mediaType: { type: 'string' },
|
|
215
|
+
bytes: { type: 'integer' },
|
|
216
|
+
width: { type: 'integer' },
|
|
217
|
+
height: { type: 'integer' },
|
|
218
|
+
name: { type: 'string' },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
images: {
|
|
222
|
+
type: 'array',
|
|
223
|
+
items: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
additionalProperties: true,
|
|
226
|
+
properties: {
|
|
227
|
+
path: { type: 'string' },
|
|
228
|
+
url: { type: 'string' },
|
|
229
|
+
width: { type: 'integer' },
|
|
230
|
+
height: { type: 'integer' },
|
|
231
|
+
seed: { type: 'integer' },
|
|
232
|
+
prompt: { type: 'string' },
|
|
233
|
+
originalPrompt: { type: 'string' },
|
|
234
|
+
format: { type: 'string' },
|
|
235
|
+
cost: { type: 'number' },
|
|
236
|
+
fromCache: { type: 'boolean' },
|
|
237
|
+
qualityReport: { type: 'object', additionalProperties: true },
|
|
238
|
+
_fallback: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
additionalProperties: true,
|
|
241
|
+
properties: {
|
|
242
|
+
triggered: { type: 'boolean' },
|
|
243
|
+
primaryProvider: { type: 'string' },
|
|
244
|
+
providerUsed: { type: 'string' },
|
|
245
|
+
attempts: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
items: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
additionalProperties: true,
|
|
250
|
+
properties: {
|
|
251
|
+
provider: { type: 'string' },
|
|
252
|
+
durationMs: { type: 'number' },
|
|
253
|
+
success: { type: 'boolean' },
|
|
254
|
+
error: { type: 'string' },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
provider: { type: 'string' },
|
|
261
|
+
model: { type: 'string' },
|
|
262
|
+
attachment: {
|
|
263
|
+
type: 'object',
|
|
264
|
+
additionalProperties: true,
|
|
265
|
+
properties: {
|
|
266
|
+
attachmentId: { type: 'string' },
|
|
267
|
+
mediaType: { type: 'string' },
|
|
268
|
+
bytes: { type: 'integer' },
|
|
269
|
+
width: { type: 'integer' },
|
|
270
|
+
height: { type: 'integer' },
|
|
271
|
+
name: { type: 'string' },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
render(args, value) {
|
|
280
|
+
const list = value.images || [value]
|
|
281
|
+
const lines = list.map((img) => `Generated image (${img.width}×${img.height}, ${img.format}, seed ${img.seed}) via ${value.provider || 'image-gen'}/${value.model || 'default'}: ${img.path}`)
|
|
282
|
+
const summary = lines.join('\n')
|
|
283
|
+
if (live().deliverAs !== 'image') {
|
|
284
|
+
const urls = list.map((img) => img.url).filter(Boolean)
|
|
285
|
+
return [{ type: 'text', text: urls.length ? `${summary}\n${urls.join('\n')}` : summary }]
|
|
286
|
+
}
|
|
287
|
+
const blocks = [{ type: 'text', text: summary }]
|
|
288
|
+
for (const img of list) if (img.attachment) blocks.push({ type: 'image', attachment: img.attachment })
|
|
289
|
+
return blocks
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
isConcurrencySafe: () => false,
|
|
293
|
+
timeoutMs: (config?.timeoutMs || 180000) + 30000,
|
|
294
|
+
async execute(args, exec) {
|
|
295
|
+
const cfg = live()
|
|
296
|
+
|
|
297
|
+
// #168: Fail-fast loop guard protecting against runaway agent loops
|
|
298
|
+
const sessionId = exec.agent?.session?.id || exec.agent?.session?.header?.id || 'session'
|
|
299
|
+
if (cfg.loopGuardLimit > 0) {
|
|
300
|
+
trackAndAssertLoopGuard(sessionId, {
|
|
301
|
+
limit: cfg.loopGuardLimit,
|
|
302
|
+
prompt: args.prompt,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { provider, deliverAs, size, aspectPixels, format } = validateGenerationParams(cfg, args)
|
|
307
|
+
|
|
308
|
+
// Subscriptions service is optional: without it, subscription providers
|
|
309
|
+
// gracefully decline while other providers continue functioning.
|
|
310
|
+
let subscriptionImages
|
|
311
|
+
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
312
|
+
|
|
313
|
+
const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
|
|
314
|
+
const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
|
|
315
|
+
|
|
316
|
+
const {
|
|
317
|
+
enhanced,
|
|
318
|
+
effectivePrompt,
|
|
319
|
+
effectiveNegative,
|
|
320
|
+
effectiveGuidance,
|
|
321
|
+
activeAnchor,
|
|
322
|
+
resolvedFaceRef,
|
|
323
|
+
} = await prepareGenerationPrompt({ ctx, cfg, args, exec, sessionId, provider, resolveSource })
|
|
324
|
+
|
|
325
|
+
// #167: Assert daily budget availability before calling API
|
|
326
|
+
const totalCount = Array.isArray(args.prompts) && args.prompts.length ? args.prompts.length : normalizeCount(args.count)
|
|
327
|
+
const estimatedCost = calculateGenerationCost({
|
|
328
|
+
provider,
|
|
329
|
+
model: cfg.model || cfg.customModel,
|
|
330
|
+
size,
|
|
331
|
+
count: totalCount,
|
|
332
|
+
})
|
|
333
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
334
|
+
const providers = makeProviders(
|
|
335
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
336
|
+
{ prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: effectiveNegative, guidanceScale: effectiveGuidance, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels, aspectRatio: args.aspect_ratio, faceReference: resolvedFaceRef },
|
|
337
|
+
)
|
|
338
|
+
// Subscription providers return {ok:false, reason} instead of throwing:
|
|
339
|
+
// rejection reaches the model as text. Without this guard,
|
|
340
|
+
// execution would proceed with empty bytes.
|
|
341
|
+
const guard = (generated) => { if (generated && generated.ok === false) throw new Error(generated.reason) }
|
|
342
|
+
const one = async (jobSeed, providerKey, promptArg = effectivePrompt) => {
|
|
343
|
+
const gen = await providers[providerKey](jobSeed, promptArg)
|
|
344
|
+
guard(gen)
|
|
345
|
+
return await saveGeneratedImageArtifact({
|
|
346
|
+
ctx,
|
|
347
|
+
exec,
|
|
348
|
+
cfg,
|
|
349
|
+
args,
|
|
350
|
+
gen,
|
|
351
|
+
promptArg,
|
|
352
|
+
jobSeed,
|
|
353
|
+
provider,
|
|
354
|
+
deliverAs,
|
|
355
|
+
size,
|
|
356
|
+
format,
|
|
357
|
+
effectiveNegative,
|
|
358
|
+
effectiveGuidance,
|
|
359
|
+
enhanced,
|
|
360
|
+
slugify,
|
|
361
|
+
estimatedCost,
|
|
362
|
+
totalCount,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const seedBase = args.seed ?? Math.floor(Math.random() * 100000)
|
|
367
|
+
// Fallback cascade: evaluate configured provider, falling back to alternates
|
|
368
|
+
// (fal -> custom -> codex -> grok), accumulating failure diagnostics.
|
|
369
|
+
const order = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
370
|
+
const qgEnabled = args.quality_gate ?? cfg.qualityGate
|
|
371
|
+
const generators = Object.fromEntries(PROVIDER_KEYS.map((k) => [
|
|
372
|
+
k,
|
|
373
|
+
async (s, p) => {
|
|
374
|
+
const { generated, qualityReport } = await executeWithQualityGate(
|
|
375
|
+
(seedVal) => one(seedVal, k, p),
|
|
376
|
+
{
|
|
377
|
+
initialSeed: s,
|
|
378
|
+
enabled: qgEnabled,
|
|
379
|
+
ctx,
|
|
380
|
+
prompt: p,
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
return { ...generated, qualityReport }
|
|
384
|
+
}
|
|
385
|
+
]))
|
|
386
|
+
const images = []
|
|
387
|
+
const historyEntries = (cfg.cacheBySeed || cfg.cacheByPrompt) ? await readHistory() : []
|
|
388
|
+
const checkCache = async (seedVal, promptVal) => {
|
|
389
|
+
const h = computeGenerationHash({
|
|
390
|
+
provider,
|
|
391
|
+
model: cfg.model || cfg.customModel,
|
|
392
|
+
prompt: promptVal,
|
|
393
|
+
seed: seedVal,
|
|
394
|
+
size: args.image_size || cfg.defaultSize,
|
|
395
|
+
style: args.style || cfg.stylePreset,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
// #170: Content-addressed disk cache check (<50ms return)
|
|
399
|
+
if (cfg.diskCache !== false && !args.force) {
|
|
400
|
+
const diskHit = getCachedGeneration(h, { force: args.force })
|
|
401
|
+
if (diskHit) {
|
|
402
|
+
const extension = diskHit.mediaType === 'image/jpeg' ? 'jpg' : diskHit.mediaType === 'image/webp' ? 'webp' : 'png'
|
|
403
|
+
const stem = `${slugify(args.output_name || promptVal)}-${Date.now().toString(36)}-${seedVal}`
|
|
404
|
+
const name = `${stem}.${extension}`
|
|
405
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: diskHit.bytes, mediaType: diskHit.mediaType, name })
|
|
406
|
+
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
407
|
+
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
408
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
409
|
+
await mkdir(outDir, { recursive: true })
|
|
410
|
+
const filePath = path.join(outDir, name)
|
|
411
|
+
await writeFile(filePath, diskHit.bytes)
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
path: filePath,
|
|
415
|
+
url: localUrl,
|
|
416
|
+
width: diskHit.width || attachment.width,
|
|
417
|
+
height: diskHit.height || attachment.height,
|
|
418
|
+
seed: seedVal,
|
|
419
|
+
prompt: promptVal,
|
|
420
|
+
cost: 0,
|
|
421
|
+
fromCache: true,
|
|
422
|
+
format: diskHit.mediaType.replace('image/', ''),
|
|
423
|
+
attachment: {
|
|
424
|
+
attachmentId: attachment.attachmentId,
|
|
425
|
+
mediaType: attachment.mediaType,
|
|
426
|
+
bytes: attachment.bytes,
|
|
427
|
+
width: attachment.width,
|
|
428
|
+
height: attachment.height,
|
|
429
|
+
name: attachment.name,
|
|
430
|
+
},
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!cfg.cacheBySeed && !cfg.cacheByPrompt) return undefined
|
|
436
|
+
const hPrompt = computeGenerationHash({
|
|
437
|
+
provider,
|
|
438
|
+
model: cfg.model,
|
|
439
|
+
prompt: promptVal,
|
|
440
|
+
seed: seedVal,
|
|
441
|
+
size: args.image_size || cfg.defaultSize,
|
|
442
|
+
style: args.style || cfg.stylePreset,
|
|
443
|
+
})
|
|
444
|
+
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
445
|
+
if (found) return await cachedResult(found)
|
|
446
|
+
if (cfg.cacheBySeed) {
|
|
447
|
+
const bySeed = await findCached(historyEntries, seedVal, promptVal)
|
|
448
|
+
if (bySeed) return await cachedResult(bySeed)
|
|
449
|
+
}
|
|
450
|
+
if (cfg.cacheByPrompt) {
|
|
451
|
+
const byPrompt = await findCachedByPrompt(historyEntries, promptVal)
|
|
452
|
+
if (byPrompt) return await cachedResult(byPrompt)
|
|
453
|
+
}
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const batchPrompts = Array.isArray(args.prompts) && args.prompts.length ? args.prompts : null
|
|
458
|
+
if (batchPrompts) {
|
|
459
|
+
const tasks = batchPrompts.map((item, i) => async () => {
|
|
460
|
+
const text = typeof item === 'string' ? item : (item && item.text) || ''
|
|
461
|
+
const cached = await checkCache(seedBase + i, text)
|
|
462
|
+
return cached || await executeWithFallback(generators, order, seedBase + i, text, { logger: ctx.logger })
|
|
463
|
+
})
|
|
464
|
+
const parallelResults = await asyncPool(tasks, 3)
|
|
465
|
+
images.push(...parallelResults)
|
|
466
|
+
} else {
|
|
467
|
+
const count = normalizeCount(args.count)
|
|
468
|
+
const tasks = Array.from({ length: count }, (_, i) => async () => {
|
|
469
|
+
const cached = await checkCache(seedBase + i, effectivePrompt)
|
|
470
|
+
return cached || await executeWithFallback(generators, order, seedBase + i, effectivePrompt, { logger: ctx.logger })
|
|
471
|
+
})
|
|
472
|
+
const parallelResults = await asyncPool(tasks, 3)
|
|
473
|
+
images.push(...parallelResults)
|
|
474
|
+
}
|
|
475
|
+
const first = images[0]
|
|
476
|
+
const dualOutputSummary = buildDualOutputMarkdown({
|
|
477
|
+
action: 'generated',
|
|
478
|
+
filePath: first.path,
|
|
479
|
+
width: first.width,
|
|
480
|
+
height: first.height,
|
|
481
|
+
mediaType: first.mediaType || ('image/' + (first.format || 'png')),
|
|
482
|
+
seed: first.seed,
|
|
483
|
+
provider,
|
|
484
|
+
model: cfg.model || cfg.customModel || 'default',
|
|
485
|
+
cost: first.cost,
|
|
486
|
+
attachmentId: first.attachment?.attachmentId || 'N/A',
|
|
487
|
+
})
|
|
488
|
+
return toLosslessJson({ summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, _fallback: first._fallback, anchor: activeAnchor ? { label: activeAnchor.label, mode: activeAnchor.mode } : undefined, ...first, images })
|
|
489
|
+
},
|
|
490
|
+
}),
|
|
491
|
+
)
|
|
492
|
+
}, 'dsh-image-gen: tool generate_image')
|
|
493
|
+
if (!options.coreOnly) {
|
|
494
|
+
registerGenerationPackTool(ctx, deps)
|
|
495
|
+
}
|
|
496
|
+
}
|