@goodandready/dsh-image-gen 0.10.0 → 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 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'] },
@@ -808,17 +810,38 @@ window.__ModuleLoader__.load({
808
810
  }
809
811
  }
810
812
 
811
- function FalImageCard(props) {
813
+ function FalImageCard(props) {
812
814
  const block = props.block
813
815
  const running = !('kind' in (block || {}))
814
816
  const failed = block && (block.isError || block.error !== undefined)
815
817
  const parsed = readResult(block)
816
- const [editing, setEditing] = react.useState(false)
817
818
 
818
- const [inpaintOpen, setInpaintOpen] = react.useState(false)
819
+ let args = {}
820
+ try {
821
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
822
+ if (raw) args = JSON.parse(raw)
823
+ } catch (e) { /* ignore */ }
819
824
 
825
+ let prompt = ''
826
+ try {
827
+ const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
828
+ prompt = raw ? (JSON.parse(raw).prompt || '') : ''
829
+ } catch (e) { prompt = '' }
830
+
831
+ const repeatText = (() => {
832
+ const parts = [String(args.prompt || '').trim()]
833
+ const size = args.image_size || args.size
834
+ const seed = args.seed
835
+ if (size) parts.push(`Размер: ${size}`)
836
+ if (seed !== undefined) parts.push(`Сид: ${seed}`)
837
+ return parts.filter(Boolean).join(', ')
838
+ })()
839
+
840
+ const [editing, setEditing] = react.useState(false)
841
+ const [inpaintOpen, setInpaintOpen] = react.useState(false)
820
842
  const [compareSlider, setCompareSlider] = react.useState(50)
821
843
  const [showCompare, setShowCompare] = react.useState(false)
844
+ const [editText, setEditText] = react.useState('')
822
845
 
823
846
  const onEditPrompt = () => {
824
847
  const textToInsert = prompt || repeatText || ''
@@ -883,12 +906,7 @@ window.__ModuleLoader__.load({
883
906
  sendActionPrompt(`Удали фон у этого изображения: remove_background(image: "${ref}")`)
884
907
  }
885
908
 
886
- const [editText, setEditText] = react.useState('')
887
- let prompt = ''
888
- try {
889
- const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
890
- prompt = raw ? (JSON.parse(raw).prompt || '') : ''
891
- } catch (e) { prompt = '' }
909
+
892
910
 
893
911
  const head = react.createElement('div', { className: 'fal-head' },
894
912
  running ? 'Генерирую изображение…' : failed ? 'Генерация не удалась' : 'Изображение')
@@ -897,19 +915,7 @@ window.__ModuleLoader__.load({
897
915
  // промптом и параметрами, извлечёнными из исходного вызова инструмента.
898
916
  // Дальше модель сама решает, как сгенерировать, — это просто повторный
899
917
  // запрос, а не вызов инструмента из карточки.
900
- let args = {}
901
- try {
902
- const raw = (block && (block.call ? block.call.argsRaw : block.argsRaw)) || ''
903
- if (raw) args = JSON.parse(raw)
904
- } catch (e) { /* ignore */ }
905
- const repeatText = (() => {
906
- const parts = [String(args.prompt || '').trim()]
907
- const size = args.image_size || args.size
908
- const seed = args.seed
909
- if (size) parts.push(`Размер: ${size}`)
910
- if (seed !== undefined) parts.push(`Сид: ${seed}`)
911
- return parts.filter(Boolean).join(', ')
912
- })()
918
+
913
919
  const repeat = async () => {
914
920
  try {
915
921
  const sessions = props.sessions
package/lib/index.js CHANGED
@@ -16,10 +16,14 @@ import z from '@deepseek-ai/schemastery'
16
16
  import { defineTool } from '@deepseek-ai/dsh-tools'
17
17
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
18
18
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
19
+ import { existsSync, unlinkSync, accessSync } from 'node:fs'
19
20
  import os from 'node:os'
20
21
  import path from 'node:path'
21
22
  import {
22
23
  computeGenerationHash,
24
+ clampProviderCount,
25
+ buildEndpointUrl,
26
+ createAbortError,
23
27
  IMAGE_SIZES,
24
28
  OUTPUT_FORMATS,
25
29
  PROVIDER_KEYS,
@@ -54,9 +58,13 @@ const NS = 'dsh-image-gen'
54
58
  // настройки лежат под старым именем. Оно читается как запасное и переносится
55
59
  // под новое имя один раз — молча, при первом запуске после обновления.
56
60
  const LEGACY_NS = 'dsh-fal-image-gen'
57
- export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm']
61
+ export const inject = ['tools', 'attachments', 'credentials', 'webServer', 'settings', 'llm', 'systemPrompt']
58
62
 
59
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),
60
68
  provider: z
61
69
  .string()
62
70
  .description(`Which provider generates the image. One of: ${PROVIDER_KEYS.join(', ')}. `
@@ -68,6 +76,7 @@ export const Config = z.object({
68
76
  .default('fal-ai/flux-2/klein/9b'),
69
77
  apiKeyEnv: z
70
78
  .string()
79
+ .role('credential-ref')
71
80
  .description('Credential reference / env var holding the FAL API key (the "Key " auth prefix is added automatically when missing).')
72
81
  .default('FAL_API_KEY'),
73
82
  baseURL: z
@@ -109,6 +118,7 @@ export const Config = z.object({
109
118
  .default(''),
110
119
  customKeyEnv: z
111
120
  .string()
121
+ .role('credential-ref')
112
122
  .description('provider=custom: credential reference / env var holding the API key. '
113
123
  + 'Empty means no authorization header, for gateways that need none.')
114
124
  .default('OPENAI_API_KEY'),
@@ -119,6 +129,7 @@ export const Config = z.object({
119
129
  .default('black-forest-labs/flux-schnell'),
120
130
  replicateKeyEnv: z
121
131
  .string()
132
+ .role('credential-ref')
122
133
  .description('provider=replicate: credential reference / env var holding API token.')
123
134
  .default('REPLICATE_API_TOKEN'),
124
135
 
@@ -154,6 +165,7 @@ export const Config = z.object({
154
165
  .default(7),
155
166
  seedreamKeyEnv: z
156
167
  .string()
168
+ .role('credential-ref')
157
169
  .description('provider=seedream: credential reference / env var holding the API key.')
158
170
  .default('SEEDREAM_API_KEY'),
159
171
  seedreamModel: z
@@ -162,6 +174,7 @@ export const Config = z.object({
162
174
  .default('seedream-4.0'),
163
175
  geminiKeyEnv: z
164
176
  .string()
177
+ .role('credential-ref')
165
178
  .description('provider=gemini: credential reference / env var holding the Google API key.')
166
179
  .default('GEMINI_API_KEY'),
167
180
  geminiModel: z
@@ -217,7 +230,12 @@ export async function resolveSource(ctx, exec, ref) {
217
230
  }
218
231
  const p = path.resolve(sessionCwd || process.cwd(), ref)
219
232
  const bytes = await readFile(p)
220
- return { bytes, mediaType: 'image/png' }
233
+ const ext = path.extname(p).toLowerCase()
234
+ const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
235
+ : ext === '.webp' ? 'image/webp'
236
+ : ext === '.svg' ? 'image/svg+xml'
237
+ : 'image/png'
238
+ return { bytes, mediaType }
221
239
  }
222
240
 
223
241
  export function slugify(input) {
@@ -295,7 +313,7 @@ export async function findCached(entries, seed, prompt) {
295
313
  /** Вернуть запись из кэша, если файл существует; иначе undefined. */
296
314
  export async function cachedResult(entry) {
297
315
  if (!entry || !entry.path) return undefined
298
- try { require('node:fs').accessSync(entry.path) } catch (e) { return undefined }
316
+ if (!entry.path || !existsSync(entry.path)) return undefined
299
317
  return {
300
318
  path: entry.path,
301
319
  url: entry.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(entry.attachmentId)}` : '',
@@ -326,8 +344,8 @@ export async function pruneHistory(entries, pruneDays) {
326
344
  for (const e of entries) {
327
345
  const created = e.createdAt ? Date.parse(e.createdAt) : NaN
328
346
  if (Number.isFinite(created) && created < cutoff) {
329
- try { require('node:fs').unlinkSync(e.path) } catch (err) { /* файл уже удалён */ }
330
- try { require('node:fs').unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
347
+ try { unlinkSync(e.path) } catch (err) { /* файл уже удалён */ }
348
+ try { unlinkSync(e.path.replace(/\.[^.]+$/, '.json')) } catch (err) { /* sidecar */ }
331
349
  continue
332
350
  }
333
351
  kept.push(e)
@@ -408,6 +426,17 @@ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
408
426
  }
409
427
 
410
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
+
411
440
 
412
441
  // The Web card edits this namespace; without registering it the card binds to
413
442
  // a namespace nobody declared, stays unready and renders nothing — which is
@@ -494,7 +523,7 @@ export function apply(ctx, config) {
494
523
  res.end(JSON.stringify({ error: 'GET only' }))
495
524
  return
496
525
  }
497
- const exists = (p) => { try { return require('node:fs').existsSync(p) } catch { return false } }
526
+ const exists = (p) => existsSync(p)
498
527
  const entries = await readHistory()
499
528
  const withThumbs = filterHistory(entries, exists).map((e) => ({
500
529
  ...e,
@@ -526,7 +555,7 @@ export function apply(ctx, config) {
526
555
  },
527
556
  aspect_ratio: {
528
557
  type: 'string',
529
- 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'],
530
559
  description: 'Optional aspect ratio; overrides image_size when both are given. Sent as width/height to providers that accept pixels.',
531
560
  },
532
561
  seed: {
@@ -542,6 +571,10 @@ export function apply(ctx, config) {
542
571
  type: 'string',
543
572
  description: 'Optional file name stem for the saved image (defaults to a slug of the prompt).',
544
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
+ },
545
578
  count: {
546
579
  type: 'integer',
547
580
  description: 'Number of variations to generate in one call, 1-4 (default 1). Cost scales with count.',
@@ -566,7 +599,7 @@ export function apply(ctx, config) {
566
599
  },
567
600
  quality: {
568
601
  type: 'string',
569
- enum: ['low', 'medium', 'high'],
602
+ enum: ['auto', 'low', 'medium', 'high'],
570
603
  description: 'Optional quality for providers that support it (seedream, gemini). Ignored otherwise.',
571
604
  },
572
605
  style: {
@@ -589,7 +622,7 @@ export function apply(ctx, config) {
589
622
  output: {
590
623
  schema: {
591
624
  type: 'object',
592
- additionalProperties: false,
625
+ additionalProperties: true,
593
626
  properties: {
594
627
  path: { type: 'string' },
595
628
  url: { type: 'string' },
@@ -630,7 +663,7 @@ export function apply(ctx, config) {
630
663
  },
631
664
  render(args, value) {
632
665
  const list = value.images || [value]
633
- 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}`)
634
667
  const summary = lines.join('\n')
635
668
  if (live().deliverAs !== 'image') {
636
669
  const urls = list.map((img) => img.url).filter(Boolean)
@@ -645,6 +678,9 @@ export function apply(ctx, config) {
645
678
  timeoutMs: config.timeoutMs + 30000,
646
679
  async execute(args, exec) {
647
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
+ }
648
684
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
649
685
  const deliverAs = cfg.deliverAs
650
686
  const size = args.aspect_ratio ? 'custom' : (args.image_size ?? cfg.defaultSize)
@@ -664,12 +700,14 @@ export function apply(ctx, config) {
664
700
  const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
665
701
  const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
666
702
  const enhanced = await enhancePrompt(ctx, cfg, args.prompt, exec.signal, provider)
667
- const styleSuffix = cfg.stylePreset ? `, ${cfg.stylePreset}` : ''
668
- const styledPrompt = args.style ? applyStylePreset(enhanced.prompt, args.style) : enhanced.prompt;
669
- const effectivePrompt = styledPrompt + styleSuffix
703
+ const effectiveStyle = args.style || cfg.stylePreset
704
+ const styleInfo = resolveStylePreset(effectiveStyle, args.negative_prompt, args.guidance_scale)
705
+ const effectivePrompt = styleInfo.promptSuffix ? `${enhanced.prompt}, ${styleInfo.promptSuffix}` : enhanced.prompt
706
+ const effectiveNegative = styleInfo.negativePrompt
707
+ const effectiveGuidance = styleInfo.guidanceScale
670
708
  const providers = makeProviders(
671
709
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
672
- { prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: args.negative_prompt, guidanceScale: args.guidance_scale, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels },
710
+ { 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 },
673
711
  )
674
712
  // Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
675
713
  // отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
@@ -703,7 +741,8 @@ export function apply(ctx, config) {
703
741
  })
704
742
 
705
743
  const sessionCwd = exec.agent?.session?.header?.cwd
706
- 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)
707
746
  await mkdir(outDir, { recursive: true })
708
747
  const filePath = path.join(outDir, name)
709
748
  await writeFile(filePath, bytes)
@@ -734,13 +773,28 @@ export function apply(ctx, config) {
734
773
  }), null, 2),
735
774
  )
736
775
 
776
+ const cacheHash = computeGenerationHash({
777
+ provider,
778
+ model: cfg.model,
779
+ prompt: promptArg,
780
+ seed: gen.seed,
781
+ size: args.image_size || cfg.defaultSize,
782
+ style: args.style || cfg.stylePreset,
783
+ })
737
784
  const entry = {
738
785
  path: filePath,
739
786
  prompt: promptArg,
740
787
  provider,
788
+ model: cfg.model,
741
789
  size,
742
790
  format,
743
791
  seed: gen.seed,
792
+ cacheHash,
793
+ thumbnailUrl: localUrl,
794
+ width: gen.width || attachment.width,
795
+ height: gen.height || attachment.height,
796
+ mediaType,
797
+ bytes: attachment.bytes,
744
798
  createdAt: new Date().toISOString(),
745
799
  attachmentId: attachment.attachmentId,
746
800
  }
@@ -817,7 +871,7 @@ export function apply(ctx, config) {
817
871
  }
818
872
  }
819
873
  const first = images[0]
820
- return { ...first, images }
874
+ return { channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images }
821
875
  },
822
876
  }),
823
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
  // Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
@@ -238,7 +267,7 @@ export async function pollStatus(fetchImpl, statusUrl, key, signal, pollInterval
238
267
  }, { once: true })
239
268
  }
240
269
  })
241
- if (signal.aborted) throw new Error('FAL generation cancelled')
270
+ if (signal?.aborted) throw new Error('FAL generation cancelled')
242
271
  const res = await fetchImpl(statusUrl, { headers: { Authorization: falAuthHeader(key) }, signal })
243
272
  const data = await res.json().catch(() => ({}))
244
273
  const status = data.status
@@ -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
  }
@@ -540,7 +578,7 @@ export function makeProviders(deps, job) {
540
578
  const deadline = Date.now() + cfg.timeoutMs
541
579
  let attempt = 0
542
580
  for (;;) {
543
- if (signal.aborted) throw new Error('Local ComfyUI generation cancelled')
581
+ if (signal?.aborted) throw new Error('Local ComfyUI generation cancelled')
544
582
  if (Date.now() > deadline) throw new Error(`Local ComfyUI timed out after ${cfg.timeoutMs} ms`)
545
583
  const cDelay = calculateBackoff(attempt++, cfg.pollIntervalMs || 1000, 5000)
546
584
  await new Promise((resolve) => {
@@ -663,7 +701,7 @@ export function makeProviders(deps, job) {
663
701
  const deadline = Date.now() + (cfg.timeoutMs || 180000)
664
702
  let attempt = 0
665
703
  while (pred.status !== 'succeeded') {
666
- if (signal.aborted) throw new Error('Replicate generation cancelled')
704
+ if (signal?.aborted) throw new Error('Replicate generation cancelled')
667
705
  if (Date.now() > deadline) throw new Error('Replicate generation timed out')
668
706
  if (pred.status === 'failed' || pred.status === 'canceled') {
669
707
  throw new Error(`Replicate failed: ${pred.error || 'unknown error'}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.0",
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",