@blockrun/franklin 3.15.47 → 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 +89 -3
- 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
|
@@ -238,9 +238,13 @@ function buildExecute(deps) {
|
|
|
238
238
|
headers,
|
|
239
239
|
body,
|
|
240
240
|
});
|
|
241
|
-
// Handle x402 payment
|
|
241
|
+
// Handle x402 payment. Lifted out of the inner block so the polling
|
|
242
|
+
// path below can reuse the signed headers — every poll request
|
|
243
|
+
// re-presents the same authorization (the gateway settles on the
|
|
244
|
+
// first completed poll, same contract as videogen.ts:251).
|
|
245
|
+
let paymentHeaders = null;
|
|
242
246
|
if (response.status === 402) {
|
|
243
|
-
|
|
247
|
+
paymentHeaders = await signPayment(response, chain, endpoint);
|
|
244
248
|
if (!paymentHeaders) {
|
|
245
249
|
return { output: 'Payment failed. Check wallet balance with: franklin balance', isError: true };
|
|
246
250
|
}
|
|
@@ -255,7 +259,44 @@ function buildExecute(deps) {
|
|
|
255
259
|
const errText = await response.text().catch(() => '');
|
|
256
260
|
return { output: `Image generation failed (${response.status}): ${errText.slice(0, 200)}`, isError: true };
|
|
257
261
|
}
|
|
258
|
-
|
|
262
|
+
let result = await response.json();
|
|
263
|
+
// Async path: gateway returns HTTP 202 (Accepted, queued) + a poll_url
|
|
264
|
+
// when the upstream image model takes longer than the inline budget
|
|
265
|
+
// (gpt-image-1/-2 routinely exceed 30s). Verified 2026-05-04 from
|
|
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
|
+
// 4 of 5 actually completed in GCS within 41–56s and would have
|
|
269
|
+
// been retrievable if Franklin had polled. Mirror videogen.ts's
|
|
270
|
+
// pollUntilReady contract: same x-payment header on each poll.
|
|
271
|
+
if (response.status === 202 && result.poll_url) {
|
|
272
|
+
const origin = new URL(apiUrl).origin;
|
|
273
|
+
const pollEndpoint = result.poll_url.startsWith('http')
|
|
274
|
+
? result.poll_url
|
|
275
|
+
: `${origin}${result.poll_url}`;
|
|
276
|
+
const pollHeaders = paymentHeaders ? { ...headers, ...paymentHeaders } : headers;
|
|
277
|
+
// Replace the POST timeout with a longer poll deadline. Image
|
|
278
|
+
// generation routinely completes within 1–3 min once queued; the
|
|
279
|
+
// 5 min ceiling matches videogen's POLL_MAX_WAIT_MS scale.
|
|
280
|
+
clearTimeout(timeout);
|
|
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
|
+
};
|
|
287
|
+
}
|
|
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') {
|
|
292
|
+
return {
|
|
293
|
+
output: `Image generation queued but did not complete within 5 minutes. Payment was settled when the gateway accepted the job (HTTP 202). ` +
|
|
294
|
+
`If this keeps happening, the upstream image model is overloaded — try a smaller / faster model or retry later.`,
|
|
295
|
+
isError: true,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
result = outcome.body;
|
|
299
|
+
}
|
|
259
300
|
const imageData = result.data?.[0];
|
|
260
301
|
if (!imageData) {
|
|
261
302
|
// Some gateways return 200 with an `error` / `message` field for
|
|
@@ -470,3 +511,48 @@ export function createImageGenCapability(deps = {}) {
|
|
|
470
511
|
}
|
|
471
512
|
/** Back-compat static capability for callers that don't want the Content bridge. */
|
|
472
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