@goodandready/dsh-image-gen 0.11.30 → 0.11.32
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 +2 -2
- package/lib/config-schema.js +1 -1
- package/lib/cost-meter.js +74 -7
- package/lib/frontend-assets.js +57 -0
- package/lib/generation-helpers.js +36 -11
- package/lib/history.js +32 -0
- package/lib/provider-utils.js +5 -2
- package/lib/providers/backends/fal.js +10 -0
- package/lib/providers/backends/gemini.js +17 -9
- package/lib/providers/backends/local.js +18 -0
- package/lib/providers.js +2 -1
- package/lib/quality-gate.js +19 -2
- package/lib/tools/character-sheet.js +19 -11
- package/lib/tools/diagram.js +19 -11
- package/lib/tools/generation.js +56 -14
- package/lib/tools/processing-basic.js +143 -110
- package/lib/tools/style-matrix.js +11 -4
- package/lib/tools/theme-pair.js +12 -5
- package/lib/tools/ui-asset.js +15 -7
- package/lib/tools/vision-ocr.js +19 -11
- package/package.json +2 -2
package/lib/tools/generation.js
CHANGED
|
@@ -2,6 +2,7 @@ import { buildPresentationMeta } from '../attachment-helper.js'
|
|
|
2
2
|
import { registerGenerationPackTool } from './generation-pack.js'
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
7
8
|
computeGenerationHash,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
} from '../providers.js'
|
|
16
17
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
17
18
|
import { executeWithQualityGate } from '../quality-gate.js'
|
|
18
|
-
import { calculateGenerationCost, assertBudgetAvailable } from '../cost-meter.js'
|
|
19
|
+
import { calculateGenerationCost, assertBudgetAvailable, reserveSpend } from '../cost-meter.js'
|
|
19
20
|
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
20
21
|
import { getCachedGeneration } from '../generation-cache.js'
|
|
21
22
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
@@ -316,8 +317,11 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
316
317
|
let subscriptionImages
|
|
317
318
|
try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
|
|
318
319
|
|
|
319
|
-
const
|
|
320
|
-
const
|
|
320
|
+
const sourceInput = args.source_image || args.image
|
|
321
|
+
const source = sourceInput ? await resolveSource(ctx, exec, sourceInput) : undefined
|
|
322
|
+
|
|
323
|
+
const maskInput = args.mask || args.mask_image
|
|
324
|
+
const mask = maskInput ? await resolveSource(ctx, exec, maskInput) : undefined
|
|
321
325
|
|
|
322
326
|
const {
|
|
323
327
|
enhanced,
|
|
@@ -378,14 +382,28 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
378
382
|
async (s, p) => {
|
|
379
383
|
let totalAttempts = 0
|
|
380
384
|
let accumulatedCost = 0
|
|
381
|
-
// #353: Pipeline evaluates raw gen.bytes
|
|
385
|
+
// #353, #355: Pipeline reserves budget per attempt, evaluates raw gen.bytes, and commits spend per attempt
|
|
386
|
+
const perAttemptCost = Number(estimatedCost / totalCount) || 0.02
|
|
382
387
|
const rawGenerate = async (seedVal) => {
|
|
388
|
+
const reservation = reserveSpend(perAttemptCost, cfg.dailyBudgetUsd)
|
|
383
389
|
totalAttempts++
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
390
|
+
let committed = false
|
|
391
|
+
try {
|
|
392
|
+
const gen = await providers[k](seedVal, p)
|
|
393
|
+
guard(gen)
|
|
394
|
+
const attemptCost = Number(gen.cost) || perAttemptCost
|
|
395
|
+
accumulatedCost += attemptCost
|
|
396
|
+
reservation.commit(attemptCost, {
|
|
397
|
+
ctx,
|
|
398
|
+
meta: { provider: k, model: cfg.model || cfg.customModel, prompt: p, seed: seedVal },
|
|
399
|
+
})
|
|
400
|
+
committed = true
|
|
401
|
+
return gen
|
|
402
|
+
} finally {
|
|
403
|
+
if (!committed) {
|
|
404
|
+
reservation.release()
|
|
405
|
+
}
|
|
406
|
+
}
|
|
389
407
|
}
|
|
390
408
|
|
|
391
409
|
const { generated: acceptedGen, qualityReport } = await executeWithQualityGate(
|
|
@@ -398,7 +416,7 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
398
416
|
}
|
|
399
417
|
)
|
|
400
418
|
|
|
401
|
-
// Save the accepted artifact ONCE
|
|
419
|
+
// Save the accepted artifact ONCE (spend already committed per attempt)
|
|
402
420
|
const saved = await saveGeneratedImageArtifact({
|
|
403
421
|
ctx,
|
|
404
422
|
exec,
|
|
@@ -417,6 +435,11 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
417
435
|
slugify,
|
|
418
436
|
estimatedCost: accumulatedCost || estimatedCost,
|
|
419
437
|
totalCount,
|
|
438
|
+
recordSpend: false,
|
|
439
|
+
source,
|
|
440
|
+
mask,
|
|
441
|
+
resolvedFaceRef,
|
|
442
|
+
resolvedRefImage,
|
|
420
443
|
})
|
|
421
444
|
|
|
422
445
|
return {
|
|
@@ -437,7 +460,11 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
437
460
|
|
|
438
461
|
const effectiveModel = provider === 'custom'
|
|
439
462
|
? (cfg.customModel || cfg.model || 'dall-e-3')
|
|
440
|
-
: (cfg.model || cfg.customModel)
|
|
463
|
+
: (cfg.model || cfg.customModel || 'default')
|
|
464
|
+
|
|
465
|
+
const sourceBytes = source && source.bytes ? source.bytes : (args.image || args.source_image)
|
|
466
|
+
const maskBytes = mask && mask.bytes ? mask.bytes : (args.mask_image || args.mask)
|
|
467
|
+
const refBytes = (resolvedRefImage && resolvedRefImage.bytes) || (resolvedFaceRef && resolvedFaceRef.bytes) || args.reference_images || args.reference_image
|
|
441
468
|
|
|
442
469
|
const h = computeGenerationHash({
|
|
443
470
|
provider,
|
|
@@ -454,9 +481,9 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
454
481
|
negativePrompt: args.negative_prompt || cfg.negativePrompt,
|
|
455
482
|
quality: args.quality,
|
|
456
483
|
strength: args.strength,
|
|
457
|
-
sourceImage:
|
|
458
|
-
maskImage:
|
|
459
|
-
reference:
|
|
484
|
+
sourceImage: sourceBytes,
|
|
485
|
+
maskImage: maskBytes,
|
|
486
|
+
reference: refBytes,
|
|
460
487
|
})
|
|
461
488
|
|
|
462
489
|
// #170: Content-addressed disk cache check (<50ms return)
|
|
@@ -500,10 +527,25 @@ export function registerGenerationTools(ctx, deps, options = {}) {
|
|
|
500
527
|
const hPrompt = h
|
|
501
528
|
const found = findCachedGeneration(historyEntries, hPrompt)
|
|
502
529
|
if (found) return await cachedResult(found)
|
|
530
|
+
const srcHash = (source && source.bytes) ? createHash('sha256').update(source.bytes).digest('hex') : undefined
|
|
531
|
+
const mskHash = (mask && mask.bytes) ? createHash('sha256').update(mask.bytes).digest('hex') : undefined
|
|
532
|
+
const rfHash = (resolvedRefImage && resolvedRefImage.bytes)
|
|
533
|
+
? createHash('sha256').update(resolvedRefImage.bytes).digest('hex')
|
|
534
|
+
: ((resolvedFaceRef && resolvedFaceRef.bytes) ? createHash('sha256').update(resolvedFaceRef.bytes).digest('hex') : undefined)
|
|
535
|
+
|
|
503
536
|
const requestedCriteria = {
|
|
537
|
+
provider,
|
|
538
|
+
model: effectiveModel,
|
|
539
|
+
cacheHash: h,
|
|
504
540
|
format: args.output_format || cfg.defaultFormat || 'png',
|
|
505
541
|
aspectRatio: args.aspect_ratio || cfg.defaultAspectRatio,
|
|
506
542
|
size: args.aspect_ratio ? 'custom' : (args.image_size || cfg.defaultSize),
|
|
543
|
+
quality: args.quality,
|
|
544
|
+
style: args.style,
|
|
545
|
+
strength: args.strength,
|
|
546
|
+
sourceHash: srcHash,
|
|
547
|
+
maskHash: mskHash,
|
|
548
|
+
refHash: rfHash,
|
|
507
549
|
}
|
|
508
550
|
if (cfg.cacheBySeed) {
|
|
509
551
|
const bySeed = await findCached(historyEntries, seedVal, promptVal, requestedCriteria)
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
saveAttachmentSafe,
|
|
16
16
|
} from '../providers.js'
|
|
17
17
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
18
|
-
import { assertBudgetAvailable, recordSpend } from '../cost-meter.js'
|
|
18
|
+
import { assertBudgetAvailable, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
19
19
|
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
20
20
|
import { sanitizeErrorAndLogs } from '../security.js'
|
|
21
21
|
|
|
@@ -55,39 +55,50 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
55
55
|
const source = await resolveSource(ctx, exec, args.image)
|
|
56
56
|
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
57
57
|
const cfg = live()
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
filePath,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
58
|
+
const resvNobg = reserveSpend(0.02, cfg.dailyBudgetUsd)
|
|
59
|
+
let committed = false
|
|
60
|
+
try {
|
|
61
|
+
const res = await removeBackgroundFal(
|
|
62
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
63
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec?.signal }
|
|
64
|
+
)
|
|
65
|
+
if (!res || !res.bytes) {
|
|
66
|
+
throw new Error('remove_background provider returned empty or invalid response')
|
|
67
|
+
}
|
|
68
|
+
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
69
|
+
const name = `${stem}.png`
|
|
70
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
71
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
72
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
73
|
+
await mkdir(outDir, { recursive: true })
|
|
74
|
+
const filePath = path.join(outDir, name)
|
|
75
|
+
await writeFile(filePath, res.bytes)
|
|
76
|
+
resvNobg.commit(0.02, { ctx, meta: { tool: 'remove_background', model: args.model || 'fal-ai/birefnet' } })
|
|
77
|
+
committed = true
|
|
78
|
+
const summary = buildDualOutputMarkdown({
|
|
79
|
+
action: 'background removed',
|
|
80
|
+
filePath,
|
|
81
|
+
width: res.width || attachment.width,
|
|
82
|
+
height: res.height || attachment.height,
|
|
83
|
+
mediaType: 'image/png',
|
|
84
|
+
provider: 'fal',
|
|
85
|
+
model: args.model || 'fal-ai/birefnet',
|
|
86
|
+
attachmentId: attachment.attachmentId,
|
|
87
|
+
})
|
|
88
|
+
return toLosslessJson({
|
|
89
|
+
summary,
|
|
90
|
+
path: filePath,
|
|
91
|
+
url: localUrl,
|
|
92
|
+
width: res.width || attachment.width,
|
|
93
|
+
height: res.height || attachment.height,
|
|
94
|
+
format: 'png',
|
|
95
|
+
attachment,
|
|
96
|
+
})
|
|
97
|
+
} finally {
|
|
98
|
+
if (!committed) {
|
|
99
|
+
resvNobg.release()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
91
102
|
} catch (err) {
|
|
92
103
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
93
104
|
}
|
|
@@ -126,39 +137,50 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
126
137
|
const source = await resolveSource(ctx, exec, args.image)
|
|
127
138
|
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
128
139
|
const cfg = live()
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
filePath,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
140
|
+
const resvUpscale = reserveSpend(0.025, cfg.dailyBudgetUsd)
|
|
141
|
+
let committed = false
|
|
142
|
+
try {
|
|
143
|
+
const res = await upscaleImageFal(
|
|
144
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
145
|
+
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec?.signal }
|
|
146
|
+
)
|
|
147
|
+
if (!res || !res.bytes) {
|
|
148
|
+
throw new Error('upscale_image provider returned empty or invalid response')
|
|
149
|
+
}
|
|
150
|
+
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
151
|
+
const name = `${stem}.png`
|
|
152
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
153
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
154
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
155
|
+
await mkdir(outDir, { recursive: true })
|
|
156
|
+
const filePath = path.join(outDir, name)
|
|
157
|
+
await writeFile(filePath, res.bytes)
|
|
158
|
+
resvUpscale.commit(0.025, { ctx, meta: { tool: 'upscale_image', scale: args.scale } })
|
|
159
|
+
committed = true
|
|
160
|
+
const summary = buildDualOutputMarkdown({
|
|
161
|
+
action: `upscaled (${args.scale || 2}x)`,
|
|
162
|
+
filePath,
|
|
163
|
+
width: res.width || attachment.width,
|
|
164
|
+
height: res.height || attachment.height,
|
|
165
|
+
mediaType: 'image/png',
|
|
166
|
+
provider: 'fal',
|
|
167
|
+
model: 'fal-ai/clarity-upscaler',
|
|
168
|
+
attachmentId: attachment.attachmentId,
|
|
169
|
+
})
|
|
170
|
+
return toLosslessJson({
|
|
171
|
+
summary,
|
|
172
|
+
path: filePath,
|
|
173
|
+
url: localUrl,
|
|
174
|
+
scale: args.scale || 2,
|
|
175
|
+
width: res.width || attachment.width,
|
|
176
|
+
height: res.height || attachment.height,
|
|
177
|
+
attachment,
|
|
178
|
+
})
|
|
179
|
+
} finally {
|
|
180
|
+
if (!committed) {
|
|
181
|
+
resvUpscale.release()
|
|
182
|
+
}
|
|
183
|
+
}
|
|
162
184
|
} catch (err) {
|
|
163
185
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
164
186
|
}
|
|
@@ -254,50 +276,61 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
254
276
|
const cfg = live()
|
|
255
277
|
const sessionId = exec?.agent?.session?.id || exec?.agent?.session?.header?.id || 'session'
|
|
256
278
|
trackAndAssertLoopGuard(sessionId, { limit: cfg.loopGuardLimit, prompt: args.prompt || '', exec })
|
|
257
|
-
assertBudgetAvailable(0.05, cfg.dailyBudgetUsd)
|
|
258
279
|
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
280
|
+
if (refs.length < 2) {
|
|
281
|
+
throw new Error('At least two images are required for blending')
|
|
282
|
+
}
|
|
283
|
+
const sources = []
|
|
284
|
+
for (const r of refs) {
|
|
285
|
+
const s = await resolveSource(ctx, exec, r)
|
|
286
|
+
if (s && s.bytes) sources.push(s)
|
|
287
|
+
}
|
|
288
|
+
if (sources.length < 2) {
|
|
289
|
+
throw new Error('At least two valid images are required to blend')
|
|
290
|
+
}
|
|
291
|
+
const resvBlend = reserveSpend(0.05, cfg.dailyBudgetUsd)
|
|
292
|
+
let committed = false
|
|
293
|
+
try {
|
|
294
|
+
const res = await blendImagesFal(
|
|
295
|
+
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
296
|
+
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec?.signal }
|
|
297
|
+
)
|
|
298
|
+
if (!res || !res.bytes) {
|
|
299
|
+
throw new Error('blend_images provider returned empty or invalid response')
|
|
300
|
+
}
|
|
301
|
+
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
302
|
+
const name = `${stem}.png`
|
|
303
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
304
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
305
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
306
|
+
await mkdir(outDir, { recursive: true })
|
|
307
|
+
const filePath = path.join(outDir, name)
|
|
308
|
+
await writeFile(filePath, res.bytes)
|
|
309
|
+
resvBlend.commit(0.05, { ctx, meta: { tool: 'blend_images' } })
|
|
310
|
+
committed = true
|
|
311
|
+
const summary = buildDualOutputMarkdown({
|
|
312
|
+
action: 'blended composition',
|
|
313
|
+
filePath,
|
|
314
|
+
width: res.width || attachment.width,
|
|
315
|
+
height: res.height || attachment.height,
|
|
316
|
+
mediaType: 'image/png',
|
|
317
|
+
provider: 'fal',
|
|
318
|
+
model: 'fal-ai/image-blend',
|
|
319
|
+
attachmentId: attachment.attachmentId,
|
|
320
|
+
})
|
|
321
|
+
return toLosslessJson({
|
|
322
|
+
summary,
|
|
323
|
+
path: filePath,
|
|
324
|
+
url: localUrl,
|
|
325
|
+
width: res.width || attachment.width,
|
|
326
|
+
height: res.height || attachment.height,
|
|
327
|
+
attachment,
|
|
328
|
+
})
|
|
329
|
+
} finally {
|
|
330
|
+
if (!committed) {
|
|
331
|
+
resvBlend.release()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
301
334
|
} catch (err) {
|
|
302
335
|
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
303
336
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
saveAttachmentSafe,
|
|
15
15
|
} from '../providers.js'
|
|
16
16
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
17
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
17
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
18
18
|
import {
|
|
19
19
|
normalizeMatrixStyles,
|
|
20
20
|
buildMatrixCellPrompt,
|
|
@@ -145,8 +145,9 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
145
145
|
size: cfg.defaultSize || '1024x1024',
|
|
146
146
|
count: cellCount,
|
|
147
147
|
})
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
149
|
+
let committed = false
|
|
150
|
+
try {
|
|
150
151
|
const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
151
152
|
const quadrantIds = ['A', 'B', 'C', 'D']
|
|
152
153
|
const matrixId = `matrix-${Date.now().toString(36)}`
|
|
@@ -187,7 +188,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
187
188
|
})
|
|
188
189
|
|
|
189
190
|
const cellCost = Number(gen.cost) || +(estimatedCost / cellCount).toFixed(4)
|
|
190
|
-
|
|
191
|
+
batchReservation.commit(cellCost, {
|
|
191
192
|
ctx,
|
|
192
193
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: cellPrompt, seed: cellSeed },
|
|
193
194
|
})
|
|
@@ -236,6 +237,7 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
236
237
|
cells,
|
|
237
238
|
})
|
|
238
239
|
|
|
240
|
+
committed = true
|
|
239
241
|
return toLosslessJson({
|
|
240
242
|
matrix_id: matrixId,
|
|
241
243
|
base_prompt: basePrompt,
|
|
@@ -244,6 +246,11 @@ export function registerStyleMatrixTools(ctx, deps) {
|
|
|
244
246
|
cells,
|
|
245
247
|
summary,
|
|
246
248
|
})
|
|
249
|
+
} finally {
|
|
250
|
+
if (!committed) {
|
|
251
|
+
batchReservation.release()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
247
254
|
},
|
|
248
255
|
}),
|
|
249
256
|
)
|
package/lib/tools/theme-pair.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
saveAttachmentSafe,
|
|
14
14
|
} from '../providers.js'
|
|
15
15
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
16
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
16
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
17
17
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
18
18
|
import {
|
|
19
19
|
THEME_STYLES,
|
|
@@ -153,8 +153,9 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
153
153
|
size: cfg.defaultSize || '1024x1024',
|
|
154
154
|
count: 2,
|
|
155
155
|
})
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
const batchReservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
157
|
+
let committed = false
|
|
158
|
+
try {
|
|
158
159
|
const chain = resolveFallbackChain(primaryProvider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
159
160
|
const baseSeed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 1000000)
|
|
160
161
|
|
|
@@ -202,7 +203,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
202
203
|
height: lightGen.height || aspectPixels[1],
|
|
203
204
|
})
|
|
204
205
|
const halfCost = +(estimatedCost / 2).toFixed(4)
|
|
205
|
-
|
|
206
|
+
batchReservation.commit(Number(lightGen.cost) || halfCost, {
|
|
206
207
|
ctx,
|
|
207
208
|
meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: lightPrompt },
|
|
208
209
|
})
|
|
@@ -245,7 +246,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
245
246
|
width: darkGen.width || aspectPixels[0],
|
|
246
247
|
height: darkGen.height || aspectPixels[1],
|
|
247
248
|
})
|
|
248
|
-
|
|
249
|
+
batchReservation.commit(Number(darkGen.cost) || halfCost, {
|
|
249
250
|
ctx,
|
|
250
251
|
meta: { provider: primaryProvider, model: cfg.model || cfg.customModel, prompt: darkPrompt },
|
|
251
252
|
})
|
|
@@ -277,6 +278,7 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
277
278
|
+ `- Dark: ${darkFilename} (seed: ${baseSeed})\n\n`
|
|
278
279
|
+ `\`\`\`html\n${html}\n\`\`\``
|
|
279
280
|
|
|
281
|
+
committed = true
|
|
280
282
|
return toLosslessJson({
|
|
281
283
|
summary,
|
|
282
284
|
prompt,
|
|
@@ -302,6 +304,11 @@ export function registerThemePairTools(ctx, deps) {
|
|
|
302
304
|
css_snippet: css,
|
|
303
305
|
attachment: lightAttachment,
|
|
304
306
|
})
|
|
307
|
+
} finally {
|
|
308
|
+
if (!committed) {
|
|
309
|
+
batchReservation.release()
|
|
310
|
+
}
|
|
311
|
+
}
|
|
305
312
|
},
|
|
306
313
|
}),
|
|
307
314
|
)
|
package/lib/tools/ui-asset.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
saveAttachmentSafe,
|
|
16
16
|
} from '../providers.js'
|
|
17
17
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
18
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
18
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
19
19
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
20
20
|
import {
|
|
21
21
|
UI_ASSET_TYPES,
|
|
@@ -168,14 +168,16 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
168
168
|
size: cfg.defaultSize || '1024x1024',
|
|
169
169
|
count: 1,
|
|
170
170
|
})
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
173
172
|
const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
|
|
174
173
|
const seedVal = args.seed ?? Math.floor(Math.random() * 100000)
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
let gen
|
|
176
|
+
let committed = false
|
|
177
|
+
try {
|
|
178
|
+
gen = await executeWithFallback(providers, chain, seedVal, synthesizedPrompt, {
|
|
179
|
+
logger: ctx.logger,
|
|
180
|
+
})
|
|
179
181
|
|
|
180
182
|
let finalBytes = gen.bytes
|
|
181
183
|
let mediaType = gen.mediaType || 'image/png'
|
|
@@ -211,10 +213,11 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
211
213
|
})
|
|
212
214
|
|
|
213
215
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
214
|
-
|
|
216
|
+
reservation.commit(effectiveCost, {
|
|
215
217
|
ctx,
|
|
216
218
|
meta: { provider, model: cfg.model || cfg.customModel, prompt: synthesizedPrompt, seed: seedVal },
|
|
217
219
|
})
|
|
220
|
+
committed = true
|
|
218
221
|
|
|
219
222
|
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
220
223
|
const targetDir = cfg.outputDir || 'generated/images'
|
|
@@ -269,6 +272,11 @@ export function registerUiAssetTools(ctx, deps) {
|
|
|
269
272
|
attachment,
|
|
270
273
|
_fallback: gen._fallback,
|
|
271
274
|
})
|
|
275
|
+
} finally {
|
|
276
|
+
if (!committed) {
|
|
277
|
+
reservation.release()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
272
280
|
},
|
|
273
281
|
}),
|
|
274
282
|
)
|
package/lib/tools/vision-ocr.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from '../providers.js'
|
|
13
13
|
import { buildDualOutputMarkdown } from '../resolve-image.js'
|
|
14
14
|
import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
|
|
15
|
-
import { assertBudgetAvailable, calculateGenerationCost, recordSpend } from '../cost-meter.js'
|
|
15
|
+
import { assertBudgetAvailable, calculateGenerationCost, reserveSpend, recordSpend } from '../cost-meter.js'
|
|
16
16
|
import { renderToolOutput } from '../attachment-helper.js'
|
|
17
17
|
import {
|
|
18
18
|
normalizeBbox,
|
|
@@ -252,15 +252,17 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
252
252
|
size: `${imgWidth}x${imgHeight}`,
|
|
253
253
|
count: 1,
|
|
254
254
|
})
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
255
|
+
const reservation = reserveSpend(estimatedCost, cfg.dailyBudgetUsd)
|
|
256
|
+
let gen
|
|
257
|
+
let committed = false
|
|
258
|
+
try {
|
|
259
|
+
gen = await executeWithFallback(
|
|
260
|
+
providers,
|
|
261
|
+
fallbackChain,
|
|
262
|
+
seed,
|
|
263
|
+
inpaintPrompt,
|
|
264
|
+
{ logger: ctx.logger },
|
|
265
|
+
)
|
|
264
266
|
|
|
265
267
|
const inpaintedBytes = gen.bytes
|
|
266
268
|
if (!inpaintedBytes) {
|
|
@@ -323,10 +325,11 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
323
325
|
})
|
|
324
326
|
|
|
325
327
|
const effectiveCost = Number(gen.cost) || estimatedCost
|
|
326
|
-
|
|
328
|
+
reservation.commit(effectiveCost, {
|
|
327
329
|
ctx,
|
|
328
330
|
meta: { provider: usedProvider, model: cfg.model || cfg.customModel, prompt: inpaintPrompt, seed },
|
|
329
331
|
})
|
|
332
|
+
committed = true
|
|
330
333
|
|
|
331
334
|
const dualOutput = buildDualOutputMarkdown({
|
|
332
335
|
action: 'generated',
|
|
@@ -359,6 +362,11 @@ export function registerVisionOcrTools(ctx, deps) {
|
|
|
359
362
|
}
|
|
360
363
|
|
|
361
364
|
return toLosslessJson(response)
|
|
365
|
+
} finally {
|
|
366
|
+
if (!committed) {
|
|
367
|
+
reservation.release()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
362
370
|
},
|
|
363
371
|
})
|
|
364
372
|
)
|