@copilotkit/aimock 1.23.1 → 1.24.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/fal-audio.cjs +171 -18
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +173 -20
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +412 -32
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +410 -34
- package/dist/fal.js.map +1 -1
- package/dist/gemini.cjs +4 -2
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +4 -2
- package/dist/gemini.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/llmock.cjs +18 -1
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +13 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +13 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +18 -1
- package/dist/llmock.js.map +1 -1
- package/dist/recorder.cjs +86 -55
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +12 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +12 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +85 -56
- package/dist/recorder.js.map +1 -1
- package/dist/server.cjs +4 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.cts +41 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/fal.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { flattenHeaders, getTestId, isAudioResponse, isErrorResponse, isJSONResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
|
|
2
2
|
import { matchFixture } from "./router.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveUpstreamUrl } from "./url.js";
|
|
4
|
+
import { buildFixtureMatch, persistFixture, proxyAndRecord } from "./recorder.js";
|
|
4
5
|
import { audioToFalFile } from "./fal-audio.js";
|
|
5
6
|
import crypto from "node:crypto";
|
|
6
7
|
|
|
@@ -48,6 +49,109 @@ var FalQueueStateMap = class {
|
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
51
|
const falQueueStates = new FalQueueStateMap();
|
|
52
|
+
function extractExtension(url, fallback) {
|
|
53
|
+
const segment = url.split("?")[0].split("#")[0].split("/").pop() ?? "";
|
|
54
|
+
const fileName = segment.length > 0 ? segment : "";
|
|
55
|
+
const dotIdx = fileName.lastIndexOf(".");
|
|
56
|
+
return {
|
|
57
|
+
fileName,
|
|
58
|
+
ext: dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function imageItemToFalImage(item, index) {
|
|
62
|
+
const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;
|
|
63
|
+
const { ext } = extractExtension(url, "png");
|
|
64
|
+
return {
|
|
65
|
+
url,
|
|
66
|
+
width: 1024,
|
|
67
|
+
height: 1024,
|
|
68
|
+
content_type: ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Translate an `ImageResponse` fixture into fal's image envelope shape:
|
|
73
|
+
* `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.
|
|
74
|
+
* Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.
|
|
75
|
+
*/
|
|
76
|
+
function imageResponseToFalJson(response) {
|
|
77
|
+
const images = (response.images ?? (response.image ? [response.image] : [])).map((item, i) => imageItemToFalImage(item, i));
|
|
78
|
+
return {
|
|
79
|
+
images,
|
|
80
|
+
timings: { inference: 0 },
|
|
81
|
+
seed: 0,
|
|
82
|
+
has_nsfw_concepts: images.map(() => false),
|
|
83
|
+
prompt: ""
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Translate a `VideoResponse` fixture into fal's video envelope shape:
|
|
88
|
+
* `{ video: { url, content_type, file_name, file_size }, seed }`.
|
|
89
|
+
*/
|
|
90
|
+
function videoResponseToFalJson(response) {
|
|
91
|
+
const url = response.video.url ?? "https://mock.fal.media/files/generated_video.mp4";
|
|
92
|
+
const { fileName, ext } = extractExtension(url, "mp4");
|
|
93
|
+
return {
|
|
94
|
+
video: {
|
|
95
|
+
url,
|
|
96
|
+
content_type: `video/${ext}`,
|
|
97
|
+
file_name: fileName || "generated_video.mp4",
|
|
98
|
+
file_size: 0
|
|
99
|
+
},
|
|
100
|
+
seed: 0
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolveProgression(config) {
|
|
104
|
+
const pollsBeforeInProgress = config?.pollsBeforeInProgress ?? 0;
|
|
105
|
+
const explicitCompleted = config?.pollsBeforeCompleted;
|
|
106
|
+
let pollsBeforeCompleted;
|
|
107
|
+
if (explicitCompleted != null) pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);
|
|
108
|
+
else if (config?.pollsBeforeInProgress != null) pollsBeforeCompleted = pollsBeforeInProgress + 1;
|
|
109
|
+
else pollsBeforeCompleted = 0;
|
|
110
|
+
return {
|
|
111
|
+
pollsBeforeInProgress,
|
|
112
|
+
pollsBeforeCompleted
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Mutates a job in place to advance its state on a status/result poll.
|
|
117
|
+
* IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op
|
|
118
|
+
* once COMPLETED or CANCELLED.
|
|
119
|
+
*/
|
|
120
|
+
function advanceJob(job) {
|
|
121
|
+
if (job.status === "COMPLETED" || job.status === "CANCELLED") return;
|
|
122
|
+
job.pollCount += 1;
|
|
123
|
+
if (job.status === "IN_QUEUE" && job.pollCount >= job.pollsBeforeInProgress) {
|
|
124
|
+
job.status = "IN_PROGRESS";
|
|
125
|
+
job.logs.push({
|
|
126
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
127
|
+
level: "INFO",
|
|
128
|
+
message: "Job started processing."
|
|
129
|
+
});
|
|
130
|
+
} else if (job.pollCount >= job.pollsBeforeCompleted) {
|
|
131
|
+
job.status = "COMPLETED";
|
|
132
|
+
job.completedAt = Date.now();
|
|
133
|
+
job.logs.push({
|
|
134
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
135
|
+
level: "INFO",
|
|
136
|
+
message: "Job completed."
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function queuePosition(job) {
|
|
141
|
+
if (job.status !== "IN_QUEUE") return 0;
|
|
142
|
+
return Math.max(0, job.pollsBeforeInProgress - job.pollCount);
|
|
143
|
+
}
|
|
144
|
+
function statusResponseBody(job) {
|
|
145
|
+
const body = {
|
|
146
|
+
status: job.status,
|
|
147
|
+
request_id: job.requestId,
|
|
148
|
+
response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,
|
|
149
|
+
logs: job.logs
|
|
150
|
+
};
|
|
151
|
+
if (job.status === "IN_QUEUE" || job.status === "IN_PROGRESS") body.queue_position = queuePosition(job);
|
|
152
|
+
if (job.status === "COMPLETED" && job.completedAt != null) body.metrics = { inference_time: (job.completedAt - job.submittedAt) / 1e3 };
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
51
155
|
const FAL_HOSTS = {
|
|
52
156
|
queue: "queue.fal.run",
|
|
53
157
|
sync: "fal.run",
|
|
@@ -164,11 +268,8 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
164
268
|
respondNotFound(req, res, pathname, journal, route.requestId);
|
|
165
269
|
return "handled";
|
|
166
270
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
request_id: job.requestId,
|
|
170
|
-
response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`
|
|
171
|
-
}, pathname, journal);
|
|
271
|
+
advanceJob(job);
|
|
272
|
+
writeJson(req, res, 200, statusResponseBody(job), pathname, journal);
|
|
172
273
|
return "handled";
|
|
173
274
|
}
|
|
174
275
|
case "queue-result": {
|
|
@@ -177,11 +278,17 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
177
278
|
respondNotFound(req, res, pathname, journal, route.requestId);
|
|
178
279
|
return "handled";
|
|
179
280
|
}
|
|
281
|
+
advanceJob(job);
|
|
282
|
+
if (job.status !== "COMPLETED") {
|
|
283
|
+
writeJson(req, res, 202, statusResponseBody(job), pathname, journal);
|
|
284
|
+
return "handled";
|
|
285
|
+
}
|
|
180
286
|
writeJson(req, res, 200, job.result, pathname, journal);
|
|
181
287
|
return "handled";
|
|
182
288
|
}
|
|
183
|
-
case "queue-cancel":
|
|
184
|
-
|
|
289
|
+
case "queue-cancel": {
|
|
290
|
+
const job = falQueueStates.get(stateKey(route.requestId));
|
|
291
|
+
if (!job) {
|
|
185
292
|
journal.add({
|
|
186
293
|
method: req.method ?? "PUT",
|
|
187
294
|
path: pathname,
|
|
@@ -196,19 +303,56 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
196
303
|
res.end(JSON.stringify({ status: "NOT_FOUND" }));
|
|
197
304
|
return "handled";
|
|
198
305
|
}
|
|
306
|
+
if (job.status === "COMPLETED") {
|
|
307
|
+
journal.add({
|
|
308
|
+
method: req.method ?? "PUT",
|
|
309
|
+
path: pathname,
|
|
310
|
+
headers: flattenHeaders(req.headers),
|
|
311
|
+
body: null,
|
|
312
|
+
response: {
|
|
313
|
+
status: 400,
|
|
314
|
+
fixture: null
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
318
|
+
res.end(JSON.stringify({ status: "ALREADY_COMPLETED" }));
|
|
319
|
+
return "handled";
|
|
320
|
+
}
|
|
321
|
+
if (job.status === "CANCELLED") {
|
|
322
|
+
journal.add({
|
|
323
|
+
method: req.method ?? "PUT",
|
|
324
|
+
path: pathname,
|
|
325
|
+
headers: flattenHeaders(req.headers),
|
|
326
|
+
body: null,
|
|
327
|
+
response: {
|
|
328
|
+
status: 200,
|
|
329
|
+
fixture: null
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
333
|
+
res.end(JSON.stringify({ status: "CANCELLED" }));
|
|
334
|
+
return "handled";
|
|
335
|
+
}
|
|
336
|
+
job.status = "CANCELLED";
|
|
337
|
+
job.logs.push({
|
|
338
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
339
|
+
level: "INFO",
|
|
340
|
+
message: "Job cancelled."
|
|
341
|
+
});
|
|
199
342
|
journal.add({
|
|
200
343
|
method: req.method ?? "PUT",
|
|
201
344
|
path: pathname,
|
|
202
345
|
headers: flattenHeaders(req.headers),
|
|
203
346
|
body: null,
|
|
204
347
|
response: {
|
|
205
|
-
status:
|
|
348
|
+
status: 200,
|
|
206
349
|
fixture: null
|
|
207
350
|
}
|
|
208
351
|
});
|
|
209
|
-
res.writeHead(
|
|
210
|
-
res.end(JSON.stringify({ status: "
|
|
352
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
353
|
+
res.end(JSON.stringify({ status: "CANCELLED" }));
|
|
211
354
|
return "handled";
|
|
355
|
+
}
|
|
212
356
|
case "storage": {
|
|
213
357
|
let filename = "upload.bin";
|
|
214
358
|
try {
|
|
@@ -226,7 +370,29 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
226
370
|
case "queue-submit":
|
|
227
371
|
case "sync-run": {
|
|
228
372
|
const modelId = route.modelId;
|
|
229
|
-
|
|
373
|
+
let parsedBody;
|
|
374
|
+
try {
|
|
375
|
+
parsedBody = parseBody(body);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
const detail = err instanceof Error ? err.message : "Invalid JSON body";
|
|
378
|
+
journal.add({
|
|
379
|
+
method: req.method ?? "POST",
|
|
380
|
+
path: pathname,
|
|
381
|
+
headers: flattenHeaders(req.headers),
|
|
382
|
+
body: null,
|
|
383
|
+
response: {
|
|
384
|
+
status: 400,
|
|
385
|
+
fixture: null
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
389
|
+
res.end(JSON.stringify({ error: {
|
|
390
|
+
message: detail,
|
|
391
|
+
type: "invalid_request_error",
|
|
392
|
+
code: "invalid_json"
|
|
393
|
+
} }));
|
|
394
|
+
return "handled";
|
|
395
|
+
}
|
|
230
396
|
const syntheticReq = {
|
|
231
397
|
model: modelId,
|
|
232
398
|
messages: [{
|
|
@@ -259,21 +425,37 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
259
425
|
}
|
|
260
426
|
if (defaults.record) {
|
|
261
427
|
const effectiveDefaults = withFalUpstream(defaults, route.targetHost);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
});
|
|
276
|
-
|
|
428
|
+
if (route.kind === "queue-submit") {
|
|
429
|
+
if (await proxyAndRecordFalQueueSubmit({
|
|
430
|
+
req,
|
|
431
|
+
res,
|
|
432
|
+
syntheticReq,
|
|
433
|
+
modelId,
|
|
434
|
+
pathname,
|
|
435
|
+
strippedPath: stripFalPrefix(pathname),
|
|
436
|
+
body,
|
|
437
|
+
fixtures,
|
|
438
|
+
defaults: effectiveDefaults,
|
|
439
|
+
stateKey,
|
|
440
|
+
journal
|
|
441
|
+
}) === "handled") return "handled";
|
|
442
|
+
} else {
|
|
443
|
+
const outcome = await proxyAndRecord(req, res, syntheticReq, "fal", stripFalPrefix(pathname), fixtures, effectiveDefaults, body);
|
|
444
|
+
if (outcome === "handled_by_hook") return "handled";
|
|
445
|
+
if (outcome !== "not_configured") {
|
|
446
|
+
journal.add({
|
|
447
|
+
method: req.method ?? "POST",
|
|
448
|
+
path: pathname,
|
|
449
|
+
headers: flattenHeaders(req.headers),
|
|
450
|
+
body: syntheticReq,
|
|
451
|
+
response: {
|
|
452
|
+
status: res.statusCode ?? 200,
|
|
453
|
+
fixture: null,
|
|
454
|
+
source: "proxy"
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return "handled";
|
|
458
|
+
}
|
|
277
459
|
}
|
|
278
460
|
}
|
|
279
461
|
journal.add({
|
|
@@ -350,19 +532,33 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
350
532
|
return "handled";
|
|
351
533
|
}
|
|
352
534
|
const requestId = crypto.randomUUID();
|
|
353
|
-
|
|
535
|
+
const progression = resolveProgression(defaults.falQueue);
|
|
536
|
+
const now = Date.now();
|
|
537
|
+
const initialStatus = progression.pollsBeforeCompleted === 0 ? "COMPLETED" : "IN_QUEUE";
|
|
538
|
+
const job = {
|
|
354
539
|
requestId,
|
|
355
540
|
modelId,
|
|
356
|
-
status:
|
|
541
|
+
status: initialStatus,
|
|
357
542
|
result: payload,
|
|
358
|
-
|
|
359
|
-
|
|
543
|
+
pollCount: 0,
|
|
544
|
+
pollsBeforeInProgress: progression.pollsBeforeInProgress,
|
|
545
|
+
pollsBeforeCompleted: progression.pollsBeforeCompleted,
|
|
546
|
+
submittedAt: now,
|
|
547
|
+
completedAt: initialStatus === "COMPLETED" ? now : null,
|
|
548
|
+
logs: [{
|
|
549
|
+
timestamp: new Date(now).toISOString(),
|
|
550
|
+
level: "INFO",
|
|
551
|
+
message: "Job enqueued."
|
|
552
|
+
}],
|
|
553
|
+
createdAt: now
|
|
554
|
+
};
|
|
555
|
+
falQueueStates.set(stateKey(requestId), job);
|
|
360
556
|
const envelope = {
|
|
361
557
|
request_id: requestId,
|
|
362
558
|
response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,
|
|
363
559
|
status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,
|
|
364
560
|
cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,
|
|
365
|
-
queue_position:
|
|
561
|
+
queue_position: queuePosition(job)
|
|
366
562
|
};
|
|
367
563
|
journal.add({
|
|
368
564
|
method: req.method ?? "POST",
|
|
@@ -384,8 +580,188 @@ function parseBody(raw) {
|
|
|
384
580
|
if (!raw.trim()) return null;
|
|
385
581
|
try {
|
|
386
582
|
return JSON.parse(raw);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
const detail = err instanceof Error ? err.message : "unknown";
|
|
585
|
+
throw new Error(`Malformed JSON: ${detail}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const DEFAULT_FAL_POLL_INTERVAL_MS = 1e3;
|
|
589
|
+
const DEFAULT_FAL_TIMEOUT_MS = 9e5;
|
|
590
|
+
const FAL_STRIP_FORWARD_HEADERS = new Set([
|
|
591
|
+
"connection",
|
|
592
|
+
"keep-alive",
|
|
593
|
+
"transfer-encoding",
|
|
594
|
+
"te",
|
|
595
|
+
"trailer",
|
|
596
|
+
"upgrade",
|
|
597
|
+
"proxy-authorization",
|
|
598
|
+
"proxy-authenticate",
|
|
599
|
+
"host",
|
|
600
|
+
"content-length",
|
|
601
|
+
"cookie",
|
|
602
|
+
"accept-encoding"
|
|
603
|
+
]);
|
|
604
|
+
function buildFalForwardHeaders(req) {
|
|
605
|
+
const out = {};
|
|
606
|
+
for (const [name, val] of Object.entries(req.headers)) {
|
|
607
|
+
if (val === void 0) continue;
|
|
608
|
+
if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;
|
|
609
|
+
out[name] = Array.isArray(val) ? val.join(", ") : val;
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Walk a fal-shaped queue protocol upstream: POST submit, poll status until
|
|
615
|
+
* COMPLETED, GET final result body. Returns the parsed final body so the caller
|
|
616
|
+
* can persist it as the fixture and seed local queue state.
|
|
617
|
+
*
|
|
618
|
+
* Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`
|
|
619
|
+
* audio path (`fal-audio.ts`) can reuse the same logic.
|
|
620
|
+
*/
|
|
621
|
+
async function walkFalQueue(args) {
|
|
622
|
+
const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, fallbackStatusPath, fallbackResultPath } = args;
|
|
623
|
+
const deadline = Date.now() + timeoutMs;
|
|
624
|
+
const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);
|
|
625
|
+
const submitRes = await fetch(submitUrl, {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers,
|
|
628
|
+
body
|
|
629
|
+
});
|
|
630
|
+
const submitText = await submitRes.text();
|
|
631
|
+
if (!submitRes.ok) throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);
|
|
632
|
+
const env = parseJsonOrThrow(submitText, "Submit");
|
|
633
|
+
const upstreamRequestId = String(env.request_id ?? "").trim();
|
|
634
|
+
if (!upstreamRequestId) throw new Error("Submit response missing request_id");
|
|
635
|
+
const envStatusUrl = env.status_url;
|
|
636
|
+
const envResponseUrl = env.response_url;
|
|
637
|
+
const statusUrl = typeof envStatusUrl === "string" && envStatusUrl ? new URL(envStatusUrl) : resolveUpstreamUrl(upstreamBase, fallbackStatusPath(upstreamRequestId));
|
|
638
|
+
const resultUrl = typeof envResponseUrl === "string" && envResponseUrl ? new URL(envResponseUrl) : resolveUpstreamUrl(upstreamBase, fallbackResultPath(upstreamRequestId));
|
|
639
|
+
while (true) {
|
|
640
|
+
if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
|
|
641
|
+
const statusRes = await fetch(statusUrl, { headers });
|
|
642
|
+
const statusText = await statusRes.text();
|
|
643
|
+
if (!statusRes.ok) throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);
|
|
644
|
+
const statusJson = parseJsonOrThrow(statusText, "Status");
|
|
645
|
+
const s = String(statusJson.status ?? "");
|
|
646
|
+
if (s === "COMPLETED") break;
|
|
647
|
+
if (s === "FAILED" || s === "ERROR" || s === "CANCELLED") throw new Error(`Upstream job terminated with status ${s}`);
|
|
648
|
+
const remaining = deadline - Date.now();
|
|
649
|
+
const sleep = Math.min(pollIntervalMs, Math.max(0, remaining));
|
|
650
|
+
if (sleep <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
|
|
651
|
+
await new Promise((r) => setTimeout(r, sleep));
|
|
652
|
+
}
|
|
653
|
+
const resultRes = await fetch(resultUrl, { headers });
|
|
654
|
+
const resultText = await resultRes.text();
|
|
655
|
+
if (!resultRes.ok) throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);
|
|
656
|
+
return parseJsonOrThrow(resultText, "Result");
|
|
657
|
+
}
|
|
658
|
+
async function proxyAndRecordFalQueueSubmit(args) {
|
|
659
|
+
const { req, res, syntheticReq, modelId, pathname, strippedPath, body, fixtures, defaults, stateKey, journal } = args;
|
|
660
|
+
const record = defaults.record;
|
|
661
|
+
if (!record) return "no_upstream";
|
|
662
|
+
const upstreamBase = record.providers.fal;
|
|
663
|
+
if (!upstreamBase) {
|
|
664
|
+
defaults.logger.warn(`No upstream URL configured for provider "fal" — cannot proxy`);
|
|
665
|
+
return "no_upstream";
|
|
666
|
+
}
|
|
667
|
+
defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);
|
|
668
|
+
let finalBody;
|
|
669
|
+
try {
|
|
670
|
+
finalBody = await walkFalQueue({
|
|
671
|
+
upstreamBase,
|
|
672
|
+
submitPath: strippedPath,
|
|
673
|
+
body,
|
|
674
|
+
headers: buildFalForwardHeaders(req),
|
|
675
|
+
pollIntervalMs: record.fal?.pollIntervalMs,
|
|
676
|
+
timeoutMs: record.fal?.timeoutMs,
|
|
677
|
+
fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,
|
|
678
|
+
fallbackResultPath: (id) => `${modelId}/requests/${id}`
|
|
679
|
+
});
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const msg = err instanceof Error ? err.message : "Unknown queue-walk error";
|
|
682
|
+
defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);
|
|
683
|
+
journal.add({
|
|
684
|
+
method: req.method ?? "POST",
|
|
685
|
+
path: pathname,
|
|
686
|
+
headers: flattenHeaders(req.headers),
|
|
687
|
+
body: syntheticReq,
|
|
688
|
+
response: {
|
|
689
|
+
status: 502,
|
|
690
|
+
fixture: null,
|
|
691
|
+
source: "proxy"
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
res.writeHead(502, { "Content-Type": "application/json" });
|
|
695
|
+
res.end(JSON.stringify({ error: {
|
|
696
|
+
message: `Proxy to upstream failed: ${msg}`,
|
|
697
|
+
type: "proxy_error"
|
|
698
|
+
} }));
|
|
699
|
+
return "handled";
|
|
700
|
+
}
|
|
701
|
+
const fixture = {
|
|
702
|
+
match: buildFixtureMatch(defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq, record),
|
|
703
|
+
response: {
|
|
704
|
+
json: finalBody,
|
|
705
|
+
status: 200
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
persistFixture({
|
|
709
|
+
record,
|
|
710
|
+
providerKey: "fal",
|
|
711
|
+
testId: getTestId(req),
|
|
712
|
+
fixture,
|
|
713
|
+
fixtures,
|
|
714
|
+
logger: defaults.logger
|
|
715
|
+
});
|
|
716
|
+
const newRequestId = crypto.randomUUID();
|
|
717
|
+
const progression = resolveProgression(defaults.falQueue);
|
|
718
|
+
const now = Date.now();
|
|
719
|
+
const initialStatus = progression.pollsBeforeCompleted === 0 ? "COMPLETED" : "IN_QUEUE";
|
|
720
|
+
const job = {
|
|
721
|
+
requestId: newRequestId,
|
|
722
|
+
modelId,
|
|
723
|
+
status: initialStatus,
|
|
724
|
+
result: finalBody,
|
|
725
|
+
pollCount: 0,
|
|
726
|
+
pollsBeforeInProgress: progression.pollsBeforeInProgress,
|
|
727
|
+
pollsBeforeCompleted: progression.pollsBeforeCompleted,
|
|
728
|
+
submittedAt: now,
|
|
729
|
+
completedAt: initialStatus === "COMPLETED" ? now : null,
|
|
730
|
+
logs: [{
|
|
731
|
+
timestamp: new Date(now).toISOString(),
|
|
732
|
+
level: "INFO",
|
|
733
|
+
message: "Job enqueued."
|
|
734
|
+
}],
|
|
735
|
+
createdAt: now
|
|
736
|
+
};
|
|
737
|
+
falQueueStates.set(stateKey(newRequestId), job);
|
|
738
|
+
const envelope = {
|
|
739
|
+
request_id: newRequestId,
|
|
740
|
+
response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,
|
|
741
|
+
status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,
|
|
742
|
+
cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,
|
|
743
|
+
queue_position: queuePosition(job)
|
|
744
|
+
};
|
|
745
|
+
journal.add({
|
|
746
|
+
method: req.method ?? "POST",
|
|
747
|
+
path: pathname,
|
|
748
|
+
headers: flattenHeaders(req.headers),
|
|
749
|
+
body: syntheticReq,
|
|
750
|
+
response: {
|
|
751
|
+
status: 200,
|
|
752
|
+
fixture: null,
|
|
753
|
+
source: "proxy"
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
757
|
+
res.end(JSON.stringify(envelope));
|
|
758
|
+
return "handled";
|
|
759
|
+
}
|
|
760
|
+
function parseJsonOrThrow(text, label) {
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(text);
|
|
387
763
|
} catch {
|
|
388
|
-
|
|
764
|
+
throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);
|
|
389
765
|
}
|
|
390
766
|
}
|
|
391
767
|
function withFalUpstream(defaults, targetHost) {
|
|
@@ -435,5 +811,5 @@ function respondNotFound(req, res, pathname, journal, requestId) {
|
|
|
435
811
|
}
|
|
436
812
|
|
|
437
813
|
//#endregion
|
|
438
|
-
export { FalQueueStateMap, falQueueStates, handleFal };
|
|
814
|
+
export { FalQueueStateMap, buildFalForwardHeaders, falQueueStates, handleFal, imageResponseToFalJson, videoResponseToFalJson, walkFalQueue };
|
|
439
815
|
//# sourceMappingURL=fal.js.map
|