@goodandready/dsh-image-gen 0.10.6 → 0.10.7

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/README.md CHANGED
@@ -70,6 +70,13 @@
70
70
  - **React Cleanup (#139)**: removed dead state hooks from `FalImageCard`.
71
71
  - **Complete Localization (#140)**: eliminated hardcoded strings, wiring comprehensive dictionaries for en, ru, and zh.
72
72
 
73
+
74
+ ### 🚀 What's New in v0.10.7 (#197)
75
+ * **Robust Error Formatting**: completely prevents `[object Object]` from appearing in provider refusal chains, extracting deep `.message`, `.detail`, and `.error` objects cleanly.
76
+ * **Deduplicated Provider Prefixes**: eliminates redundant `codex: codex: ...` prefixes.
77
+ * **FAL Credential Aliasing**: seamless fallback between `FAL_API_KEY` and `FAL_KEY` in credentials and environment.
78
+ * **Subscription Aspect Ratio Mapping**: maps aspect ratios (`16:9`, `3:2`, `9:16`, `2:3`) to appropriate subscription dimensions (`1536x1024` / `1024x1536`) instead of falling back to default square `1024x1024`.
79
+
73
80
  ## 🎨 Supported Generation Backends
74
81
 
75
82
  * **`fal`** (Default): FAL.ai queue for FLUX.1, SDXL, Clarity Upscaler, and BiRefNet.
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  pixelDiff,
34
34
  normalizeCount,
35
35
  tryGenerate,
36
+ resolveApiKeyCandidates,
36
37
  fallbackOrder,
37
38
  normalizeMediaType,
38
39
  embedPngMetadata,
@@ -354,14 +355,18 @@ export function slugify(input) {
354
355
  /** Resolve an API key for one call (credentials service first, then env). */
355
356
  export async function resolveApiKey(ctx, ref) {
356
357
  if (!ref) return ''
357
- try {
358
- const resolved = await ctx.credentials.resolve(credentialRef(ref))
359
- if (resolved && resolved.value) return resolved.value
360
- } catch {
361
- // fall through to the environment
358
+ const candidates = resolveApiKeyCandidates(ref)
359
+
360
+ for (const candidate of candidates) {
361
+ try {
362
+ const resolved = await ctx.credentials.resolve(credentialRef(candidate))
363
+ if (resolved && resolved.value) return resolved.value
364
+ } catch {
365
+ // fall through to the environment
366
+ }
367
+ const fromEnv = process.env[candidate]
368
+ if (fromEnv) return fromEnv
362
369
  }
363
- const fromEnv = process.env[ref]
364
- if (fromEnv) return fromEnv
365
370
  throw new Error(
366
371
  `API key not configured: set credential/env "${ref}" (Web: Settings → Credentials, or add "${ref}: <key>" to $DSH_HOME/.credentials.yaml)`,
367
372
  )
@@ -813,7 +818,7 @@ export function apply(ctx, config) {
813
818
  const effectiveGuidance = styleInfo.guidanceScale
814
819
  const providers = makeProviders(
815
820
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
816
- { 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 },
821
+ { 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 },
817
822
  )
818
823
  // Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
819
824
  // отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
package/lib/providers.js CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Возвращает список кандидатов для имени ключа API с учётом известных алиасов (напр. FAL_API_KEY <-> FAL_KEY).
3
+ */
4
+ export function resolveApiKeyCandidates(ref) {
5
+ if (!ref) return []
6
+ const candidates = [ref]
7
+ if (ref === 'FAL_API_KEY') candidates.push('FAL_KEY')
8
+ else if (ref === 'FAL_KEY') candidates.push('FAL_API_KEY')
9
+ return candidates
10
+ }
11
+
1
12
 
2
13
  export const PROVIDER_MAX_COUNTS = {
3
14
  fal: 4,
@@ -145,6 +156,81 @@ export function isFatalClientError(error) {
145
156
  )
146
157
  }
147
158
 
159
+ /**
160
+ * Safely extract a human-readable message from any error, object, or response.
161
+ * Completely eliminates "[object Object]" and prevents duplicate provider prefixes (e.g. "codex: codex: ...").
162
+ */
163
+ export function formatErrorMessage(e, providerKey = '') {
164
+ if (e === null || e === undefined) {
165
+ return providerKey ? `${providerKey}: unknown error` : 'unknown error'
166
+ }
167
+
168
+ let msg = ''
169
+ if (typeof e === 'string') {
170
+ msg = e
171
+ } else if (typeof e === 'object') {
172
+ if (typeof e.message === 'string' && e.message && e.message !== '[object Object]') {
173
+ msg = e.message
174
+ } else if (typeof e.detail === 'string' && e.detail) {
175
+ msg = e.detail
176
+ } else if (e.error) {
177
+ if (typeof e.error === 'string') msg = e.error
178
+ else if (typeof e.error.message === 'string') msg = e.error.message
179
+ else if (typeof e.error.detail === 'string') msg = e.error.detail
180
+ else {
181
+ try { msg = JSON.stringify(e.error) } catch {}
182
+ }
183
+ } else if (typeof e.statusText === 'string' && e.statusText) {
184
+ msg = `HTTP ${e.status || ''} ${e.statusText}`.trim()
185
+ } else if (typeof e.cause === 'string') {
186
+ msg = e.cause
187
+ } else if (e.cause && typeof e.cause.message === 'string') {
188
+ msg = e.cause.message
189
+ }
190
+
191
+ if (!msg || msg === '[object Object]') {
192
+ try {
193
+ const json = JSON.stringify(e)
194
+ if (json && json !== '{}') {
195
+ msg = json.slice(0, 500)
196
+ }
197
+ } catch {}
198
+ }
199
+
200
+ if (!msg || msg === '[object Object]') {
201
+ const keys = Object.getOwnPropertyNames(e)
202
+ if (keys.length) {
203
+ msg = `error [${keys.join(', ')}]`
204
+ } else {
205
+ msg = String(e)
206
+ }
207
+ }
208
+ } else {
209
+ msg = String(e)
210
+ }
211
+
212
+ msg = String(msg || 'unknown error').trim()
213
+
214
+ if (providerKey) {
215
+ const prefixRegex = new RegExp(`^${providerKey}\\s*:\\s*`, 'i')
216
+ while (prefixRegex.test(msg)) {
217
+ msg = msg.replace(prefixRegex, '').trim()
218
+ }
219
+ }
220
+
221
+ if (!msg || msg === '[object Object]') {
222
+ msg = 'unknown error object'
223
+ } else if (msg.includes('[object Object]')) {
224
+ msg = msg.replace(/\[object Object\]/g, 'unknown error object').trim()
225
+ }
226
+
227
+ if (providerKey) {
228
+ return `${providerKey}: ${msg}`
229
+ }
230
+
231
+ return msg
232
+ }
233
+
148
234
  export async function tryGenerate(generators, order, seed, promptArg) {
149
235
  const refusals = []
150
236
  for (const key of order) {
@@ -153,9 +239,9 @@ export async function tryGenerate(generators, order, seed, promptArg) {
153
239
  return produced
154
240
  } catch (e) {
155
241
  if (isFatalClientError(e)) {
156
- throw new Error(`Client error on provider ${key} (not retrying fallback): ${e.message || String(e)}`)
242
+ throw new Error(`Client error on provider ${key} (not retrying fallback): ${formatErrorMessage(e, key)}`)
157
243
  }
158
- refusals.push(`${key}: ${e.message || String(e)}`)
244
+ refusals.push(formatErrorMessage(e, key))
159
245
  }
160
246
  }
161
247
  throw new Error(`Image generation failed on all providers. ${refusals.join('; ')}`)
@@ -169,6 +255,34 @@ export function normalizeCount(count, max = 4) {
169
255
 
170
256
  // Размеры, которыми оперируют подписки, — свой набор, не похожий на именованные
171
257
  // размеры FAL. Перевод один к одному по смыслу: квадрат, вертикаль, горизонталь.
258
+ /**
259
+ * Разрешает размер для подписочных сервисов (codex / grok).
260
+ * Понимает именованные размеры (landscape_16_9 и др.), пропорции (16:9, 3:2, 9:16 и др.)
261
+ * и размеры в пикселях [width, height], исключая дефолтный квадрат при запросе соотношений сторон.
262
+ */
263
+ export function resolveSubscriptionSize(size, aspectPixels, aspectRatio) {
264
+ if (size && SUBSCRIPTION_SIZES[size]) {
265
+ return SUBSCRIPTION_SIZES[size]
266
+ }
267
+ const ratio = String(aspectRatio || '').trim()
268
+ if (ratio === '16:9' || ratio === '3:2' || ratio === '4:3') {
269
+ return '1536x1024'
270
+ }
271
+ if (ratio === '9:16' || ratio === '2:3' || ratio === '3:4') {
272
+ return '1024x1536'
273
+ }
274
+ if (ratio === '1:1') {
275
+ return '1024x1024'
276
+ }
277
+ if (Array.isArray(aspectPixels) && aspectPixels.length === 2) {
278
+ const [w, h] = aspectPixels
279
+ if (w > h) return '1536x1024'
280
+ if (h > w) return '1024x1536'
281
+ return '1024x1024'
282
+ }
283
+ return '1024x1024'
284
+ }
285
+
172
286
  export const SUBSCRIPTION_SIZES = {
173
287
  square_hd: '1024x1024',
174
288
  square: '1024x1024',
@@ -349,7 +463,7 @@ export async function pixelDiff(a, b) {
349
463
 
350
464
  export function makeProviders(deps, job) {
351
465
  const { fetchImpl, resolveKey, cfg } = deps
352
- const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels } = job
466
+ const { prompt, size, format, seed, signal, negativePrompt, guidanceScale, source, mask, strength, quality, style, aspectPixels, aspectRatio } = job
353
467
 
354
468
  async function fal(seedArg = seed, promptArg = prompt) {
355
469
  const key = await resolveKey(cfg.apiKeyEnv)
@@ -498,12 +612,12 @@ export function makeProviders(deps, job) {
498
612
  produced = await images.generate({
499
613
  provider,
500
614
  prompt: promptArg,
501
- size: SUBSCRIPTION_SIZES[size] || '1024x1024',
615
+ size: resolveSubscriptionSize(size, aspectPixels, aspectRatio),
502
616
  quality: cfg.subscriptionQuality || undefined,
503
617
  signal,
504
618
  })
505
619
  } catch (e) {
506
- return { ok: false, provider, reason: `${provider}: ${String(e && e.message || e)}` }
620
+ return { ok: false, provider, reason: formatErrorMessage(e, provider) }
507
621
  }
508
622
  const first = Array.isArray(produced) ? produced[0] : null
509
623
  if (!first || !first.b64_json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
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",