@blockrun/franklin 3.15.48 → 3.15.49
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/dist/tools/imagegen.d.ts +29 -0
- package/dist/tools/imagegen.js +59 -36
- package/package.json +1 -1
package/dist/tools/imagegen.d.ts
CHANGED
|
@@ -32,3 +32,32 @@ export interface ImageGenDeps {
|
|
|
32
32
|
export declare function createImageGenCapability(deps?: ImageGenDeps): CapabilityHandler;
|
|
33
33
|
/** Back-compat static capability for callers that don't want the Content bridge. */
|
|
34
34
|
export declare const imageGenCapability: CapabilityHandler;
|
|
35
|
+
export interface ImagePollBody {
|
|
36
|
+
data?: {
|
|
37
|
+
b64_json?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
revised_prompt?: string;
|
|
40
|
+
}[];
|
|
41
|
+
error?: unknown;
|
|
42
|
+
status?: string;
|
|
43
|
+
}
|
|
44
|
+
export type ImagePollOutcome = {
|
|
45
|
+
kind: 'completed';
|
|
46
|
+
body: ImagePollBody;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'failed';
|
|
49
|
+
error?: unknown;
|
|
50
|
+
} | {
|
|
51
|
+
kind: 'timed_out';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'poll_http_error';
|
|
54
|
+
status: number;
|
|
55
|
+
bodyPreview: string;
|
|
56
|
+
};
|
|
57
|
+
export interface PollImageJobOptions {
|
|
58
|
+
/** Total wall-clock ceiling. Defaults to 5 min (matches videogen scale). */
|
|
59
|
+
maxWaitMs?: number;
|
|
60
|
+
/** Sleep between polls. Defaults to 3 s. */
|
|
61
|
+
intervalMs?: number;
|
|
62
|
+
}
|
|
63
|
+
export declare function pollImageJob(pollEndpoint: string, headers: Record<string, string>, signal: AbortSignal, options?: PollImageJobOptions): Promise<ImagePollOutcome>;
|
package/dist/tools/imagegen.js
CHANGED
|
@@ -264,11 +264,9 @@ function buildExecute(deps) {
|
|
|
264
264
|
// when the upstream image model takes longer than the inline budget
|
|
265
265
|
// (gpt-image-1/-2 routinely exceed 30s). Verified 2026-05-04 from
|
|
266
266
|
// Cloud Run logs — five back-to-back ImageGen calls that the agent
|
|
267
|
-
// saw as "No image data returned from API" had all returned 202
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// (each charged) and the image silently completes elsewhere with
|
|
271
|
-
// no way for Franklin to retrieve it. Mirror videogen.ts's
|
|
267
|
+
// saw as "No image data returned from API" had all returned 202;
|
|
268
|
+
// 4 of 5 actually completed in GCS within 41–56s and would have
|
|
269
|
+
// been retrievable if Franklin had polled. Mirror videogen.ts's
|
|
272
270
|
// pollUntilReady contract: same x-payment header on each poll.
|
|
273
271
|
if (response.status === 202 && result.poll_url) {
|
|
274
272
|
const origin = new URL(apiUrl).origin;
|
|
@@ -280,44 +278,24 @@ function buildExecute(deps) {
|
|
|
280
278
|
// generation routinely completes within 1–3 min once queued; the
|
|
281
279
|
// 5 min ceiling matches videogen's POLL_MAX_WAIT_MS scale.
|
|
282
280
|
clearTimeout(timeout);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const pollResp = await fetch(pollEndpoint, {
|
|
290
|
-
method: 'GET',
|
|
291
|
-
headers: pollHeaders,
|
|
292
|
-
signal: controller.signal,
|
|
293
|
-
});
|
|
294
|
-
if (pollResp.status === 202)
|
|
295
|
-
continue; // still queued
|
|
296
|
-
if (pollResp.status === 429 || pollResp.status >= 500)
|
|
297
|
-
continue; // transient
|
|
298
|
-
if (pollResp.ok) {
|
|
299
|
-
polled = await pollResp.json().catch(() => null);
|
|
300
|
-
if (polled && (polled.status === 'completed' || polled.data?.[0]))
|
|
301
|
-
break;
|
|
302
|
-
if (polled && polled.status === 'failed') {
|
|
303
|
-
return {
|
|
304
|
-
output: `Image generation failed upstream: ${JSON.stringify(polled.error ?? polled).slice(0, 240)}`,
|
|
305
|
-
isError: true,
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
const text = await pollResp.text().catch(() => '');
|
|
311
|
-
return { output: `Image poll failed (${pollResp.status}): ${text.slice(0, 200)}`, isError: true };
|
|
281
|
+
const outcome = await pollImageJob(pollEndpoint, pollHeaders, controller.signal);
|
|
282
|
+
if (outcome.kind === 'failed') {
|
|
283
|
+
return {
|
|
284
|
+
output: `Image generation failed upstream: ${JSON.stringify(outcome.error ?? '').slice(0, 240)}`,
|
|
285
|
+
isError: true,
|
|
286
|
+
};
|
|
312
287
|
}
|
|
313
|
-
if (
|
|
288
|
+
if (outcome.kind === 'poll_http_error') {
|
|
289
|
+
return { output: `Image poll failed (${outcome.status}): ${outcome.bodyPreview}`, isError: true };
|
|
290
|
+
}
|
|
291
|
+
if (outcome.kind === 'timed_out') {
|
|
314
292
|
return {
|
|
315
293
|
output: `Image generation queued but did not complete within 5 minutes. Payment was settled when the gateway accepted the job (HTTP 202). ` +
|
|
316
294
|
`If this keeps happening, the upstream image model is overloaded — try a smaller / faster model or retry later.`,
|
|
317
295
|
isError: true,
|
|
318
296
|
};
|
|
319
297
|
}
|
|
320
|
-
result =
|
|
298
|
+
result = outcome.body;
|
|
321
299
|
}
|
|
322
300
|
const imageData = result.data?.[0];
|
|
323
301
|
if (!imageData) {
|
|
@@ -533,3 +511,48 @@ export function createImageGenCapability(deps = {}) {
|
|
|
533
511
|
}
|
|
534
512
|
/** Back-compat static capability for callers that don't want the Content bridge. */
|
|
535
513
|
export const imageGenCapability = createImageGenCapability();
|
|
514
|
+
export async function pollImageJob(pollEndpoint, headers, signal, options = {}) {
|
|
515
|
+
const maxWaitMs = options.maxWaitMs ?? 5 * 60 * 1000;
|
|
516
|
+
const intervalMs = options.intervalMs ?? 3_000;
|
|
517
|
+
const deadline = Date.now() + maxWaitMs;
|
|
518
|
+
while (Date.now() < deadline) {
|
|
519
|
+
if (signal.aborted)
|
|
520
|
+
throw new Error('aborted');
|
|
521
|
+
await sleep(intervalMs, signal);
|
|
522
|
+
const resp = await fetch(pollEndpoint, { method: 'GET', headers, signal });
|
|
523
|
+
if (resp.status === 202)
|
|
524
|
+
continue; // still queued
|
|
525
|
+
if (resp.status === 429 || resp.status >= 500)
|
|
526
|
+
continue; // transient
|
|
527
|
+
if (resp.ok) {
|
|
528
|
+
const body = (await resp.json().catch(() => null));
|
|
529
|
+
if (!body)
|
|
530
|
+
continue;
|
|
531
|
+
if (body.status === 'failed')
|
|
532
|
+
return { kind: 'failed', error: body.error };
|
|
533
|
+
if (body.status === 'completed' || (body.data && body.data[0])) {
|
|
534
|
+
return { kind: 'completed', body };
|
|
535
|
+
}
|
|
536
|
+
// Non-terminal but ok shape (e.g. status: 'in_progress') — wait.
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const text = await resp.text().catch(() => '');
|
|
540
|
+
return { kind: 'poll_http_error', status: resp.status, bodyPreview: text.slice(0, 200) };
|
|
541
|
+
}
|
|
542
|
+
return { kind: 'timed_out' };
|
|
543
|
+
}
|
|
544
|
+
function sleep(ms, signal) {
|
|
545
|
+
return new Promise((resolve, reject) => {
|
|
546
|
+
if (signal.aborted)
|
|
547
|
+
return reject(new Error('aborted'));
|
|
548
|
+
const t = setTimeout(() => {
|
|
549
|
+
signal.removeEventListener('abort', onAbort);
|
|
550
|
+
resolve();
|
|
551
|
+
}, ms);
|
|
552
|
+
const onAbort = () => {
|
|
553
|
+
clearTimeout(t);
|
|
554
|
+
reject(new Error('aborted'));
|
|
555
|
+
};
|
|
556
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
557
|
+
});
|
|
558
|
+
}
|
package/package.json
CHANGED