@goodandready/dsh-image-gen 0.10.1 → 0.10.2
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/client.js +3 -1
- package/lib/index.js +37 -6
- package/lib/providers.js +41 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -461,10 +461,12 @@ window.__ModuleLoader__.load({
|
|
|
461
461
|
}
|
|
462
462
|
const OUTPUT_FORMATS = ['png', 'jpeg', 'webp']
|
|
463
463
|
const DELIVERY_MODES = ['link', 'image']
|
|
464
|
-
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local']
|
|
464
|
+
const PROVIDERS = ['fal', 'custom', 'codex', 'grok', 'local', 'seedream', 'gemini', 'replicate']
|
|
465
465
|
|
|
466
466
|
/** Field specs in display order. */
|
|
467
|
+
const booleanField = (name) => ({ name, type: 'boolean', parse: (v) => v === 'true' || v === true, format: (v) => String(!!v) })
|
|
467
468
|
const FIELDS = [
|
|
469
|
+
{ field: 'enabled', spec: booleanField('enabled'), kind: 'select', options: ['true', 'false'], labelKey: 'f.enabled', hintKey: 'f.enabledHint' },
|
|
468
470
|
{ field: 'provider', spec: selectField('provider', PROVIDERS), kind: 'select', options: PROVIDERS, labelKey: 'f.provider', hintKey: 'f.providerHint', providerNeeds: { fal: 'ключ FAL', custom: 'адрес + ключ', codex: 'подписка ChatGPT', grok: 'подписка Grok', local: 'адрес сервера' } },
|
|
469
471
|
{ field: 'model', spec: textField('model'), kind: 'text', labelKey: 'f.model', hintKey: 'f.modelHint', placeholderKey: 'p.model', when: ['fal'] },
|
|
470
472
|
{ field: 'apiKeyEnv', spec: textField('apiKeyEnv'), kind: 'text', labelKey: 'f.apiKeyEnv', hintKey: 'f.apiKeyEnvHint', placeholderKey: 'p.apiKeyEnv', when: ['fal'] },
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,9 @@ import os from 'node:os'
|
|
|
21
21
|
import path from 'node:path'
|
|
22
22
|
import {
|
|
23
23
|
computeGenerationHash,
|
|
24
|
+
clampProviderCount,
|
|
25
|
+
buildEndpointUrl,
|
|
26
|
+
createAbortError,
|
|
24
27
|
IMAGE_SIZES,
|
|
25
28
|
OUTPUT_FORMATS,
|
|
26
29
|
PROVIDER_KEYS,
|
|
@@ -55,9 +58,13 @@ const NS = 'dsh-image-gen'
|
|
|
55
58
|
// настройки лежат под старым именем. Оно читается как запасное и переносится
|
|
56
59
|
// под новое имя один раз — молча, при первом запуске после обновления.
|
|
57
60
|
const LEGACY_NS = 'dsh-fal-image-gen'
|
|
58
|
-
export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm']
|
|
61
|
+
export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm', 'systemPrompt']
|
|
59
62
|
|
|
60
63
|
export const Config = z.object({
|
|
64
|
+
enabled: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.description('Master switch for image generation. When false, tools refuse to execute.')
|
|
67
|
+
.default(true),
|
|
61
68
|
provider: z
|
|
62
69
|
.string()
|
|
63
70
|
.description(`Which provider generates the image. One of: ${PROVIDER_KEYS.join(', ')}. `
|
|
@@ -69,6 +76,7 @@ export const Config = z.object({
|
|
|
69
76
|
.default('fal-ai/flux-2/klein/9b'),
|
|
70
77
|
apiKeyEnv: z
|
|
71
78
|
.string()
|
|
79
|
+
.role('credential-ref')
|
|
72
80
|
.description('Credential reference / env var holding the FAL API key (the "Key " auth prefix is added automatically when missing).')
|
|
73
81
|
.default('FAL_API_KEY'),
|
|
74
82
|
baseURL: z
|
|
@@ -110,6 +118,7 @@ export const Config = z.object({
|
|
|
110
118
|
.default(''),
|
|
111
119
|
customKeyEnv: z
|
|
112
120
|
.string()
|
|
121
|
+
.role('credential-ref')
|
|
113
122
|
.description('provider=custom: credential reference / env var holding the API key. '
|
|
114
123
|
+ 'Empty means no authorization header, for gateways that need none.')
|
|
115
124
|
.default('OPENAI_API_KEY'),
|
|
@@ -120,6 +129,7 @@ export const Config = z.object({
|
|
|
120
129
|
.default('black-forest-labs/flux-schnell'),
|
|
121
130
|
replicateKeyEnv: z
|
|
122
131
|
.string()
|
|
132
|
+
.role('credential-ref')
|
|
123
133
|
.description('provider=replicate: credential reference / env var holding API token.')
|
|
124
134
|
.default('REPLICATE_API_TOKEN'),
|
|
125
135
|
|
|
@@ -155,6 +165,7 @@ export const Config = z.object({
|
|
|
155
165
|
.default(7),
|
|
156
166
|
seedreamKeyEnv: z
|
|
157
167
|
.string()
|
|
168
|
+
.role('credential-ref')
|
|
158
169
|
.description('provider=seedream: credential reference / env var holding the API key.')
|
|
159
170
|
.default('SEEDREAM_API_KEY'),
|
|
160
171
|
seedreamModel: z
|
|
@@ -163,6 +174,7 @@ export const Config = z.object({
|
|
|
163
174
|
.default('seedream-4.0'),
|
|
164
175
|
geminiKeyEnv: z
|
|
165
176
|
.string()
|
|
177
|
+
.role('credential-ref')
|
|
166
178
|
.description('provider=gemini: credential reference / env var holding the Google API key.')
|
|
167
179
|
.default('GEMINI_API_KEY'),
|
|
168
180
|
geminiModel: z
|
|
@@ -414,6 +426,17 @@ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
|
|
|
414
426
|
}
|
|
415
427
|
|
|
416
428
|
export function apply(ctx, config) {
|
|
429
|
+
if (ctx.systemPrompt && typeof ctx.systemPrompt.section === 'function') {
|
|
430
|
+
ctx.systemPrompt.section({
|
|
431
|
+
name: 'tool:image-generation',
|
|
432
|
+
order: 121,
|
|
433
|
+
text: 'To create images, call generate_image with a prompt and optional count, aspect_ratio, quality, or output_dir. '
|
|
434
|
+
+ 'The tool writes images to disk and shows them in the conversation. The model cannot see the image directly in the text stream; '
|
|
435
|
+
+ 'for quality inspection, verification of expected elements, or autonomous correction (Vision Loop), call analyze_image from the @goodandready/dsh-vision-bridge plugin '
|
|
436
|
+
+ 'or inspect_image_quality on the returned path.',
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
|
|
417
440
|
|
|
418
441
|
// The Web card edits this namespace; without registering it the card binds to
|
|
419
442
|
// a namespace nobody declared, stays unready and renders nothing — which is
|
|
@@ -532,7 +555,7 @@ export function apply(ctx, config) {
|
|
|
532
555
|
},
|
|
533
556
|
aspect_ratio: {
|
|
534
557
|
type: 'string',
|
|
535
|
-
enum: ['1:1', '16:9', '9:16', '4:3', '3:4'],
|
|
558
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'],
|
|
536
559
|
description: 'Optional aspect ratio; overrides image_size when both are given. Sent as width/height to providers that accept pixels.',
|
|
537
560
|
},
|
|
538
561
|
seed: {
|
|
@@ -548,6 +571,10 @@ export function apply(ctx, config) {
|
|
|
548
571
|
type: 'string',
|
|
549
572
|
description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
|
|
550
573
|
},
|
|
574
|
+
output_dir: {
|
|
575
|
+
type: 'string',
|
|
576
|
+
description: 'Optional custom directory to write images into (relative to session cwd or absolute). Defaults to outputDir from settings.',
|
|
577
|
+
},
|
|
551
578
|
count: {
|
|
552
579
|
type: 'integer',
|
|
553
580
|
description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
|
|
@@ -572,7 +599,7 @@ export function apply(ctx, config) {
|
|
|
572
599
|
},
|
|
573
600
|
quality: {
|
|
574
601
|
type: 'string',
|
|
575
|
-
enum: ['low', 'medium', 'high'],
|
|
602
|
+
enum: ['auto', 'low', 'medium', 'high'],
|
|
576
603
|
description: 'Optional quality for providers that support it (seedream, gemini). Ignored otherwise.',
|
|
577
604
|
},
|
|
578
605
|
style: {
|
|
@@ -636,7 +663,7 @@ export function apply(ctx, config) {
|
|
|
636
663
|
},
|
|
637
664
|
render(args, value) {
|
|
638
665
|
const list = value.images || [value]
|
|
639
|
-
const lines = list.map((img) => `
|
|
666
|
+
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}`)
|
|
640
667
|
const summary = lines.join('\n')
|
|
641
668
|
if (live().deliverAs !== 'image') {
|
|
642
669
|
const urls = list.map((img) => img.url).filter(Boolean)
|
|
@@ -651,6 +678,9 @@ export function apply(ctx, config) {
|
|
|
651
678
|
timeoutMs: config.timeoutMs + 30000,
|
|
652
679
|
async execute(args, exec) {
|
|
653
680
|
const cfg = live()
|
|
681
|
+
if (cfg.enabled === false) {
|
|
682
|
+
throw new Error('Image generation is disabled in settings (dsh-image-gen.enabled is false); enable it in Settings → Image generation.')
|
|
683
|
+
}
|
|
654
684
|
const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
|
|
655
685
|
const deliverAs = cfg.deliverAs
|
|
656
686
|
const size = args.aspect_ratio ? 'custom' : (args.image_size ?? cfg.defaultSize)
|
|
@@ -711,7 +741,8 @@ export function apply(ctx, config) {
|
|
|
711
741
|
})
|
|
712
742
|
|
|
713
743
|
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
714
|
-
const
|
|
744
|
+
const targetDir = (args.output_dir && String(args.output_dir).trim()) || cfg.outputDir || 'generated/images'
|
|
745
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), targetDir)
|
|
715
746
|
await mkdir(outDir, { recursive: true })
|
|
716
747
|
const filePath = path.join(outDir, name)
|
|
717
748
|
await writeFile(filePath, bytes)
|
|
@@ -840,7 +871,7 @@ export function apply(ctx, config) {
|
|
|
840
871
|
}
|
|
841
872
|
}
|
|
842
873
|
const first = images[0]
|
|
843
|
-
return { ...first, images }
|
|
874
|
+
return { channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images }
|
|
844
875
|
},
|
|
845
876
|
}),
|
|
846
877
|
)
|
package/lib/providers.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
|
|
2
|
+
export const PROVIDER_MAX_COUNTS = {
|
|
3
|
+
fal: 4,
|
|
4
|
+
replicate: 4,
|
|
5
|
+
custom: 10,
|
|
6
|
+
seedream: 10,
|
|
7
|
+
gemini: 4,
|
|
8
|
+
codex: 1,
|
|
9
|
+
grok: 1,
|
|
10
|
+
local: 4,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function clampProviderCount(provider, count) {
|
|
14
|
+
const max = PROVIDER_MAX_COUNTS[provider] || 4
|
|
15
|
+
return normalizeCount(count, max)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildEndpointUrl(baseURL, pathSuffix) {
|
|
19
|
+
const base = String(baseURL || '').trim().replace(/\/+$/, '')
|
|
20
|
+
const suffix = String(pathSuffix || '').trim().replace(/^\/+/, '')
|
|
21
|
+
return `${base}/${suffix}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createAbortError(message = 'Image generation cancelled') {
|
|
25
|
+
const err = new Error(message)
|
|
26
|
+
err.name = 'AbortError'
|
|
27
|
+
return err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
2
31
|
/** Fast Laplacian-like sharpness and entropy estimator across image scanlines. */
|
|
3
32
|
export function estimateSharpnessAndVariance(bytes) {
|
|
4
33
|
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes || [])
|
|
@@ -132,10 +161,10 @@ export async function tryGenerate(generators, order, seed, promptArg) {
|
|
|
132
161
|
throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
|
|
133
162
|
}
|
|
134
163
|
|
|
135
|
-
export function normalizeCount(count) {
|
|
164
|
+
export function normalizeCount(count, max = 4) {
|
|
136
165
|
const n = Math.floor(Number(count))
|
|
137
166
|
if (!Number.isFinite(n)) return 1
|
|
138
|
-
return Math.max(1, Math.min(
|
|
167
|
+
return Math.max(1, Math.min(max, n))
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
// Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
|
|
@@ -268,6 +297,8 @@ export const ASPECT_RATIOS = {
|
|
|
268
297
|
'9:16': [768, 1344],
|
|
269
298
|
'4:3': [1152, 896],
|
|
270
299
|
'3:4': [896, 1152],
|
|
300
|
+
'3:2': [1152, 768],
|
|
301
|
+
'2:3': [768, 1152],
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
/** Размер в пикселях: aspectPixels (если задан) или из именованного размера. */
|
|
@@ -370,7 +401,7 @@ export function makeProviders(deps, job) {
|
|
|
370
401
|
|
|
371
402
|
// response_format не отправляем: новые модели OpenAI его отвергают, а ответ
|
|
372
403
|
// всё равно приходит либо base64, либо ссылкой — принимаем оба.
|
|
373
|
-
const endpoint = source ?
|
|
404
|
+
const endpoint = buildEndpointUrl(base, source ? 'images/edits' : 'images/generations')
|
|
374
405
|
const res = await fetchImpl(endpoint, {
|
|
375
406
|
method: 'POST',
|
|
376
407
|
headers,
|
|
@@ -383,11 +414,18 @@ export function makeProviders(deps, job) {
|
|
|
383
414
|
size: cfg.customSize || (aspectPixels ? aspectPixels.join('x') : SIZE_PIXELS[size]) || size,
|
|
384
415
|
...(negativePrompt !== undefined ? { negative_prompt: negativePrompt } : {}),
|
|
385
416
|
...(guidanceScale !== undefined ? { guidance_scale: guidanceScale } : {}),
|
|
417
|
+
...(quality && quality !== 'auto' ? { quality } : {}),
|
|
386
418
|
}),
|
|
387
419
|
signal,
|
|
388
420
|
})
|
|
389
421
|
const data = await res.json().catch(() => ({}))
|
|
390
422
|
if (!res.ok) {
|
|
423
|
+
if (res.status === 503) {
|
|
424
|
+
const msg = (data?.error?.message || JSON.stringify(data)).toLowerCase()
|
|
425
|
+
if (msg.includes('no available channel') || msg.includes('model_not_found')) {
|
|
426
|
+
throw new Error(`Custom image API has no provisioned channel for "${cfg.customModel}" (HTTP 503); check gateway channel and routing configuration`)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
391
429
|
const detail = data?.error?.message || JSON.stringify(data).slice(0, 600)
|
|
392
430
|
throw new Error(`Image API failed (HTTP ${res.status}): ${detail}`)
|
|
393
431
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|