@goodandready/dsh-image-gen 0.10.1 → 0.10.3

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 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) => `Image generated (${img.width}×${img.height}, ${img.format}, seed ${img.seed}): ${img.path}`)
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 outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir)
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(4, n))
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 ? `${base}/images/edits` : `${base}/images/generations`
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.1",
3
+ "version": "0.10.3",
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",
@@ -49,9 +49,7 @@
49
49
  "client": {
50
50
  "platform": "web",
51
51
  "inject": [
52
- "@deepseek-ai/dsh-client-runtime",
53
52
  "@deepseek-ai/dsh-client-locale",
54
- "@deepseek-ai/dsh-client-ui-slots",
55
53
  "@deepseek-ai/dsh-client-ui-settings"
56
54
  ]
57
55
  }