@ykurilov/agent-chat-images 0.80.1 → 0.82.0
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/CHANGELOG.md +35 -0
- package/dist/{chunk-5IORNMTX.js → chunk-JRBKNWG3.js} +3 -1
- package/dist/chunk-JRBKNWG3.js.map +1 -0
- package/dist/{chunk-33SNN6BB.js → chunk-W74LUWVQ.js} +31 -12
- package/dist/chunk-W74LUWVQ.js.map +1 -0
- package/dist/index.js +136 -46
- package/dist/index.js.map +1 -1
- package/dist/postgres.js +1 -1
- package/dist/react/index.js +1 -1
- package/dist/sqlite.js +1 -1
- package/dist/types/create-image-generation.d.ts +8 -0
- package/dist/types/download-image.d.ts +9 -1
- package/dist/types/image-catalog.d.ts +3 -0
- package/dist/types/image-failure.d.ts +11 -0
- package/dist/types/image-generation-error.d.ts +5 -0
- package/dist/types/image-queue/create-image-queue.d.ts +28 -0
- package/dist/types/image-queue/image-job-store.d.ts +12 -1
- package/dist/types/image-setup.d.ts +2 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +6 -5
- package/dist/chunk-33SNN6BB.js.map +0 -1
- package/dist/chunk-5IORNMTX.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ImageJobKeyTakenError,
|
|
3
|
+
hasPendingImageFailure,
|
|
3
4
|
isOpenImageJob,
|
|
4
5
|
memoryImageJobStore
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-JRBKNWG3.js";
|
|
6
7
|
import {
|
|
7
8
|
ImageGenerationError,
|
|
8
9
|
aspectRatios,
|
|
10
|
+
canRetryImageFailure,
|
|
9
11
|
extendImageCatalog,
|
|
10
12
|
hideSecrets,
|
|
11
13
|
imageQualities,
|
|
12
14
|
imageSizes,
|
|
13
15
|
kieProvider,
|
|
16
|
+
providerFailureKind,
|
|
14
17
|
runwareProvider,
|
|
15
18
|
standardImageCatalog,
|
|
16
19
|
waitForImageTask
|
|
17
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-W74LUWVQ.js";
|
|
18
21
|
|
|
19
22
|
// src/create-image-generation.ts
|
|
20
23
|
import { tool } from "ai";
|
|
@@ -23,6 +26,7 @@ import { SpendingLimitReachedError as SpendingLimitReachedError2, declareToolTim
|
|
|
23
26
|
|
|
24
27
|
// src/download-image.ts
|
|
25
28
|
import { detectFileKind } from "@ykurilov/agent-chat-attachments";
|
|
29
|
+
import { dnsHostResolver, sharedPinnedFetch, WebToolError } from "@ykurilov/agent-chat-web-tools/network";
|
|
26
30
|
|
|
27
31
|
// src/download-address.ts
|
|
28
32
|
var PRIVATE_IPV4 = [
|
|
@@ -109,7 +113,7 @@ function pngSize(bytes) {
|
|
|
109
113
|
var DOWNLOAD_TIMEOUT_MS = 6e4;
|
|
110
114
|
var MAX_REDIRECTS = 3;
|
|
111
115
|
var imageExtensions = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
|
|
112
|
-
var failure = (reason) => new ImageGenerationError(reason, { refusedBeforeWork: false });
|
|
116
|
+
var failure = (reason, kind = "unknown") => new ImageGenerationError(reason, { refusedBeforeWork: false, kind });
|
|
113
117
|
var megabytes = (bytes) => `${Math.round(bytes / (1024 * 1024) * 10) / 10} \u041C\u0411`;
|
|
114
118
|
async function readWithin(response, maxBytes) {
|
|
115
119
|
if (!response.body) return new Uint8Array();
|
|
@@ -140,15 +144,20 @@ function checkedAddress(url, base, trustedOrigin) {
|
|
|
140
144
|
try {
|
|
141
145
|
address = new URL(url, base);
|
|
142
146
|
} catch {
|
|
143
|
-
throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441");
|
|
147
|
+
throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
|
|
144
148
|
}
|
|
145
|
-
if (!isAllowedImageAddress(address, trustedOrigin)) throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441");
|
|
149
|
+
if (!isAllowedImageAddress(address, trustedOrigin)) throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
|
|
146
150
|
return address;
|
|
147
151
|
}
|
|
152
|
+
function fetchFor(input, address) {
|
|
153
|
+
if (input.fetch) return input.fetch;
|
|
154
|
+
if (input.trustedOrigin !== void 0 && address.origin === input.trustedOrigin) return globalThis.fetch;
|
|
155
|
+
return sharedPinnedFetch(input.resolveHost ?? dnsHostResolver);
|
|
156
|
+
}
|
|
148
157
|
async function fetchImage(input, first, signal) {
|
|
149
158
|
let address = first;
|
|
150
159
|
for (let hop = 0; ; hop++) {
|
|
151
|
-
const response = await input
|
|
160
|
+
const response = await fetchFor(input, address)(address.href, { signal, redirect: "manual" });
|
|
152
161
|
const location = response.status >= 300 && response.status < 400 ? response.headers.get("location") : null;
|
|
153
162
|
if (location === null) return response;
|
|
154
163
|
await response.body?.cancel().catch(() => {
|
|
@@ -166,14 +175,15 @@ async function downloadGeneratedImage(input) {
|
|
|
166
175
|
let bytes;
|
|
167
176
|
try {
|
|
168
177
|
const response = await fetchImage(input, address, signal);
|
|
169
|
-
if (!response.ok) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: ${response.status}
|
|
178
|
+
if (!response.ok) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: ${response.status}`, response.status === 408 || response.status === 429 || response.status >= 500 ? "transient" : "configuration");
|
|
170
179
|
const declared = Number(response.headers.get("content-length"));
|
|
171
180
|
if (Number.isFinite(declared) && declared > input.maxImageBytes) throw failure(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 ${megabytes(input.maxImageBytes)}`);
|
|
172
181
|
bytes = await readWithin(response, input.maxImageBytes);
|
|
173
182
|
} catch (error) {
|
|
174
183
|
if (input.abortSignal?.aborted) throw input.abortSignal.reason ?? error;
|
|
175
184
|
if (error instanceof ImageGenerationError) throw error;
|
|
176
|
-
|
|
185
|
+
if (error instanceof WebToolError && error.kind === "bad_address") throw failure("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441", "configuration");
|
|
186
|
+
throw failure(timeout.signal.aborted ? "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C: \u043D\u0435 \u0443\u0441\u043F\u0435\u043B\u0430" : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", "transient");
|
|
177
187
|
} finally {
|
|
178
188
|
clearTimeout(timer);
|
|
179
189
|
}
|
|
@@ -302,7 +312,7 @@ function createImageJobs(options) {
|
|
|
302
312
|
const output = input.setup.output;
|
|
303
313
|
const file = await downloadGeneratedImage({
|
|
304
314
|
url,
|
|
305
|
-
fetch: last.
|
|
315
|
+
...last.downloadFetch && { fetch: last.downloadFetch },
|
|
306
316
|
maxImageBytes: options.maxImageBytes,
|
|
307
317
|
...last.trustedOrigin !== void 0 && { trustedOrigin: last.trustedOrigin },
|
|
308
318
|
...output && { transform: (bytes, mediaType) => postprocessImage(bytes, mediaType, output) },
|
|
@@ -534,6 +544,7 @@ function resolveImageSetups(choice) {
|
|
|
534
544
|
sizes,
|
|
535
545
|
priceUsd: roundUsd(sizePriceUsd * qualityFactor),
|
|
536
546
|
...provider.baseURL !== void 0 && { trustedOrigin: new URL(provider.baseURL).origin },
|
|
547
|
+
...choice.fetch && { downloadFetch: choice.fetch },
|
|
537
548
|
...providerSettings && { providerSettings },
|
|
538
549
|
...settings.transparentBackground && { outputFormat: "png" }
|
|
539
550
|
};
|
|
@@ -666,9 +677,14 @@ function markRecorded(record, usageId) {
|
|
|
666
677
|
}));
|
|
667
678
|
if (!found) return null;
|
|
668
679
|
if (isOpen(record)) return { steps };
|
|
669
|
-
const left = hasUnrecorded(steps);
|
|
680
|
+
const left = hasUnrecorded(steps) || hasPendingImageFailure(record);
|
|
670
681
|
return { steps, active: left, ...!left && { leaseOwner: void 0, leaseUntil: void 0 } };
|
|
671
682
|
}
|
|
683
|
+
function failureOf(record) {
|
|
684
|
+
if (record.failure) return record.failure;
|
|
685
|
+
if (record.status !== "failed" || record.lateUntil !== void 0 || !record.reason) return void 0;
|
|
686
|
+
return { kind: providerFailureKind(void 0, void 0, record.reason), reason: record.reason };
|
|
687
|
+
}
|
|
672
688
|
function view(record) {
|
|
673
689
|
return {
|
|
674
690
|
id: record.id,
|
|
@@ -683,6 +699,7 @@ function view(record) {
|
|
|
683
699
|
...record.data !== void 0 && { data: record.data },
|
|
684
700
|
status: record.status,
|
|
685
701
|
...record.reason !== void 0 && { reason: record.reason },
|
|
702
|
+
...failureOf(record) !== void 0 && { failure: structuredClone(failureOf(record)) },
|
|
686
703
|
...record.result !== void 0 && { result: record.result },
|
|
687
704
|
attempt: record.attempt,
|
|
688
705
|
...record.lateUntil !== void 0 && { lateUntil: record.lateUntil },
|
|
@@ -693,7 +710,7 @@ function view(record) {
|
|
|
693
710
|
}
|
|
694
711
|
var refused = (reason) => ({ status: "refused", reason });
|
|
695
712
|
function checkOptions(options) {
|
|
696
|
-
const positive = ["pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
|
|
713
|
+
const positive = ["failureRetryMs", "pollIntervalMs", "attemptTimeoutMs", "latePollIntervalMs", "callTimeoutMs", "acceptWaitMs", "leaseMs", "forgetFinishedAfterMs", "maxImageBytes"];
|
|
697
714
|
for (const name of positive) {
|
|
698
715
|
const value = options[name];
|
|
699
716
|
if (value !== void 0 && !(Number.isFinite(value) && value > 0)) throw new Error(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: ${name} \u2014 \u043D\u0443\u0436\u043D\u043E \u0447\u0438\u0441\u043B\u043E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F`);
|
|
@@ -704,6 +721,7 @@ function checkOptions(options) {
|
|
|
704
721
|
}
|
|
705
722
|
const window = options.lateResultWindowMs;
|
|
706
723
|
if (window !== void 0 && !(Number.isFinite(window) && window >= 0)) throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: lateResultWindowMs \u2014 \u0447\u0438\u0441\u043B\u043E \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 \u043D\u0443\u043B\u044F");
|
|
724
|
+
if (options.onFailed !== void 0 && typeof options.onFailed !== "function") throw new TypeError("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: onFailed \u2014 \u043D\u0443\u0436\u043D\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F");
|
|
707
725
|
if (typeof options.deliver !== "function") throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: deliver \u2014 \u043D\u0443\u0436\u043D\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F-\u043F\u0440\u0438\u0451\u043C\u043D\u0438\u043A");
|
|
708
726
|
}
|
|
709
727
|
function createImageQueue(options) {
|
|
@@ -717,6 +735,7 @@ function createImageQueue(options) {
|
|
|
717
735
|
const nowMs = () => now().getTime();
|
|
718
736
|
const hide = setups.hideSecrets;
|
|
719
737
|
const logError = (message) => options.logError?.(hide(message));
|
|
738
|
+
const failureRetryMs = options.failureRetryMs ?? 3e4;
|
|
720
739
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
721
740
|
const attemptTimeoutMs = options.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS;
|
|
722
741
|
const attempts = options.attempts ?? 1;
|
|
@@ -853,7 +872,7 @@ function createImageQueue(options) {
|
|
|
853
872
|
current = await updateJob(current, (fresh) => markRecorded(fresh, charge.usageId), () => true);
|
|
854
873
|
if (!current) return;
|
|
855
874
|
}
|
|
856
|
-
await updateJob(current, (fresh) => isOpen(fresh) || !fresh.active && fresh.leaseOwner === void 0 ? null : { active: hasUnrecorded(fresh.steps), leaseOwner: void 0, leaseUntil: void 0 }, () => true);
|
|
875
|
+
await updateJob(current, (fresh) => isOpen(fresh) || !fresh.active && fresh.leaseOwner === void 0 ? null : { active: hasUnrecorded(fresh.steps) || hasPendingImageFailure(fresh), leaseOwner: void 0, leaseUntil: void 0 }, () => true);
|
|
857
876
|
}
|
|
858
877
|
async function chargeTask(rt, index, costUsd) {
|
|
859
878
|
await write(rt, (record) => {
|
|
@@ -878,24 +897,29 @@ function createImageQueue(options) {
|
|
|
878
897
|
(fresh) => {
|
|
879
898
|
if (!isOpen(fresh)) return null;
|
|
880
899
|
const steps = fresh.steps.map((step) => step.task ? { ...withoutTask(step), charges: [...step.charges, chargeOf(step.task, step.task.priceUsd)] } : step);
|
|
881
|
-
return { status, reason, run: fresh.run + 1, steps, lateUntil: void 0, active: hasUnrecorded(steps), finishedAt: iso(nowMs()), leaseOwner: void 0, leaseUntil: void 0 };
|
|
900
|
+
return { status, reason, failure: status === "failed" ? { kind: "configuration", reason: reason ?? "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0441\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C" } : void 0, failureNotification: status === "failed" && options.onFailed ? { id: `image-failed:${fresh.id}:${fresh.run + 1}`, attempts: 0, nextAttemptAt: iso(nowMs()) } : void 0, run: fresh.run + 1, steps, lateUntil: void 0, active: hasUnrecorded(steps) || status === "failed" && options.onFailed !== void 0, finishedAt: iso(nowMs()), leaseOwner: void 0, leaseUntil: void 0 };
|
|
882
901
|
},
|
|
883
902
|
() => true
|
|
884
903
|
);
|
|
885
904
|
if (!closed) return;
|
|
886
905
|
byJob.get(record.id)?.controller.abort(RUN_ENDED);
|
|
887
906
|
await settleClosed(closed);
|
|
907
|
+
if (status === "failed" && options.onFailed) {
|
|
908
|
+
const pending = await store.get(closed.id);
|
|
909
|
+
if (pending && hasPendingImageFailure(pending)) await claim(pending);
|
|
910
|
+
}
|
|
888
911
|
}
|
|
889
912
|
function openUsd(rt) {
|
|
890
913
|
const record = rt.record;
|
|
891
914
|
if (!record) return rt.reservedUsd;
|
|
892
|
-
if (!isOpen(record)) return 0;
|
|
915
|
+
if (!isOpen(record) || !("setup" in rt)) return 0;
|
|
916
|
+
const setup = rt.setup;
|
|
893
917
|
const late = record.lateUntil !== void 0;
|
|
894
918
|
const last = record.steps.length - 1;
|
|
895
919
|
return record.steps.reduce((total, step, index) => {
|
|
896
|
-
if (step.urls) return rt.reorderOnDeliveryFailure && index === last ? total +
|
|
920
|
+
if (step.urls) return rt.reorderOnDeliveryFailure && index === last ? total + setup.steps[index].priceUsd : total;
|
|
897
921
|
if (step.task) return total + step.task.priceUsd;
|
|
898
|
-
return late ? total : total + (
|
|
922
|
+
return late ? total : total + (setup.steps[index]?.priceUsd ?? 0);
|
|
899
923
|
}, 0);
|
|
900
924
|
}
|
|
901
925
|
function pendingUsd(ownerId, chatId, before) {
|
|
@@ -941,9 +965,10 @@ function createImageQueue(options) {
|
|
|
941
965
|
}
|
|
942
966
|
async function failedSend(rt, index, error, wholeAttempt) {
|
|
943
967
|
throwIfEnded(rt);
|
|
968
|
+
const failure2 = { kind: error instanceof ImageGenerationError ? error.kind : error instanceof CallTimedOut ? "transient" : "unknown", reason: error instanceof ImageGenerationError ? hide(error.reason) : error instanceof CallTimedOut ? error.message : "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", ...error instanceof ImageGenerationError && error.providerCode && { providerCode: hide(error.providerCode) } };
|
|
944
969
|
if (error instanceof ImageGenerationError && error.refusedBeforeWork) {
|
|
945
970
|
await write(rt, (record) => ({ steps: withStep(record.steps, index, withoutTask) }));
|
|
946
|
-
return { kind: "refused", reason: hide(error.reason) };
|
|
971
|
+
return { kind: "refused", reason: hide(error.reason), failure: failure2 };
|
|
947
972
|
}
|
|
948
973
|
let reason;
|
|
949
974
|
if (error instanceof ImageGenerationError) reason = hide(error.reason);
|
|
@@ -953,7 +978,7 @@ function createImageQueue(options) {
|
|
|
953
978
|
reason = "\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443";
|
|
954
979
|
}
|
|
955
980
|
await chargeTask(rt, index);
|
|
956
|
-
return { kind: "failed", reason };
|
|
981
|
+
return { kind: canRetryImageFailure(failure2.kind) ? "failed" : "refused", reason, failure: failure2 };
|
|
957
982
|
}
|
|
958
983
|
async function send(rt, index) {
|
|
959
984
|
const step = rt.setup.steps[index];
|
|
@@ -1009,7 +1034,7 @@ function createImageQueue(options) {
|
|
|
1009
1034
|
throwIfEnded(rt);
|
|
1010
1035
|
if (error instanceof ImageGenerationError) {
|
|
1011
1036
|
await chargeTask(rt, index);
|
|
1012
|
-
return { kind: "failed", reason: hide(error.reason) };
|
|
1037
|
+
return { kind: canRetryImageFailure(error.kind) ? "failed" : "refused", reason: hide(error.reason), failure: { kind: error.kind, reason: hide(error.reason), ...error.providerCode && { providerCode: hide(error.providerCode) } } };
|
|
1013
1038
|
}
|
|
1014
1039
|
continue;
|
|
1015
1040
|
}
|
|
@@ -1020,21 +1045,23 @@ function createImageQueue(options) {
|
|
|
1020
1045
|
}
|
|
1021
1046
|
if (state.status === "failed") {
|
|
1022
1047
|
await chargeTask(rt, index, state.costUsd);
|
|
1023
|
-
return { kind: "failed", reason: hide(state.reason) };
|
|
1048
|
+
return { kind: canRetryImageFailure(state.failureKind) ? "failed" : "refused", reason: hide(state.reason), failure: { kind: state.failureKind ?? "unknown", reason: hide(state.reason), ...state.providerCode && { providerCode: hide(state.providerCode) } } };
|
|
1024
1049
|
}
|
|
1025
1050
|
}
|
|
1026
1051
|
}
|
|
1027
|
-
async function finishFailed(rt, reason, extra = {}) {
|
|
1052
|
+
async function finishFailed(rt, reason, extra = {}, failure2 = { kind: "unknown", reason }) {
|
|
1028
1053
|
await write(rt, (record) => ({
|
|
1029
1054
|
status: "failed",
|
|
1030
1055
|
reason,
|
|
1031
1056
|
lateUntil: void 0,
|
|
1032
1057
|
...extra,
|
|
1033
|
-
|
|
1058
|
+
failure: failure2,
|
|
1059
|
+
failureNotification: options.onFailed ? { id: `image-failed:${record.id}:${record.run}`, attempts: 0, nextAttemptAt: iso(nowMs()) } : void 0,
|
|
1060
|
+
active: hasUnrecorded(record.steps) || options.onFailed !== void 0,
|
|
1034
1061
|
finishedAt: iso(nowMs()),
|
|
1035
|
-
leaseOwner: void 0,
|
|
1036
|
-
leaseUntil: void 0
|
|
1062
|
+
...options.onFailed ? {} : { leaseOwner: void 0, leaseUntil: void 0 }
|
|
1037
1063
|
}));
|
|
1064
|
+
rt.sent.resolve();
|
|
1038
1065
|
}
|
|
1039
1066
|
const nextAttempt = (rt) => write(rt, (record) => ({ attempt: record.attempt + 1, attemptDeadline: iso(nowMs() + attemptTimeoutMs) }));
|
|
1040
1067
|
async function runStep(rt, index) {
|
|
@@ -1057,7 +1084,7 @@ function createImageQueue(options) {
|
|
|
1057
1084
|
return true;
|
|
1058
1085
|
}
|
|
1059
1086
|
if (outcome.kind === "refused") {
|
|
1060
|
-
await finishFailed(rt, outcome.reason);
|
|
1087
|
+
await finishFailed(rt, outcome.reason, {}, outcome.failure);
|
|
1061
1088
|
return false;
|
|
1062
1089
|
}
|
|
1063
1090
|
if (outcome.kind === "failed") {
|
|
@@ -1065,12 +1092,12 @@ function createImageQueue(options) {
|
|
|
1065
1092
|
await nextAttempt(rt);
|
|
1066
1093
|
continue;
|
|
1067
1094
|
}
|
|
1068
|
-
await finishFailed(rt, outcome.reason);
|
|
1095
|
+
await finishFailed(rt, outcome.reason, {}, outcome.failure);
|
|
1069
1096
|
return false;
|
|
1070
1097
|
}
|
|
1071
1098
|
if (late) {
|
|
1072
1099
|
await chargeTask(rt, index);
|
|
1073
|
-
await finishFailed(rt, rt.record.reason ?? timeoutReason);
|
|
1100
|
+
await finishFailed(rt, rt.record.reason ?? timeoutReason, {}, { kind: "transient", reason: rt.record.reason ?? timeoutReason });
|
|
1074
1101
|
return false;
|
|
1075
1102
|
}
|
|
1076
1103
|
if (rt.record.attempt < attempts) {
|
|
@@ -1083,7 +1110,7 @@ function createImageQueue(options) {
|
|
|
1083
1110
|
continue;
|
|
1084
1111
|
}
|
|
1085
1112
|
await chargeTask(rt, index);
|
|
1086
|
-
await finishFailed(rt, timeoutReason);
|
|
1113
|
+
await finishFailed(rt, timeoutReason, {}, { kind: "transient", reason: timeoutReason });
|
|
1087
1114
|
return false;
|
|
1088
1115
|
}
|
|
1089
1116
|
}
|
|
@@ -1102,7 +1129,7 @@ function createImageQueue(options) {
|
|
|
1102
1129
|
try {
|
|
1103
1130
|
file = await downloadGeneratedImage({
|
|
1104
1131
|
url,
|
|
1105
|
-
fetch: last.
|
|
1132
|
+
...last.downloadFetch && { fetch: last.downloadFetch },
|
|
1106
1133
|
maxImageBytes,
|
|
1107
1134
|
...last.trustedOrigin !== void 0 && { trustedOrigin: last.trustedOrigin },
|
|
1108
1135
|
abortSignal: rt.controller.signal,
|
|
@@ -1113,7 +1140,7 @@ function createImageQueue(options) {
|
|
|
1113
1140
|
throwIfEnded(rt);
|
|
1114
1141
|
if (attempt >= downloadAttempts) {
|
|
1115
1142
|
if (rt.reorderOnDeliveryFailure) return "reorder";
|
|
1116
|
-
await finishFailed(rt, error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", { deliveryFailed: true });
|
|
1143
|
+
await finishFailed(rt, error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C", { deliveryFailed: true }, { kind: error instanceof ImageGenerationError ? error.kind : "unknown", reason: error instanceof ImageGenerationError ? hide(error.reason) : "\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u0430\u0441\u044C" });
|
|
1117
1144
|
return "done";
|
|
1118
1145
|
}
|
|
1119
1146
|
await sleep(pollIntervalMs, rt.controller.signal);
|
|
@@ -1138,7 +1165,7 @@ function createImageQueue(options) {
|
|
|
1138
1165
|
} catch (error) {
|
|
1139
1166
|
throwIfEnded(rt);
|
|
1140
1167
|
logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u043F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 ${record.id}: ${errorText2(error)}`);
|
|
1141
|
-
await finishFailed(rt, "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", { deliveryFailed: true });
|
|
1168
|
+
await finishFailed(rt, "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443", { deliveryFailed: true }, { kind: "delivery", reason: "\u041F\u0440\u0438\u0451\u043C\u043D\u0438\u043A \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443" });
|
|
1142
1169
|
return "done";
|
|
1143
1170
|
}
|
|
1144
1171
|
throwIfEnded(rt);
|
|
@@ -1174,16 +1201,62 @@ function createImageQueue(options) {
|
|
|
1174
1201
|
try {
|
|
1175
1202
|
await recordCharges(rt);
|
|
1176
1203
|
if (isOpen(rt.record)) await generate(rt);
|
|
1204
|
+
await notifyFailure(rt);
|
|
1177
1205
|
} catch (error) {
|
|
1178
1206
|
if (error !== RUN_ENDED && !rt.controller.signal.aborted) logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0437\u0430\u0434\u0430\u0447\u0430 ${rt.record?.id ?? ""} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u0430\u0441\u044C: ${errorText2(error)}`);
|
|
1179
1207
|
} finally {
|
|
1180
|
-
rt
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1208
|
+
finishRuntime(rt);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function finishRuntime(rt) {
|
|
1212
|
+
rt.ended = true;
|
|
1213
|
+
clearTimeout(rt.leaseTimer);
|
|
1214
|
+
runtimes.delete(rt);
|
|
1215
|
+
if (rt.record && byJob.get(rt.record.id) === rt) byJob.delete(rt.record.id);
|
|
1216
|
+
rt.sent.resolve();
|
|
1217
|
+
}
|
|
1218
|
+
async function notifyFailure(rt) {
|
|
1219
|
+
const record = rt.record;
|
|
1220
|
+
const notice = record.failureNotification;
|
|
1221
|
+
if (!options.onFailed || !notice || notice.deliveredAt || record.status !== "failed" || !record.failure || record.lateUntil) return;
|
|
1222
|
+
if (Date.parse(notice.nextAttemptAt) > nowMs()) return;
|
|
1223
|
+
const reserved = await write(rt, (fresh) => ({ failureNotification: { ...fresh.failureNotification, attempts: fresh.failureNotification.attempts + 1, nextAttemptAt: iso(nowMs() + failureRetryMs) } }));
|
|
1224
|
+
const id = notice.id;
|
|
1225
|
+
const isCurrent = async () => {
|
|
1226
|
+
if (rt.controller.signal.aborted) return false;
|
|
1227
|
+
const fresh = await store.get(record.id);
|
|
1228
|
+
const current = fresh?.status === "failed" && fresh.run === record.run && fresh.leaseOwner === me && Date.parse(fresh.leaseUntil ?? "") > nowMs() && fresh.failureNotification?.id === id && fresh.failureNotification.deliveredAt === void 0 && !rt.controller.signal.aborted;
|
|
1229
|
+
if (!current) rt.controller.abort(RUN_ENDED);
|
|
1230
|
+
return current;
|
|
1231
|
+
};
|
|
1232
|
+
if (!await isCurrent()) throw RUN_ENDED;
|
|
1233
|
+
try {
|
|
1234
|
+
await options.onFailed({ id, run: reserved.run, job: view(reserved), failure: structuredClone(reserved.failure) }, { signal: rt.controller.signal, isCurrent });
|
|
1235
|
+
if (!await isCurrent()) throw RUN_ENDED;
|
|
1236
|
+
await write(rt, (fresh) => ({ failureNotification: { ...fresh.failureNotification, deliveredAt: iso(nowMs()) }, active: hasUnrecorded(fresh.steps), leaseOwner: void 0, leaseUntil: void 0 }));
|
|
1237
|
+
} catch {
|
|
1238
|
+
throwIfEnded(rt);
|
|
1239
|
+
logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 ${id} \u043D\u0435 \u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0441\u044F \u0431\u0435\u0437 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438`);
|
|
1240
|
+
await write(rt, () => ({ active: true, leaseOwner: void 0, leaseUntil: void 0 }));
|
|
1185
1241
|
}
|
|
1186
1242
|
}
|
|
1243
|
+
function launchFailure(record) {
|
|
1244
|
+
const rt = newJobRuntime(record.ownerId, record.chatId, 0);
|
|
1245
|
+
rt.record = record;
|
|
1246
|
+
runtimes.add(rt);
|
|
1247
|
+
byJob.set(record.id, rt);
|
|
1248
|
+
keepLease(rt);
|
|
1249
|
+
rt.done = (async () => {
|
|
1250
|
+
try {
|
|
1251
|
+
await recordCharges(rt);
|
|
1252
|
+
await notifyFailure(rt);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
if (error !== RUN_ENDED && !rt.controller.signal.aborted) logError(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u0441\u043E\u0431\u044B\u0442\u0438\u0435 ${record.id} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B\u043E\u0441\u044C, \u0431\u0443\u0434\u0435\u0442 \u043F\u043E\u0434\u043E\u0431\u0440\u0430\u043D\u043E \u043F\u043E\u0441\u043B\u0435 \u0430\u0440\u0435\u043D\u0434\u044B`);
|
|
1255
|
+
} finally {
|
|
1256
|
+
finishRuntime(rt);
|
|
1257
|
+
}
|
|
1258
|
+
})();
|
|
1259
|
+
}
|
|
1187
1260
|
function keepLease(rt) {
|
|
1188
1261
|
const renew = () => {
|
|
1189
1262
|
rt.leaseTimer = setTimeout(async () => {
|
|
@@ -1199,10 +1272,13 @@ function createImageQueue(options) {
|
|
|
1199
1272
|
};
|
|
1200
1273
|
renew();
|
|
1201
1274
|
}
|
|
1202
|
-
function
|
|
1275
|
+
function newJobRuntime(ownerId, chatId, reservedUsd) {
|
|
1203
1276
|
let resolve;
|
|
1204
1277
|
const promise = new Promise((done) => resolve = done);
|
|
1205
|
-
return { ownerId, chatId,
|
|
1278
|
+
return { ownerId, chatId, reservedUsd, controller: new AbortController(), chain: Promise.resolve(), sent: { promise, resolve }, ended: false, checkNow: false, reorderOnDeliveryFailure: false };
|
|
1279
|
+
}
|
|
1280
|
+
function newRuntime(ownerId, chatId, setup, reservedUsd) {
|
|
1281
|
+
return { ...newJobRuntime(ownerId, chatId, reservedUsd), setup };
|
|
1206
1282
|
}
|
|
1207
1283
|
function launch(rt, record) {
|
|
1208
1284
|
rt.record = record;
|
|
@@ -1229,9 +1305,13 @@ function createImageQueue(options) {
|
|
|
1229
1305
|
}
|
|
1230
1306
|
async function claim(record) {
|
|
1231
1307
|
if (stopped || byJob.has(record.id)) return;
|
|
1308
|
+
if (!isOpen(record) && hasPendingImageFailure(record) && !hasUnrecorded(record.steps) && Date.parse(record.failureNotification.nextAttemptAt) > nowMs()) return;
|
|
1232
1309
|
const claimed = await store.update(record.id, { leaseOwner: me, leaseUntil: iso(nowMs() + leaseMs), updatedAt: iso(nowMs()) }, record.version);
|
|
1233
1310
|
if (!claimed) return;
|
|
1234
|
-
if (!isOpen(claimed))
|
|
1311
|
+
if (!isOpen(claimed)) {
|
|
1312
|
+
if (options.onFailed && hasPendingImageFailure(claimed) && Date.parse(claimed.failureNotification.nextAttemptAt) <= nowMs()) return launchFailure(claimed);
|
|
1313
|
+
return settleClosed(claimed);
|
|
1314
|
+
}
|
|
1235
1315
|
const setup = setupOf(claimed);
|
|
1236
1316
|
if (!setup) return closeJob(claimed, "failed", `\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0441\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C: \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \xAB${claimed.purpose}\xBB \u0432 \u043D\u0435\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442`);
|
|
1237
1317
|
const rt = newRuntime(claimed.ownerId, claimed.chatId, setup, 0);
|
|
@@ -1350,11 +1430,13 @@ function createImageQueue(options) {
|
|
|
1350
1430
|
throw error;
|
|
1351
1431
|
}
|
|
1352
1432
|
}
|
|
1353
|
-
|
|
1433
|
+
const matchesRetry = (record, guard) => guard.expectedJobId === void 0 || record?.id === guard.expectedJobId && record.run === guard.expectedRun;
|
|
1434
|
+
function retryJob(ownerId, key, guard) {
|
|
1354
1435
|
return underKey(ownerId, key, async () => {
|
|
1355
1436
|
const latest = (await store.list({ ownerId, key })).at(-1);
|
|
1356
|
-
if (!latest) return null;
|
|
1437
|
+
if (!latest || !matchesRetry(latest, guard)) return null;
|
|
1357
1438
|
if (latest.status !== "failed") return view(latest);
|
|
1439
|
+
if (failureOf(latest)?.kind === "content") return refused("\u041F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435: \u0442\u043E\u0442 \u0436\u0435 \u0437\u0430\u043F\u0440\u043E\u0441 \u043D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0435\u0442\u0441\u044F; \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u0443\u044E \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u0443");
|
|
1358
1440
|
const setup = setupOf(latest);
|
|
1359
1441
|
if (!setup) return refused(`\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \xAB${latest.purpose}\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442`);
|
|
1360
1442
|
const remaining = latest.steps.reduce((total, step, index) => step.urls ? total : total + (step.task?.priceUsd ?? setup.steps[index].priceUsd), 0);
|
|
@@ -1368,6 +1450,7 @@ function createImageQueue(options) {
|
|
|
1368
1450
|
let current = latest;
|
|
1369
1451
|
let revived = null;
|
|
1370
1452
|
while (current?.status === "failed") {
|
|
1453
|
+
if (!matchesRetry(current, guard)) return null;
|
|
1371
1454
|
if (stopped) return refused(QUEUE_STOPPED);
|
|
1372
1455
|
const at = nowMs();
|
|
1373
1456
|
revived = await reviveOrTaken(
|
|
@@ -1378,6 +1461,8 @@ function createImageQueue(options) {
|
|
|
1378
1461
|
attempt: 1,
|
|
1379
1462
|
attemptDeadline: iso(at + attemptTimeoutMs),
|
|
1380
1463
|
reason: void 0,
|
|
1464
|
+
failure: void 0,
|
|
1465
|
+
failureNotification: void 0,
|
|
1381
1466
|
lateUntil: void 0,
|
|
1382
1467
|
deliveryFailed: void 0,
|
|
1383
1468
|
finishedAt: void 0,
|
|
@@ -1389,13 +1474,14 @@ function createImageQueue(options) {
|
|
|
1389
1474
|
current.version
|
|
1390
1475
|
);
|
|
1391
1476
|
if (revived === "taken") {
|
|
1477
|
+
if (guard.expectedJobId !== void 0) return null;
|
|
1392
1478
|
const newest = (await store.list({ ownerId, key })).at(-1);
|
|
1393
1479
|
return newest ? view(newest) : null;
|
|
1394
1480
|
}
|
|
1395
1481
|
if (revived) break;
|
|
1396
1482
|
current = await store.get(latest.id);
|
|
1397
1483
|
}
|
|
1398
|
-
if (!revived) return current ? view(current) : null;
|
|
1484
|
+
if (!revived) return current && matchesRetry(current, guard) ? view(current) : null;
|
|
1399
1485
|
if (stopped) {
|
|
1400
1486
|
await updateJob(revived, (record) => record.leaseOwner === me ? { leaseOwner: void 0, leaseUntil: void 0 } : null, (record) => record.leaseOwner === me);
|
|
1401
1487
|
return refused(QUEUE_STOPPED);
|
|
@@ -1441,12 +1527,14 @@ function createImageQueue(options) {
|
|
|
1441
1527
|
void start();
|
|
1442
1528
|
return {
|
|
1443
1529
|
order,
|
|
1444
|
-
retry({ ownerId, key }) {
|
|
1530
|
+
retry({ ownerId, key, expectedJobId, expectedRun }) {
|
|
1531
|
+
const guarded = expectedJobId !== void 0 || expectedRun !== void 0;
|
|
1532
|
+
if (guarded && (typeof expectedJobId !== "string" || expectedJobId.length === 0 || !Number.isInteger(expectedRun) || expectedRun < 1)) return Promise.reject(new TypeError("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: expectedJobId \u0438 expectedRun \u043F\u0435\u0440\u0435\u0434\u0430\u044E\u0442\u0441\u044F \u0432\u043C\u0435\u0441\u0442\u0435; run \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1"));
|
|
1445
1533
|
if (stopped) return Promise.reject(new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 (stop) \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F"));
|
|
1446
|
-
const flight = JSON.stringify([ownerId, key]);
|
|
1534
|
+
const flight = JSON.stringify([ownerId, key, expectedJobId, expectedRun]);
|
|
1447
1535
|
const running = retries.get(flight);
|
|
1448
1536
|
if (running) return running;
|
|
1449
|
-
const promise = retryJob(ownerId, key).finally(() => retries.delete(flight));
|
|
1537
|
+
const promise = retryJob(ownerId, key, { expectedJobId, expectedRun }).finally(() => retries.delete(flight));
|
|
1450
1538
|
retries.set(flight, promise);
|
|
1451
1539
|
return promise;
|
|
1452
1540
|
},
|
|
@@ -1533,7 +1621,7 @@ function checkNumbers(options) {
|
|
|
1533
1621
|
const limit = options.maxImagesPerTurn;
|
|
1534
1622
|
if (limit !== void 0 && !(Number.isInteger(limit) && limit >= 1)) throw new Error("\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: maxImagesPerTurn \u2014 \u0446\u0435\u043B\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u043E\u0442 1");
|
|
1535
1623
|
if (options.mode !== "background") {
|
|
1536
|
-
const backgroundOnly = ["jobStore", "pollIntervalMs", "attempts", "onQueued"].filter((name) => options[name] !== void 0);
|
|
1624
|
+
const backgroundOnly = ["jobStore", "pollIntervalMs", "attempts", "onQueued", "onFailed", "failureRetryMs"].filter((name) => options[name] !== void 0);
|
|
1537
1625
|
if (backgroundOnly.length > 0) throw new Error(`\u041A\u0430\u0440\u0442\u0438\u043D\u043A\u0438: ${backgroundOnly.join(", ")} \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F mode: "background"`);
|
|
1538
1626
|
}
|
|
1539
1627
|
}
|
|
@@ -1752,6 +1840,8 @@ function createImageGeneration(options) {
|
|
|
1752
1840
|
...options.purposes && { purposes: options.purposes },
|
|
1753
1841
|
...options.fetch && { fetch: options.fetch },
|
|
1754
1842
|
deliver: receiveQueued,
|
|
1843
|
+
...options.onFailed !== void 0 && { onFailed: options.onFailed },
|
|
1844
|
+
...options.failureRetryMs !== void 0 && { failureRetryMs: options.failureRetryMs },
|
|
1755
1845
|
...options.jobStore && { store: options.jobStore },
|
|
1756
1846
|
...options.spending && { spending: options.spending },
|
|
1757
1847
|
...options.pollIntervalMs !== void 0 && { pollIntervalMs: options.pollIntervalMs },
|