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