@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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +32 -0
  4. package/README.md +1 -1
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/config-loader.d.ts.map +1 -1
  7. package/dist/fal-audio.cjs +171 -18
  8. package/dist/fal-audio.cjs.map +1 -1
  9. package/dist/fal-audio.d.cts.map +1 -1
  10. package/dist/fal-audio.d.ts.map +1 -1
  11. package/dist/fal-audio.js +173 -20
  12. package/dist/fal-audio.js.map +1 -1
  13. package/dist/fal.cjs +412 -32
  14. package/dist/fal.cjs.map +1 -1
  15. package/dist/fal.d.cts +16 -1
  16. package/dist/fal.d.cts.map +1 -1
  17. package/dist/fal.d.ts +16 -1
  18. package/dist/fal.d.ts.map +1 -1
  19. package/dist/fal.js +410 -34
  20. package/dist/fal.js.map +1 -1
  21. package/dist/gemini.cjs +4 -2
  22. package/dist/gemini.cjs.map +1 -1
  23. package/dist/gemini.d.cts.map +1 -1
  24. package/dist/gemini.d.ts.map +1 -1
  25. package/dist/gemini.js +4 -2
  26. package/dist/gemini.js.map +1 -1
  27. package/dist/index.cjs +1 -1
  28. package/dist/index.js +1 -1
  29. package/dist/llmock.cjs +18 -1
  30. package/dist/llmock.cjs.map +1 -1
  31. package/dist/llmock.d.cts +13 -1
  32. package/dist/llmock.d.cts.map +1 -1
  33. package/dist/llmock.d.ts +13 -1
  34. package/dist/llmock.d.ts.map +1 -1
  35. package/dist/llmock.js +18 -1
  36. package/dist/llmock.js.map +1 -1
  37. package/dist/recorder.cjs +86 -55
  38. package/dist/recorder.cjs.map +1 -1
  39. package/dist/recorder.d.cts +12 -1
  40. package/dist/recorder.d.cts.map +1 -1
  41. package/dist/recorder.d.ts +12 -1
  42. package/dist/recorder.d.ts.map +1 -1
  43. package/dist/recorder.js +85 -56
  44. package/dist/recorder.js.map +1 -1
  45. package/dist/server.cjs +4 -1
  46. package/dist/server.cjs.map +1 -1
  47. package/dist/server.js +4 -1
  48. package/dist/server.js.map +1 -1
  49. package/dist/types.d.cts +41 -0
  50. package/dist/types.d.cts.map +1 -1
  51. package/dist/types.d.ts +41 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/vector-types.d.ts.map +1 -1
  54. 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 { proxyAndRecord } from "./recorder.js";
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
- writeJson(req, res, 200, {
168
- status: job.status,
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
- if (!falQueueStates.get(stateKey(route.requestId))) {
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: 400,
348
+ status: 200,
206
349
  fixture: null
207
350
  }
208
351
  });
209
- res.writeHead(400, { "Content-Type": "application/json" });
210
- res.end(JSON.stringify({ status: "ALREADY_COMPLETED" }));
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
- const parsedBody = parseBody(body);
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
- const outcome = await proxyAndRecord(req, res, syntheticReq, "fal", stripFalPrefix(pathname), fixtures, effectiveDefaults, body);
263
- if (outcome === "handled_by_hook") return "handled";
264
- if (outcome !== "not_configured") {
265
- journal.add({
266
- method: req.method ?? "POST",
267
- path: pathname,
268
- headers: flattenHeaders(req.headers),
269
- body: syntheticReq,
270
- response: {
271
- status: res.statusCode ?? 200,
272
- fixture: null,
273
- source: "proxy"
274
- }
275
- });
276
- return "handled";
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
- falQueueStates.set(stateKey(requestId), {
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: "COMPLETED",
541
+ status: initialStatus,
357
542
  result: payload,
358
- createdAt: Date.now()
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: 0
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
- return null;
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