@goodandready/dsh-image-gen 0.10.5 → 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 +7 -0
- package/lib/index.js +24 -18
- package/lib/providers.js +142 -5
- package/package.json +1 -1
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,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
applyStylePreset,
|
|
46
47
|
resolveStylePreset,
|
|
47
48
|
blendImagesFal,
|
|
49
|
+
toLosslessJson,
|
|
48
50
|
} from './providers.js'
|
|
49
51
|
|
|
50
52
|
import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from './resolve-image.js'
|
|
@@ -131,7 +133,7 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
|
131
133
|
attachmentId: attachment.attachmentId,
|
|
132
134
|
})
|
|
133
135
|
|
|
134
|
-
return {
|
|
136
|
+
return toLosslessJson({
|
|
135
137
|
summary,
|
|
136
138
|
path: filePath,
|
|
137
139
|
url: deliverAs === 'image' && sourceUrl ? sourceUrl : localUrl,
|
|
@@ -149,7 +151,7 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
|
|
|
149
151
|
height: attachment.height,
|
|
150
152
|
name: attachment.name,
|
|
151
153
|
},
|
|
152
|
-
}
|
|
154
|
+
})
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
export const name = 'dsh-image-gen'
|
|
@@ -353,14 +355,18 @@ export function slugify(input) {
|
|
|
353
355
|
/** Resolve an API key for one call (credentials service first, then env). */
|
|
354
356
|
export async function resolveApiKey(ctx, ref) {
|
|
355
357
|
if (!ref) return ''
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
361
369
|
}
|
|
362
|
-
const fromEnv = process.env[ref]
|
|
363
|
-
if (fromEnv) return fromEnv
|
|
364
370
|
throw new Error(
|
|
365
371
|
`API key not configured: set credential/env "${ref}" (Web: Settings → Credentials, or add "${ref}: <key>" to $DSH_HOME/.credentials.yaml)`,
|
|
366
372
|
)
|
|
@@ -812,7 +818,7 @@ export function apply(ctx, config) {
|
|
|
812
818
|
const effectiveGuidance = styleInfo.guidanceScale
|
|
813
819
|
const providers = makeProviders(
|
|
814
820
|
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
|
|
815
|
-
{ 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 },
|
|
816
822
|
)
|
|
817
823
|
// Подписочные провайдеры (codex/grok) отдают {ok:false, reason} вместо исключения:
|
|
818
824
|
// отказ должен дойти до модели текстом. Без проверки execute шёл дальше с пустыми
|
|
@@ -916,7 +922,7 @@ export function apply(ctx, config) {
|
|
|
916
922
|
height: gen.height || attachment.height,
|
|
917
923
|
seed: gen.seed,
|
|
918
924
|
prompt: promptArg,
|
|
919
|
-
|
|
925
|
+
...(enhanced.enhanced ? { originalPrompt: args.prompt } : {}),
|
|
920
926
|
cost: gen.cost,
|
|
921
927
|
format: mediaType.replace('image/', ''),
|
|
922
928
|
attachment: {
|
|
@@ -988,7 +994,7 @@ export function apply(ctx, config) {
|
|
|
988
994
|
cost: first.cost,
|
|
989
995
|
attachmentId: first.attachment?.attachmentId || 'N/A',
|
|
990
996
|
})
|
|
991
|
-
return { summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images }
|
|
997
|
+
return toLosslessJson({ summary: dualOutputSummary, channel: provider, provider, model: cfg.model || cfg.customModel, ...first, images })
|
|
992
998
|
},
|
|
993
999
|
}),
|
|
994
1000
|
)
|
|
@@ -1288,11 +1294,11 @@ export function apply(ctx, config) {
|
|
|
1288
1294
|
throw new Error(`All aspect ratio generations failed in pack: ${warnings.join('; ')}`)
|
|
1289
1295
|
}
|
|
1290
1296
|
|
|
1291
|
-
return {
|
|
1297
|
+
return toLosslessJson({
|
|
1292
1298
|
images: results,
|
|
1293
1299
|
count: results.length,
|
|
1294
|
-
warnings
|
|
1295
|
-
}
|
|
1300
|
+
...(warnings.length ? { warnings } : {}),
|
|
1301
|
+
})
|
|
1296
1302
|
},
|
|
1297
1303
|
}),
|
|
1298
1304
|
)
|
|
@@ -1414,7 +1420,7 @@ export function apply(ctx, config) {
|
|
|
1414
1420
|
if (vision.summary) {
|
|
1415
1421
|
result.visionAnalysis = vision.summary
|
|
1416
1422
|
}
|
|
1417
|
-
return result
|
|
1423
|
+
return toLosslessJson(result)
|
|
1418
1424
|
},
|
|
1419
1425
|
}),
|
|
1420
1426
|
)
|
|
@@ -1535,10 +1541,10 @@ export function apply(ctx, config) {
|
|
|
1535
1541
|
}
|
|
1536
1542
|
|
|
1537
1543
|
const first = results[0]
|
|
1538
|
-
return {
|
|
1544
|
+
return toLosslessJson({
|
|
1539
1545
|
...first,
|
|
1540
1546
|
variations: results.map((r) => ({ path: r.path, attachmentId: r.attachment?.attachmentId, seed: r.seed })),
|
|
1541
|
-
}
|
|
1547
|
+
})
|
|
1542
1548
|
},
|
|
1543
1549
|
}),
|
|
1544
1550
|
)
|
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
|
|
242
|
+
throw new Error(`Client error on provider ${key} (not retrying fallback): ${formatErrorMessage(e, key)}`)
|
|
157
243
|
}
|
|
158
|
-
refusals.push(
|
|
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:
|
|
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:
|
|
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) {
|
|
@@ -983,3 +1097,26 @@ export async function varyImageDirect(deps, job) {
|
|
|
983
1097
|
if (!fn) throw new Error(`Provider "${providerKey}" does not support image variations`)
|
|
984
1098
|
return fn(job.seed, job.prompt)
|
|
985
1099
|
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Recursively removes any property with an undefined value so the result satisfies
|
|
1103
|
+
* DSH lossless JSON requirements (dsh-util-values isJsonValue).
|
|
1104
|
+
*
|
|
1105
|
+
* @param {any} val
|
|
1106
|
+
* @returns {any}
|
|
1107
|
+
*/
|
|
1108
|
+
export function toLosslessJson(val) {
|
|
1109
|
+
if (val === null || typeof val === 'boolean' || typeof val === 'string') return val
|
|
1110
|
+
if (typeof val === 'number') return Number.isFinite(val) && !Object.is(val, -0) ? val : null
|
|
1111
|
+
if (Array.isArray(val)) return val.map(toLosslessJson)
|
|
1112
|
+
if (typeof val === 'object' && val !== null) {
|
|
1113
|
+
const res = {}
|
|
1114
|
+
for (const [k, v] of Object.entries(val)) {
|
|
1115
|
+
if (v !== undefined) {
|
|
1116
|
+
res[k] = toLosslessJson(v)
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return res
|
|
1120
|
+
}
|
|
1121
|
+
return null
|
|
1122
|
+
}
|
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.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",
|