@copilotkit/aimock 1.30.0 → 1.31.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 +16 -0
- package/README.md +1 -1
- package/dist/cli.cjs +18 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +18 -1
- package/dist/cli.js.map +1 -1
- package/dist/fal-audio.cjs +10 -4
- 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 +12 -6
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +51 -44
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +8 -0
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +8 -0
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +52 -45
- package/dist/fal.js.map +1 -1
- package/dist/index.cjs +10 -2
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -3
- package/dist/llmock.cjs +2 -1
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +2 -1
- package/dist/llmock.js.map +1 -1
- package/dist/metrics.cjs +9 -0
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +7 -1
- package/dist/metrics.js.map +1 -1
- package/dist/openrouter-video.cjs +1609 -0
- package/dist/openrouter-video.cjs.map +1 -0
- package/dist/openrouter-video.d.cts +154 -0
- package/dist/openrouter-video.d.cts.map +1 -0
- package/dist/openrouter-video.d.ts +154 -0
- package/dist/openrouter-video.d.ts.map +1 -0
- package/dist/openrouter-video.js +1601 -0
- package/dist/openrouter-video.js.map +1 -0
- package/dist/recorder.cjs +48 -7
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +46 -8
- package/dist/recorder.js.map +1 -1
- package/dist/server.cjs +158 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -0
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +159 -11
- package/dist/server.js.map +1 -1
- package/dist/types.d.cts +81 -8
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +81 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/ws-gemini-live.d.cts +2 -2
- package/dist/ws-realtime.d.cts +2 -2
- package/package.json +2 -2
package/dist/fal.cjs
CHANGED
|
@@ -104,11 +104,13 @@ function videoResponseToFalJson(response) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
function resolveProgression(config) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
107
|
+
const sanitize = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
|
|
108
|
+
const explicitInProgress = sanitize(config?.pollsBeforeInProgress);
|
|
109
|
+
const explicitCompleted = sanitize(config?.pollsBeforeCompleted);
|
|
110
|
+
const pollsBeforeInProgress = explicitInProgress ?? 0;
|
|
109
111
|
let pollsBeforeCompleted;
|
|
110
|
-
if (explicitCompleted
|
|
111
|
-
else if (
|
|
112
|
+
if (explicitCompleted !== void 0) pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);
|
|
113
|
+
else if (explicitInProgress !== void 0) pollsBeforeCompleted = pollsBeforeInProgress + 1;
|
|
112
114
|
else pollsBeforeCompleted = 0;
|
|
113
115
|
return {
|
|
114
116
|
pollsBeforeInProgress,
|
|
@@ -592,29 +594,7 @@ function parseBody(raw) {
|
|
|
592
594
|
}
|
|
593
595
|
const DEFAULT_FAL_POLL_INTERVAL_MS = 1e3;
|
|
594
596
|
const DEFAULT_FAL_TIMEOUT_MS = 9e5;
|
|
595
|
-
const
|
|
596
|
-
"connection",
|
|
597
|
-
"keep-alive",
|
|
598
|
-
"transfer-encoding",
|
|
599
|
-
"te",
|
|
600
|
-
"trailer",
|
|
601
|
-
"upgrade",
|
|
602
|
-
"proxy-authorization",
|
|
603
|
-
"proxy-authenticate",
|
|
604
|
-
"host",
|
|
605
|
-
"content-length",
|
|
606
|
-
"cookie",
|
|
607
|
-
"accept-encoding"
|
|
608
|
-
]);
|
|
609
|
-
function buildFalForwardHeaders(req) {
|
|
610
|
-
const out = {};
|
|
611
|
-
for (const [name, val] of Object.entries(req.headers)) {
|
|
612
|
-
if (val === void 0) continue;
|
|
613
|
-
if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;
|
|
614
|
-
out[name] = Array.isArray(val) ? val.join(", ") : val;
|
|
615
|
-
}
|
|
616
|
-
return out;
|
|
617
|
-
}
|
|
597
|
+
const DEFAULT_FAL_FETCH_TIMEOUT_MS = 3e4;
|
|
618
598
|
/**
|
|
619
599
|
* Walk a fal-shaped queue protocol upstream: POST submit, poll status until
|
|
620
600
|
* COMPLETED, GET final result body. Returns the parsed final body so the caller
|
|
@@ -624,38 +604,60 @@ function buildFalForwardHeaders(req) {
|
|
|
624
604
|
* audio path (`fal-audio.ts`) can reuse the same logic.
|
|
625
605
|
*/
|
|
626
606
|
async function walkFalQueue(args) {
|
|
627
|
-
const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, fallbackStatusPath, fallbackResultPath } = args;
|
|
607
|
+
const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, upstreamTimeoutMs, fallbackStatusPath, fallbackResultPath, logger } = args;
|
|
628
608
|
const deadline = Date.now() + timeoutMs;
|
|
609
|
+
const perFetchTimeoutMs = require_recorder.clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);
|
|
610
|
+
const fetchSignal = () => AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));
|
|
629
611
|
const submitUrl = require_url.resolveUpstreamUrl(upstreamBase, submitPath);
|
|
630
612
|
const submitRes = await fetch(submitUrl, {
|
|
631
613
|
method: "POST",
|
|
632
614
|
headers,
|
|
633
|
-
body
|
|
615
|
+
body,
|
|
616
|
+
signal: fetchSignal()
|
|
634
617
|
});
|
|
635
618
|
const submitText = await submitRes.text();
|
|
636
619
|
if (!submitRes.ok) throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);
|
|
637
|
-
const
|
|
620
|
+
const submitJson = parseJsonOrThrow(submitText, "Submit");
|
|
621
|
+
if (submitJson === null || typeof submitJson !== "object" || Array.isArray(submitJson)) throw new Error("Submit response is not a JSON object");
|
|
622
|
+
const env = submitJson;
|
|
638
623
|
const upstreamRequestId = String(env.request_id ?? "").trim();
|
|
639
624
|
if (!upstreamRequestId) throw new Error("Submit response missing request_id");
|
|
640
|
-
const
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
625
|
+
const upstreamOrigin = submitUrl.origin;
|
|
626
|
+
const adoptSameOrigin = (value, label, fallbackPath) => {
|
|
627
|
+
if (typeof value === "string" && value) try {
|
|
628
|
+
const parsed = new URL(value);
|
|
629
|
+
if (parsed.origin === upstreamOrigin) return parsed;
|
|
630
|
+
logger?.warn(`Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`);
|
|
631
|
+
} catch {
|
|
632
|
+
logger?.warn(`Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`);
|
|
633
|
+
}
|
|
634
|
+
return require_url.resolveUpstreamUrl(upstreamBase, fallbackPath);
|
|
635
|
+
};
|
|
636
|
+
const statusUrl = adoptSameOrigin(env.status_url, "status_url", fallbackStatusPath(upstreamRequestId));
|
|
637
|
+
const resultUrl = adoptSameOrigin(env.response_url, "response_url", fallbackResultPath(upstreamRequestId));
|
|
644
638
|
while (true) {
|
|
645
639
|
if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
|
|
646
|
-
const statusRes = await fetch(statusUrl, {
|
|
640
|
+
const statusRes = await fetch(statusUrl, {
|
|
641
|
+
headers,
|
|
642
|
+
signal: fetchSignal()
|
|
643
|
+
});
|
|
647
644
|
const statusText = await statusRes.text();
|
|
648
645
|
if (!statusRes.ok) throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);
|
|
649
|
-
const
|
|
646
|
+
const statusParsed = parseJsonOrThrow(statusText, "Status");
|
|
647
|
+
if (statusParsed === null || typeof statusParsed !== "object" || Array.isArray(statusParsed)) throw new Error("Status response is not a JSON object");
|
|
648
|
+
const statusJson = statusParsed;
|
|
650
649
|
const s = String(statusJson.status ?? "");
|
|
651
650
|
if (s === "COMPLETED") break;
|
|
652
651
|
if (s === "FAILED" || s === "ERROR" || s === "CANCELLED") throw new Error(`Upstream job terminated with status ${s}`);
|
|
653
652
|
const remaining = deadline - Date.now();
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
await new Promise((r) => setTimeout(r, sleep));
|
|
653
|
+
if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
|
|
654
|
+
const sleep = Math.min(pollIntervalMs, remaining);
|
|
655
|
+
await new Promise((r) => setTimeout(r, Math.max(0, sleep)));
|
|
657
656
|
}
|
|
658
|
-
const resultRes = await fetch(resultUrl, {
|
|
657
|
+
const resultRes = await fetch(resultUrl, {
|
|
658
|
+
headers,
|
|
659
|
+
signal: fetchSignal()
|
|
660
|
+
});
|
|
659
661
|
const resultText = await resultRes.text();
|
|
660
662
|
if (!resultRes.ok) throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);
|
|
661
663
|
return parseJsonOrThrow(resultText, "Result");
|
|
@@ -676,15 +678,18 @@ async function proxyAndRecordFalQueueSubmit(args) {
|
|
|
676
678
|
upstreamBase,
|
|
677
679
|
submitPath: strippedPath,
|
|
678
680
|
body,
|
|
679
|
-
headers:
|
|
681
|
+
headers: require_recorder.buildForwardHeaders(req),
|
|
680
682
|
pollIntervalMs: record.fal?.pollIntervalMs,
|
|
681
683
|
timeoutMs: record.fal?.timeoutMs,
|
|
684
|
+
upstreamTimeoutMs: record.upstreamTimeoutMs,
|
|
682
685
|
fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,
|
|
683
|
-
fallbackResultPath: (id) => `${modelId}/requests/${id}
|
|
686
|
+
fallbackResultPath: (id) => `${modelId}/requests/${id}`,
|
|
687
|
+
logger: defaults.logger
|
|
684
688
|
});
|
|
685
689
|
} catch (err) {
|
|
686
690
|
const msg = err instanceof Error ? err.message : "Unknown queue-walk error";
|
|
687
691
|
defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);
|
|
692
|
+
if (res.destroyed || res.writableEnded) return "handled";
|
|
688
693
|
journal.add({
|
|
689
694
|
method: req.method ?? "POST",
|
|
690
695
|
path: pathname,
|
|
@@ -710,7 +715,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
|
|
|
710
715
|
status: 200
|
|
711
716
|
}
|
|
712
717
|
};
|
|
713
|
-
require_recorder.persistFixture({
|
|
718
|
+
const persistResult = require_recorder.persistFixture({
|
|
714
719
|
record,
|
|
715
720
|
providerKey: "fal",
|
|
716
721
|
testId: require_helpers.getTestId(req),
|
|
@@ -718,6 +723,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
|
|
|
718
723
|
fixtures,
|
|
719
724
|
logger: defaults.logger
|
|
720
725
|
});
|
|
726
|
+
if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", require_recorder.sanitizeHeaderValue(persistResult.error));
|
|
721
727
|
const newRequestId = node_crypto.default.randomUUID();
|
|
722
728
|
const progression = resolveProgression(defaults.falQueue);
|
|
723
729
|
const now = Date.now();
|
|
@@ -747,6 +753,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
|
|
|
747
753
|
cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,
|
|
748
754
|
queue_position: queuePosition(job)
|
|
749
755
|
};
|
|
756
|
+
if (res.destroyed || res.writableEnded) return "handled";
|
|
750
757
|
journal.add({
|
|
751
758
|
method: req.method ?? "POST",
|
|
752
759
|
path: pathname,
|
|
@@ -817,10 +824,10 @@ function respondNotFound(req, res, pathname, journal, requestId) {
|
|
|
817
824
|
|
|
818
825
|
//#endregion
|
|
819
826
|
exports.FalQueueStateMap = FalQueueStateMap;
|
|
820
|
-
exports.buildFalForwardHeaders = buildFalForwardHeaders;
|
|
821
827
|
exports.falQueueStates = falQueueStates;
|
|
822
828
|
exports.handleFal = handleFal;
|
|
823
829
|
exports.imageResponseToFalJson = imageResponseToFalJson;
|
|
830
|
+
exports.resolveProgression = resolveProgression;
|
|
824
831
|
exports.videoResponseToFalJson = videoResponseToFalJson;
|
|
825
832
|
exports.walkFalQueue = walkFalQueue;
|
|
826
833
|
//# sourceMappingURL=fal.cjs.map
|
package/dist/fal.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fal.cjs","names":["getTestId","flattenHeaders","crypto","getContext","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isJSONResponse","isAudioResponse","audioToFalFile","resolveUpstreamUrl","buildFixtureMatch"],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getContext,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { buildFixtureMatch, persistFixture, proxyAndRecord } from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nfunction resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n const pollsBeforeInProgress = config?.pollsBeforeInProgress ?? 0;\n const explicitCompleted = config?.pollsBeforeCompleted;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted != null) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (config?.pollsBeforeInProgress != null) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n writeJson(req, res, 200, job.result, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n _context: getContext(req),\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n matchCounts,\n defaults.requestTransform,\n );\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(\n strictNoMatchLogLine(req.method ?? \"POST\", pathname, skippedBySequenceOrTurn),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to strict/404\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n\n// Hop-by-hop and client-set headers excluded from upstream forwarding.\n// Mirrors STRIP_HEADERS in recorder.ts.\nconst FAL_STRIP_FORWARD_HEADERS = new Set([\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authorization\",\n \"proxy-authenticate\",\n \"host\",\n \"content-length\",\n \"cookie\",\n \"accept-encoding\",\n]);\n\nexport function buildFalForwardHeaders(req: http.IncomingMessage): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [name, val] of Object.entries(req.headers)) {\n if (val === undefined) continue;\n if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;\n out[name] = Array.isArray(val) ? val.join(\", \") : val;\n }\n return out;\n}\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result body. Returns the parsed final body so the caller\n * can persist it as the fixture and seed local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n}): Promise<unknown> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n fallbackStatusPath,\n fallbackResultPath,\n } = args;\n\n const deadline = Date.now() + timeoutMs;\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, { method: \"POST\", headers, body });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — a proxy in front of fal.ai may sit on\n // a different host than the canonical `queue.fal.run` — and only fall back\n // to constructed paths if the envelope omits them.\n const envStatusUrl = env.status_url;\n const envResponseUrl = env.response_url;\n const statusUrl =\n typeof envStatusUrl === \"string\" && envStatusUrl\n ? new URL(envStatusUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackStatusPath(upstreamRequestId));\n const resultUrl =\n typeof envResponseUrl === \"string\" && envResponseUrl\n ? new URL(envResponseUrl)\n : resolveUpstreamUrl(upstreamBase, fallbackResultPath(upstreamRequestId));\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusJson = parseJsonOrThrow(statusText, \"Status\") as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n const sleep = Math.min(pollIntervalMs, Math.max(0, remaining));\n if (sleep <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n await new Promise<void>((r) => setTimeout(r, sleep));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return parseJsonOrThrow(resultText, \"Result\");\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n try {\n finalBody = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildFalForwardHeaders(req),\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n response: { json: finalBody, status: 200 },\n };\n persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fal.cjs","names":["getTestId","flattenHeaders","crypto","getContext","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isJSONResponse","isAudioResponse","audioToFalFile","clampTimeout","resolveUpstreamUrl","buildForwardHeaders","buildFixtureMatch","persistFixture","sanitizeHeaderValue"],"sources":["../src/fal.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n FalQueueConfig,\n Fixture,\n HandlerDefaults,\n ImageItem,\n ImageResponse,\n RawJSONResponse,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n isJSONResponse,\n flattenHeaders,\n getContext,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport type { Logger } from \"./logger.js\";\nimport {\n buildFixtureMatch,\n buildForwardHeaders,\n clampTimeout,\n persistFixture,\n proxyAndRecord,\n sanitizeHeaderValue,\n} from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport type { Journal } from \"./journal.js\";\nimport { audioToFalFile } from \"./fal-audio.js\";\n\n// ─── FalQueueState (TTL + bounded) ───────────────────────────────────────\n\nconst FAL_QUEUE_MAX_ENTRIES = 10_000;\nconst FAL_QUEUE_TTL_MS = 3_600_000; // 1 hour\n\ntype FalQueueStatus = \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\" | \"CANCELLED\";\n\ninterface FalQueueJob {\n requestId: string;\n modelId: string;\n status: FalQueueStatus;\n result: unknown;\n /** Number of `/status` (or `/{id}`) polls the caller has made against this job. */\n pollCount: number;\n /** Poll-count threshold for `IN_QUEUE → IN_PROGRESS` transition. */\n pollsBeforeInProgress: number;\n /** Poll-count threshold for `IN_PROGRESS → COMPLETED` transition. */\n pollsBeforeCompleted: number;\n submittedAt: number;\n completedAt: number | null;\n /** State-transition log entries surfaced in the `/status` response. */\n logs: Array<{ timestamp: string; level: string; message: string }>;\n createdAt: number;\n}\n\ninterface FalQueueEntry {\n job: FalQueueJob;\n createdAt: number;\n}\n\n/**\n * Per-testId queue state for the general fal handler. Mirrors FalJobMap from\n * fal-audio.ts but stores arbitrary JSON payloads instead of audio file\n * objects, so it can serve any fal model (image, video, motion, music, etc.).\n */\nexport class FalQueueStateMap {\n private readonly entries = new Map<string, FalQueueEntry>();\n\n get(key: string): FalQueueJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_QUEUE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalQueueJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > FAL_QUEUE_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_QUEUE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport const falQueueStates = new FalQueueStateMap();\n\n// ─── Typed-response → fal envelope converters ───────────────────────────\n\nfunction extractExtension(url: string, fallback: string): { fileName: string; ext: string } {\n const segment = url.split(\"?\")[0].split(\"#\")[0].split(\"/\").pop() ?? \"\";\n const fileName = segment.length > 0 ? segment : \"\";\n const dotIdx = fileName.lastIndexOf(\".\");\n const ext = dotIdx >= 0 ? fileName.slice(dotIdx + 1).toLowerCase() : fallback;\n return { fileName, ext };\n}\n\nfunction imageItemToFalImage(item: ImageItem, index: number): Record<string, unknown> {\n const url = item.url ?? `https://mock.fal.media/files/generated_image_${index}.png`;\n const { ext } = extractExtension(url, \"png\");\n const contentType = ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\" : `image/${ext}`;\n return {\n url,\n width: 1024,\n height: 1024,\n content_type: contentType,\n };\n}\n\n/**\n * Translate an `ImageResponse` fixture into fal's image envelope shape:\n * `{ images: [...], timings, seed, has_nsfw_concepts, prompt }`.\n * Used by `LLMock.onFalImage` to keep callers from re-deriving the wire shape.\n */\nexport function imageResponseToFalJson(response: ImageResponse): Record<string, unknown> {\n const items = response.images ?? (response.image ? [response.image] : []);\n const images = items.map((item, i) => imageItemToFalImage(item, i));\n return {\n images,\n timings: { inference: 0 },\n seed: 0,\n has_nsfw_concepts: images.map(() => false),\n prompt: \"\",\n };\n}\n\n/**\n * Translate a `VideoResponse` fixture into fal's video envelope shape:\n * `{ video: { url, content_type, file_name, file_size }, seed }`.\n */\nexport function videoResponseToFalJson(response: VideoResponse): Record<string, unknown> {\n const url = response.video.url ?? \"https://mock.fal.media/files/generated_video.mp4\";\n const { fileName, ext } = extractExtension(url, \"mp4\");\n return {\n video: {\n url,\n content_type: `video/${ext}`,\n file_name: fileName || \"generated_video.mp4\",\n file_size: 0,\n },\n seed: 0,\n };\n}\n\n// ─── Queue progression ─────────────────────────────────────────────────\n\nexport function resolveProgression(config: FalQueueConfig | undefined): {\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n} {\n // Thresholds must resolve to non-negative integers: a NaN would make\n // advanceJob's `pollCount >= threshold` comparison permanently false (a\n // polling client never reaches terminal), and a negative would break the\n // documented \"explicit 0 enables progression\" contract. Non-finite values\n // are treated as unset; anything else is floored and clamped to >= 0.\n const sanitize = (value: number | undefined): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value)\n ? Math.max(0, Math.floor(value))\n : undefined;\n const explicitInProgress = sanitize(config?.pollsBeforeInProgress);\n const explicitCompleted = sanitize(config?.pollsBeforeCompleted);\n const pollsBeforeInProgress = explicitInProgress ?? 0;\n // When only pollsBeforeInProgress is set, default pollsBeforeCompleted to one\n // poll later so the job actually passes through IN_PROGRESS. When the caller\n // sets both explicitly, clamp completed >= inProgress so a misconfigured\n // pair (e.g. completed < inProgress) can't silently skip the IN_PROGRESS\n // transition. When neither is set, both stay 0 (completes on submit).\n let pollsBeforeCompleted: number;\n if (explicitCompleted !== undefined) {\n pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);\n } else if (explicitInProgress !== undefined) {\n pollsBeforeCompleted = pollsBeforeInProgress + 1;\n } else {\n pollsBeforeCompleted = 0;\n }\n return { pollsBeforeInProgress, pollsBeforeCompleted };\n}\n\n/**\n * Mutates a job in place to advance its state on a status/result poll.\n * IN_QUEUE → IN_PROGRESS → COMPLETED based on poll-count thresholds. No-op\n * once COMPLETED or CANCELLED.\n */\nfunction advanceJob(job: FalQueueJob): void {\n if (job.status === \"COMPLETED\" || job.status === \"CANCELLED\") return;\n\n job.pollCount += 1;\n // Check IN_PROGRESS before COMPLETED so a job whose thresholds are equal\n // still spends one poll in IN_PROGRESS instead of jumping straight to\n // COMPLETED.\n if (job.status === \"IN_QUEUE\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"IN_PROGRESS\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job started processing.\",\n });\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = \"COMPLETED\";\n job.completedAt = Date.now();\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job completed.\",\n });\n }\n}\n\nfunction queuePosition(job: FalQueueJob): number {\n if (job.status !== \"IN_QUEUE\") return 0;\n return Math.max(0, job.pollsBeforeInProgress - job.pollCount);\n}\n\nfunction statusResponseBody(job: FalQueueJob): Record<string, unknown> {\n const body: Record<string, unknown> = {\n status: job.status,\n request_id: job.requestId,\n response_url: `https://${FAL_HOSTS.queue}/${job.modelId}/requests/${job.requestId}`,\n logs: job.logs,\n };\n if (job.status === \"IN_QUEUE\" || job.status === \"IN_PROGRESS\") {\n body.queue_position = queuePosition(job);\n }\n if (job.status === \"COMPLETED\" && job.completedAt != null) {\n body.metrics = {\n inference_time: (job.completedAt - job.submittedAt) / 1000,\n };\n }\n return body;\n}\n\n// ─── Hosts and routing ──────────────────────────────────────────────────\n\nconst FAL_HOSTS = {\n queue: \"queue.fal.run\",\n sync: \"fal.run\",\n storage: \"rest.fal.ai\",\n storageAlpha: \"rest.alpha.fal.ai\",\n gateway: \"gateway.fal.ai\",\n} as const;\n\nconst QUEUE_REQUESTS_RE = /^(.+)\\/requests\\/([^/]+)(\\/status|\\/cancel)?$/;\nconst STORAGE_INITIATE_PATH = \"/storage/upload/initiate\";\n\nfunction stripFalPrefix(pathname: string): string {\n const stripped = pathname.replace(/^\\/fal/, \"\");\n return stripped.length > 0 ? stripped : \"/\";\n}\n\nfunction extractPromptFromBody(body: unknown): string {\n if (!body || typeof body !== \"object\") return \"\";\n const obj = body as Record<string, unknown>;\n if (typeof obj.prompt === \"string\") return obj.prompt;\n if (typeof obj.text === \"string\") return obj.text;\n const input = obj.input;\n if (input && typeof input === \"object\") {\n const inputObj = input as Record<string, unknown>;\n if (typeof inputObj.prompt === \"string\") return inputObj.prompt;\n if (typeof inputObj.text === \"string\") return inputObj.text;\n }\n return \"\";\n}\n\ninterface ParsedFalPath {\n modelId: string;\n requestId?: string;\n action?: \"status\" | \"cancel\" | \"result\";\n}\n\nfunction parseFalPath(stripped: string): ParsedFalPath | null {\n if (!stripped.startsWith(\"/\")) return null;\n const trimmed = stripped.replace(/^\\/+/, \"\");\n if (!trimmed) return null;\n\n const m = QUEUE_REQUESTS_RE.exec(`/${trimmed}`);\n if (m) {\n const modelId = m[1].replace(/^\\/+/, \"\");\n const action = m[3] === \"/status\" ? \"status\" : m[3] === \"/cancel\" ? \"cancel\" : \"result\";\n return { modelId, requestId: m[2], action };\n }\n return { modelId: trimmed };\n}\n\nexport type HandleFalOutcome = \"handled\" | \"passthrough\";\n\ninterface FalRouteInfo {\n kind: \"queue-submit\" | \"queue-status\" | \"queue-result\" | \"queue-cancel\" | \"sync-run\" | \"storage\";\n modelId?: string;\n requestId?: string;\n targetHost: string;\n}\n\nfunction classifyRoute(\n req: http.IncomingMessage,\n pathname: string,\n targetHost: string,\n): FalRouteInfo | null {\n const stripped = stripFalPrefix(pathname);\n\n if (targetHost === FAL_HOSTS.storage || targetHost === FAL_HOSTS.storageAlpha) {\n if (req.method === \"POST\" && stripped === STORAGE_INITIATE_PATH) {\n return { kind: \"storage\", targetHost };\n }\n return null;\n }\n\n const parsed = parseFalPath(stripped);\n if (!parsed) return null;\n\n if (targetHost === FAL_HOSTS.queue) {\n if (parsed.requestId) {\n if (parsed.action === \"status\" && req.method === \"GET\") {\n return {\n kind: \"queue-status\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"cancel\" && req.method === \"PUT\") {\n return {\n kind: \"queue-cancel\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n if (parsed.action === \"result\" && req.method === \"GET\") {\n return {\n kind: \"queue-result\",\n modelId: parsed.modelId,\n requestId: parsed.requestId,\n targetHost,\n };\n }\n return null;\n }\n if (req.method === \"POST\") {\n return { kind: \"queue-submit\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n if (targetHost === FAL_HOSTS.sync) {\n if (req.method === \"POST\" && parsed.modelId) {\n return { kind: \"sync-run\", modelId: parsed.modelId, targetHost };\n }\n return null;\n }\n\n return null;\n}\n\n/**\n * General fal.ai handler. Routes by `x-fal-target-host` header (the convention\n * used by `@fal-ai/client`'s server-side requestMiddleware workaround for the\n * fact that `proxyUrl` is browser-only).\n *\n * Returns `\"passthrough\"` when the request does not look like a host-mirrored\n * fal call, so the caller can fall back to the legacy `/fal/queue/...` and\n * `/fal/run/...` audio routes.\n */\nexport async function handleFal(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<HandleFalOutcome> {\n const targetHostHeader = req.headers[\"x-fal-target-host\"];\n const targetHost = Array.isArray(targetHostHeader) ? targetHostHeader[0] : targetHostHeader;\n if (!targetHost) return \"passthrough\";\n\n const route = classifyRoute(req, pathname, targetHost);\n if (!route) return \"passthrough\";\n\n const testId = getTestId(req);\n const stateKey = (id: string) => `${testId}:${id}`;\n\n switch (route.kind) {\n case \"queue-status\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n advanceJob(job);\n writeJson(req, res, 200, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n\n case \"queue-result\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n respondNotFound(req, res, pathname, journal, route.requestId!);\n return \"handled\";\n }\n // Callers may fetch result without first polling status — advance so\n // tests that skip the status check still reach completion.\n advanceJob(job);\n if (job.status !== \"COMPLETED\") {\n writeJson(req, res, 202, statusResponseBody(job), pathname, journal);\n return \"handled\";\n }\n writeJson(req, res, 200, job.result, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-cancel\": {\n const job = falQueueStates.get(stateKey(route.requestId!));\n if (!job) {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return \"handled\";\n }\n if (job.status === \"COMPLETED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n return \"handled\";\n }\n if (job.status === \"CANCELLED\") {\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n job.status = \"CANCELLED\";\n job.logs.push({\n timestamp: new Date().toISOString(),\n level: \"INFO\",\n message: \"Job cancelled.\",\n });\n journal.add({\n method: req.method ?? \"PUT\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"CANCELLED\" }));\n return \"handled\";\n }\n\n case \"storage\": {\n let filename = \"upload.bin\";\n try {\n const parsed = body ? (JSON.parse(body) as Record<string, unknown>) : {};\n if (typeof parsed.filename === \"string\") filename = parsed.filename;\n if (typeof parsed.file_name === \"string\") filename = parsed.file_name;\n } catch {\n // ignore — stub doesn't require a structured body\n }\n const fileId = crypto.randomUUID();\n const responseBody = {\n upload_url: `https://${route.targetHost}/storage/upload/${fileId}`,\n file_url: `https://${route.targetHost}/files/${fileId}/${filename}`,\n };\n writeJson(req, res, 200, responseBody, pathname, journal);\n return \"handled\";\n }\n\n case \"queue-submit\":\n case \"sync-run\": {\n const modelId = route.modelId!;\n let parsedBody: Record<string, unknown> | null;\n try {\n parsedBody = parseBody(body);\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"Invalid JSON body\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: detail,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return \"handled\";\n }\n const prompt = extractPromptFromBody(parsedBody);\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt || JSON.stringify(parsedBody ?? {}) }],\n _endpointType: \"fal\",\n _context: getContext(req),\n };\n\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n matchCounts,\n defaults.requestTransform,\n );\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(\n strictNoMatchLogLine(req.method ?? \"POST\", pathname, skippedBySequenceOrTurn),\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n if (defaults.record) {\n const effectiveDefaults = withFalUpstream(defaults, route.targetHost);\n // queue-submit must walk the queue upstream (submit → poll status →\n // get result) before persisting, so the fixture stores the FINAL job\n // body, not the IN_QUEUE envelope. sync-run is already a single\n // request/response cycle and the generic recorder handles it.\n if (route.kind === \"queue-submit\") {\n const outcome = await proxyAndRecordFalQueueSubmit({\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath: stripFalPrefix(pathname),\n body,\n fixtures,\n defaults: effectiveDefaults,\n stateKey,\n journal,\n });\n if (outcome === \"handled\") return \"handled\";\n // outcome === \"no_upstream\" — fall through to 404 (strict was\n // already handled above)\n } else {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n stripFalPrefix(pathname),\n fixtures,\n effectiveDefaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return \"handled\";\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return \"handled\";\n }\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return \"handled\";\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return \"handled\";\n }\n\n let payload: unknown;\n if (isJSONResponse(response)) {\n payload = (response as RawJSONResponse).json;\n } else if (isAudioResponse(response)) {\n payload = audioToFalFile(response);\n } else {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Fixture response is not JSON or audio for fal endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return \"handled\";\n }\n\n if (route.kind === \"sync-run\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n return \"handled\";\n }\n\n const requestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId,\n modelId,\n status: initialStatus,\n result: payload,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [\n {\n timestamp: new Date(now).toISOString(),\n level: \"INFO\",\n message: \"Job enqueued.\",\n },\n ],\n createdAt: now,\n };\n falQueueStates.set(stateKey(requestId), job);\n const envelope = {\n request_id: requestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${requestId}/cancel`,\n queue_position: queuePosition(job),\n };\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n }\n }\n}\n\nfunction parseBody(raw: string): Record<string, unknown> | null {\n if (!raw.trim()) return null;\n try {\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err) {\n const detail = err instanceof Error ? err.message : \"unknown\";\n throw new Error(`Malformed JSON: ${detail}`);\n }\n}\n\n// ─── Queue-walk recording ──────────────────────────────────────────────\n//\n// The fal queue protocol surfaces three endpoints — submit (POST), status\n// (GET, polled), and result (GET) — but at the fixture layer we only store ONE\n// thing: the FINAL job body. A naive `proxyAndRecord` against submit would\n// persist the IN_QUEUE envelope, which is useless to replay (the SDK polls\n// status and then reads the result body, expecting `{ images: [...] }`-shaped\n// model output, not an envelope). So during recording we walk the upstream\n// queue ourselves, capture the result body, and write THAT as the fixture —\n// then synthesise the local envelope the same way the replay path does.\n\nconst DEFAULT_FAL_POLL_INTERVAL_MS = 1000;\n// Video generations (kling, veo, runway, etc.) routinely take 5–10 minutes\n// on the upstream queue; 15 min gives headroom without trapping a genuinely\n// hung job indefinitely.\nconst DEFAULT_FAL_TIMEOUT_MS = 900_000;\n// Per-fetch upstream timeout default for the walk's submit/status/result\n// fetches — same value and clamp conventions as the rest of the recording\n// surfaces (recorder.ts / openrouter-video.ts).\nconst DEFAULT_FAL_FETCH_TIMEOUT_MS = 30_000;\n\n// Upstream header forwarding uses buildForwardHeaders (recorder.ts) — the\n// shared strip list (hop-by-hop, client-set, and the mock-internal x-test-id\n// / x-aimock-strict / x-aimock-context / x-aimock-chaos-* family), so the\n// fal queue walk never leaks control headers onto a real provider's wire\n// (one shared list, not a per-surface copy, so the surfaces cannot drift).\n\n/**\n * Walk a fal-shaped queue protocol upstream: POST submit, poll status until\n * COMPLETED, GET final result body. Returns the parsed final body so the caller\n * can persist it as the fixture and seed local queue state.\n *\n * Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`\n * audio path (`fal-audio.ts`) can reuse the same logic.\n */\nexport async function walkFalQueue(args: {\n upstreamBase: string;\n submitPath: string;\n body: string;\n headers: Record<string, string>;\n pollIntervalMs?: number;\n timeoutMs?: number;\n /**\n * Per-fetch upstream timeout (`record.upstreamTimeoutMs` clamp conventions,\n * 30s default) applied to each of the walk's submit/status/result fetches\n * via AbortSignal — a hung upstream socket must not pin the walk past its\n * budget. Each fetch's signal is additionally clamped to the walk's\n * remaining deadline; an abort surfaces through the caller's existing\n * failure handling (502, no fixture persisted).\n */\n upstreamTimeoutMs?: number;\n /**\n * Build the status-poll URL from `request_id` when upstream's submit\n * response doesn't return a usable `status_url`. The legacy path uses\n * aimock-internal `/fal/queue/requests/<id>/status` rather than fal.ai's\n * `/<model>/requests/<id>/status` layout.\n */\n fallbackStatusPath: (requestId: string) => string;\n fallbackResultPath: (requestId: string) => string;\n /** Warn sink for the same-origin envelope-URL gate (omitting it only mutes the warns). */\n logger?: Logger;\n}): Promise<unknown> {\n const {\n upstreamBase,\n submitPath,\n body,\n headers,\n pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS,\n timeoutMs = DEFAULT_FAL_TIMEOUT_MS,\n upstreamTimeoutMs,\n fallbackStatusPath,\n fallbackResultPath,\n logger,\n } = args;\n\n const deadline = Date.now() + timeoutMs;\n const perFetchTimeoutMs = clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);\n // Bound every upstream fetch: the per-fetch timeout, additionally clamped\n // to the walk's remaining budget so a fetch can never outlive the deadline\n // (floored at 1ms — AbortSignal.timeout rejects non-positive values; the\n // deadline check in the poll loop is the authoritative expiry).\n const fetchSignal = (): AbortSignal =>\n AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));\n\n // ── 1. POST submit ────────────────────────────────────────────────\n const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n const submitRes = await fetch(submitUrl, {\n method: \"POST\",\n headers,\n body,\n signal: fetchSignal(),\n });\n const submitText = await submitRes.text();\n if (!submitRes.ok) {\n throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);\n }\n const submitJson = parseJsonOrThrow(submitText, \"Submit\");\n // JSON.parse admits null/arrays/scalars — reject non-object envelopes with\n // a clean error instead of TypeError-ing on the field reads below (the\n // OpenRouter video proxies apply the same guard).\n if (submitJson === null || typeof submitJson !== \"object\" || Array.isArray(submitJson)) {\n throw new Error(\"Submit response is not a JSON object\");\n }\n const env = submitJson as Record<string, unknown>;\n const upstreamRequestId = String(env.request_id ?? \"\").trim();\n if (!upstreamRequestId) {\n throw new Error(\"Submit response missing request_id\");\n }\n\n // Prefer the URLs upstream returned — but ONLY same-origin with the\n // configured upstream (mirroring the OpenRouter video proxy's\n // polling_url gate): every status/result fetch below forwards the client's\n // headers — including Authorization — so an envelope nominating a foreign\n // host must never receive them. Same-origin still covers the documented\n // proxy-in-front case (the configured upstream IS that proxy); off-origin,\n // unparseable, or absent envelope URLs fall back to the constructed\n // canonical paths on the upstream origin, with a warn.\n const upstreamOrigin = submitUrl.origin;\n const adoptSameOrigin = (value: unknown, label: string, fallbackPath: string): URL => {\n if (typeof value === \"string\" && value) {\n try {\n const parsed = new URL(value);\n if (parsed.origin === upstreamOrigin) return parsed;\n logger?.warn(\n `Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`,\n );\n } catch {\n logger?.warn(\n `Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`,\n );\n }\n }\n return resolveUpstreamUrl(upstreamBase, fallbackPath);\n };\n const statusUrl = adoptSameOrigin(\n env.status_url,\n \"status_url\",\n fallbackStatusPath(upstreamRequestId),\n );\n const resultUrl = adoptSameOrigin(\n env.response_url,\n \"response_url\",\n fallbackResultPath(upstreamRequestId),\n );\n\n // ── 2. Poll status until COMPLETED ───────────────────────────────\n while (true) {\n if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const statusRes = await fetch(statusUrl, { headers, signal: fetchSignal() });\n const statusText = await statusRes.text();\n if (!statusRes.ok) {\n throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);\n }\n const statusParsed = parseJsonOrThrow(statusText, \"Status\");\n // Same non-object guard as the submit envelope above.\n if (statusParsed === null || typeof statusParsed !== \"object\" || Array.isArray(statusParsed)) {\n throw new Error(\"Status response is not a JSON object\");\n }\n const statusJson = statusParsed as Record<string, unknown>;\n const s = String(statusJson.status ?? \"\");\n if (s === \"COMPLETED\") break;\n if (s === \"FAILED\" || s === \"ERROR\" || s === \"CANCELLED\") {\n throw new Error(`Upstream job terminated with status ${s}`);\n }\n const remaining = deadline - Date.now();\n // Time out only when the budget is actually exhausted — a zero\n // pollIntervalMs is a valid \"poll as fast as possible\" configuration,\n // not an instant expiry (setTimeout(0) still yields the event loop).\n if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);\n const sleep = Math.min(pollIntervalMs, remaining);\n await new Promise<void>((r) => setTimeout(r, Math.max(0, sleep)));\n }\n\n // ── 3. GET final result ──────────────────────────────────────────\n const resultRes = await fetch(resultUrl, { headers, signal: fetchSignal() });\n const resultText = await resultRes.text();\n if (!resultRes.ok) {\n throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);\n }\n return parseJsonOrThrow(resultText, \"Result\");\n}\n\nasync function proxyAndRecordFalQueueSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n syntheticReq: ChatCompletionRequest;\n modelId: string;\n pathname: string;\n strippedPath: string;\n body: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n stateKey: (id: string) => string;\n journal: Journal;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const {\n req,\n res,\n syntheticReq,\n modelId,\n pathname,\n strippedPath,\n body,\n fixtures,\n defaults,\n stateKey,\n journal,\n } = args;\n\n const record = defaults.record;\n if (!record) return \"no_upstream\";\n const upstreamBase = record.providers.fal;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"fal\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n defaults.logger.warn(`NO FIXTURE MATCH — walking fal queue at ${upstreamBase}${strippedPath}`);\n\n let finalBody: unknown;\n try {\n finalBody = await walkFalQueue({\n upstreamBase,\n submitPath: strippedPath,\n body,\n headers: buildForwardHeaders(req),\n pollIntervalMs: record.fal?.pollIntervalMs,\n timeoutMs: record.fal?.timeoutMs,\n upstreamTimeoutMs: record.upstreamTimeoutMs,\n fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,\n fallbackResultPath: (id) => `${modelId}/requests/${id}`,\n logger: defaults.logger,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown queue-walk error\";\n defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 502, fixture: null, source: \"proxy\" },\n });\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n }\n\n // ── 4. Persist fixture using the FINAL body, not the submit envelope ──\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const fixture: Fixture = {\n match: buildFixtureMatch(matchRequest, record),\n response: { json: finalBody, status: 200 },\n };\n const persistResult = persistFixture({\n record,\n providerKey: \"fal\",\n testId: getTestId(req),\n fixture,\n fixtures,\n logger: defaults.logger,\n });\n // Surface a persist failure on the envelope (parity with the generic\n // recorder relay and the OpenRouter failed branch) — the synthesized\n // envelope below has not been written yet, so the header can still ride it.\n if (persistResult.kind === \"failed\" && !res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n }\n\n // ── 5. Synthesise envelope + seed state (same shape as the replay path) ──\n const newRequestId = crypto.randomUUID();\n const progression = resolveProgression(defaults.falQueue);\n const now = Date.now();\n const initialStatus: FalQueueStatus =\n progression.pollsBeforeCompleted === 0 ? \"COMPLETED\" : \"IN_QUEUE\";\n const job: FalQueueJob = {\n requestId: newRequestId,\n modelId,\n status: initialStatus,\n result: finalBody,\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n submittedAt: now,\n completedAt: initialStatus === \"COMPLETED\" ? now : null,\n logs: [{ timestamp: new Date(now).toISOString(), level: \"INFO\", message: \"Job enqueued.\" }],\n createdAt: now,\n };\n falQueueStates.set(stateKey(newRequestId), job);\n const envelope = {\n request_id: newRequestId,\n response_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}`,\n status_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/status`,\n cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,\n queue_position: queuePosition(job),\n };\n // Guard BEFORE journaling (openrouter-video convention): a client that\n // disconnected during the multi-second walk gets neither a write nor a\n // journal entry. The persisted fixture and seeded state above stay — the\n // captured upstream response is valuable regardless.\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null, source: \"proxy\" },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(envelope));\n return \"handled\";\n}\n\nfunction parseJsonOrThrow(text: string, label: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n throw new Error(`${label} returned non-JSON: ${text.slice(0, 200)}`);\n }\n}\n\nfunction withFalUpstream(defaults: HandlerDefaults, targetHost: string): HandlerDefaults {\n if (!defaults.record) return defaults;\n // Respect an explicit record.providers.fal — tests and dev configs need to\n // point at a stub upstream. Only synthesise from the header when the user\n // didn't configure one (the \"or omit upstream URL — it's in the request\n // hostname\" mode from the issue).\n if (defaults.record.providers.fal) return defaults;\n return {\n ...defaults,\n record: {\n ...defaults.record,\n providers: {\n ...defaults.record.providers,\n fal: `https://${targetHost}`,\n },\n },\n };\n}\n\nfunction writeJson(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n status: number,\n payload: unknown,\n pathname: string,\n journal: Journal,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status, fixture: null },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(payload));\n}\n\nfunction respondNotFound(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n journal: Journal,\n requestId: string,\n): void {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;AA2CA,MAAM,wBAAwB;AAC9B,MAAM,mBAAmB;;;;;;AAgCzB,IAAa,mBAAb,MAA8B;CAC5B,AAAiB,0BAAU,IAAI,KAA4B;CAE3D,IAAI,KAAsC;EACxC,MAAM,QAAQ,KAAK,QAAQ,IAAI,IAAI;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,KAAK,KAAK,GAAG,MAAM,YAAY,kBAAkB;AACnD,QAAK,QAAQ,OAAO,IAAI;AACxB;;AAEF,SAAO,MAAM;;CAGf,IAAI,KAAa,KAAwB;AACvC,OAAK,QAAQ,IAAI,KAAK;GAAE;GAAK,WAAW,KAAK,KAAK;GAAE,CAAC;AACrD,MAAI,KAAK,QAAQ,OAAO,uBAAuB;GAC7C,MAAM,SAAS,KAAK,QAAQ,OAAO;GACnC,MAAM,OAAO,KAAK,QAAQ,MAAM;AAChC,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,KAAK;IAC/B,MAAM,OAAO,KAAK,MAAM;AACxB,QAAI,CAAC,KAAK,KAAM,MAAK,QAAQ,OAAO,KAAK,MAAM;;;;CAKrD,OAAO,KAAsB;AAC3B,SAAO,KAAK,QAAQ,OAAO,IAAI;;CAGjC,QAAc;AACZ,OAAK,QAAQ,OAAO;;CAGtB,IAAI,OAAe;AACjB,SAAO,KAAK,QAAQ;;;AAIxB,MAAa,iBAAiB,IAAI,kBAAkB;AAIpD,SAAS,iBAAiB,KAAa,UAAqD;CAC1F,MAAM,UAAU,IAAI,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,IAAI;CACpE,MAAM,WAAW,QAAQ,SAAS,IAAI,UAAU;CAChD,MAAM,SAAS,SAAS,YAAY,IAAI;AAExC,QAAO;EAAE;EAAU,KADP,UAAU,IAAI,SAAS,MAAM,SAAS,EAAE,CAAC,aAAa,GAAG;EAC7C;;AAG1B,SAAS,oBAAoB,MAAiB,OAAwC;CACpF,MAAM,MAAM,KAAK,OAAO,gDAAgD,MAAM;CAC9E,MAAM,EAAE,QAAQ,iBAAiB,KAAK,MAAM;AAE5C,QAAO;EACL;EACA,OAAO;EACP,QAAQ;EACR,cALkB,QAAQ,SAAS,QAAQ,SAAS,eAAe,SAAS;EAM7E;;;;;;;AAQH,SAAgB,uBAAuB,UAAkD;CAEvF,MAAM,UADQ,SAAS,WAAW,SAAS,QAAQ,CAAC,SAAS,MAAM,GAAG,EAAE,GACnD,KAAK,MAAM,MAAM,oBAAoB,MAAM,EAAE,CAAC;AACnE,QAAO;EACL;EACA,SAAS,EAAE,WAAW,GAAG;EACzB,MAAM;EACN,mBAAmB,OAAO,UAAU,MAAM;EAC1C,QAAQ;EACT;;;;;;AAOH,SAAgB,uBAAuB,UAAkD;CACvF,MAAM,MAAM,SAAS,MAAM,OAAO;CAClC,MAAM,EAAE,UAAU,QAAQ,iBAAiB,KAAK,MAAM;AACtD,QAAO;EACL,OAAO;GACL;GACA,cAAc,SAAS;GACvB,WAAW,YAAY;GACvB,WAAW;GACZ;EACD,MAAM;EACP;;AAKH,SAAgB,mBAAmB,QAGjC;CAMA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,GAC/C,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,CAAC,GAC9B;CACN,MAAM,qBAAqB,SAAS,QAAQ,sBAAsB;CAClE,MAAM,oBAAoB,SAAS,QAAQ,qBAAqB;CAChE,MAAM,wBAAwB,sBAAsB;CAMpD,IAAI;AACJ,KAAI,sBAAsB,OACxB,wBAAuB,KAAK,IAAI,uBAAuB,kBAAkB;UAChE,uBAAuB,OAChC,wBAAuB,wBAAwB;KAE/C,wBAAuB;AAEzB,QAAO;EAAE;EAAuB;EAAsB;;;;;;;AAQxD,SAAS,WAAW,KAAwB;AAC1C,KAAI,IAAI,WAAW,eAAe,IAAI,WAAW,YAAa;AAE9D,KAAI,aAAa;AAIjB,KAAI,IAAI,WAAW,cAAc,IAAI,aAAa,IAAI,uBAAuB;AAC3E,MAAI,SAAS;AACb,MAAI,KAAK,KAAK;GACZ,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,OAAO;GACP,SAAS;GACV,CAAC;YACO,IAAI,aAAa,IAAI,sBAAsB;AACpD,MAAI,SAAS;AACb,MAAI,cAAc,KAAK,KAAK;AAC5B,MAAI,KAAK,KAAK;GACZ,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,OAAO;GACP,SAAS;GACV,CAAC;;;AAIN,SAAS,cAAc,KAA0B;AAC/C,KAAI,IAAI,WAAW,WAAY,QAAO;AACtC,QAAO,KAAK,IAAI,GAAG,IAAI,wBAAwB,IAAI,UAAU;;AAG/D,SAAS,mBAAmB,KAA2C;CACrE,MAAM,OAAgC;EACpC,QAAQ,IAAI;EACZ,YAAY,IAAI;EAChB,cAAc,WAAW,UAAU,MAAM,GAAG,IAAI,QAAQ,YAAY,IAAI;EACxE,MAAM,IAAI;EACX;AACD,KAAI,IAAI,WAAW,cAAc,IAAI,WAAW,cAC9C,MAAK,iBAAiB,cAAc,IAAI;AAE1C,KAAI,IAAI,WAAW,eAAe,IAAI,eAAe,KACnD,MAAK,UAAU,EACb,iBAAiB,IAAI,cAAc,IAAI,eAAe,KACvD;AAEH,QAAO;;AAKT,MAAM,YAAY;CAChB,OAAO;CACP,MAAM;CACN,SAAS;CACT,cAAc;CACd,SAAS;CACV;AAED,MAAM,oBAAoB;AAC1B,MAAM,wBAAwB;AAE9B,SAAS,eAAe,UAA0B;CAChD,MAAM,WAAW,SAAS,QAAQ,UAAU,GAAG;AAC/C,QAAO,SAAS,SAAS,IAAI,WAAW;;AAG1C,SAAS,sBAAsB,MAAuB;AACpD,KAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;CAC9C,MAAM,MAAM;AACZ,KAAI,OAAO,IAAI,WAAW,SAAU,QAAO,IAAI;AAC/C,KAAI,OAAO,IAAI,SAAS,SAAU,QAAO,IAAI;CAC7C,MAAM,QAAQ,IAAI;AAClB,KAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,WAAW;AACjB,MAAI,OAAO,SAAS,WAAW,SAAU,QAAO,SAAS;AACzD,MAAI,OAAO,SAAS,SAAS,SAAU,QAAO,SAAS;;AAEzD,QAAO;;AAST,SAAS,aAAa,UAAwC;AAC5D,KAAI,CAAC,SAAS,WAAW,IAAI,CAAE,QAAO;CACtC,MAAM,UAAU,SAAS,QAAQ,QAAQ,GAAG;AAC5C,KAAI,CAAC,QAAS,QAAO;CAErB,MAAM,IAAI,kBAAkB,KAAK,IAAI,UAAU;AAC/C,KAAI,GAAG;EACL,MAAM,UAAU,EAAE,GAAG,QAAQ,QAAQ,GAAG;EACxC,MAAM,SAAS,EAAE,OAAO,YAAY,WAAW,EAAE,OAAO,YAAY,WAAW;AAC/E,SAAO;GAAE;GAAS,WAAW,EAAE;GAAI;GAAQ;;AAE7C,QAAO,EAAE,SAAS,SAAS;;AAY7B,SAAS,cACP,KACA,UACA,YACqB;CACrB,MAAM,WAAW,eAAe,SAAS;AAEzC,KAAI,eAAe,UAAU,WAAW,eAAe,UAAU,cAAc;AAC7E,MAAI,IAAI,WAAW,UAAU,aAAa,sBACxC,QAAO;GAAE,MAAM;GAAW;GAAY;AAExC,SAAO;;CAGT,MAAM,SAAS,aAAa,SAAS;AACrC,KAAI,CAAC,OAAQ,QAAO;AAEpB,KAAI,eAAe,UAAU,OAAO;AAClC,MAAI,OAAO,WAAW;AACpB,OAAI,OAAO,WAAW,YAAY,IAAI,WAAW,MAC/C,QAAO;IACL,MAAM;IACN,SAAS,OAAO;IAChB,WAAW,OAAO;IAClB;IACD;AAEH,OAAI,OAAO,WAAW,YAAY,IAAI,WAAW,MAC/C,QAAO;IACL,MAAM;IACN,SAAS,OAAO;IAChB,WAAW,OAAO;IAClB;IACD;AAEH,OAAI,OAAO,WAAW,YAAY,IAAI,WAAW,MAC/C,QAAO;IACL,MAAM;IACN,SAAS,OAAO;IAChB,WAAW,OAAO;IAClB;IACD;AAEH,UAAO;;AAET,MAAI,IAAI,WAAW,OACjB,QAAO;GAAE,MAAM;GAAgB,SAAS,OAAO;GAAS;GAAY;AAEtE,SAAO;;AAGT,KAAI,eAAe,UAAU,MAAM;AACjC,MAAI,IAAI,WAAW,UAAU,OAAO,QAClC,QAAO;GAAE,MAAM;GAAY,SAAS,OAAO;GAAS;GAAY;AAElE,SAAO;;AAGT,QAAO;;;;;;;;;;;AAYT,eAAsB,UACpB,KACA,KACA,MACA,UACA,UACA,UACA,SAC2B;CAC3B,MAAM,mBAAmB,IAAI,QAAQ;CACrC,MAAM,aAAa,MAAM,QAAQ,iBAAiB,GAAG,iBAAiB,KAAK;AAC3E,KAAI,CAAC,WAAY,QAAO;CAExB,MAAM,QAAQ,cAAc,KAAK,UAAU,WAAW;AACtD,KAAI,CAAC,MAAO,QAAO;CAEnB,MAAM,SAASA,0BAAU,IAAI;CAC7B,MAAM,YAAY,OAAe,GAAG,OAAO,GAAG;AAE9C,SAAQ,MAAM,MAAd;EACE,KAAK,gBAAgB;GACnB,MAAM,MAAM,eAAe,IAAI,SAAS,MAAM,UAAW,CAAC;AAC1D,OAAI,CAAC,KAAK;AACR,oBAAgB,KAAK,KAAK,UAAU,SAAS,MAAM,UAAW;AAC9D,WAAO;;AAET,cAAW,IAAI;AACf,aAAU,KAAK,KAAK,KAAK,mBAAmB,IAAI,EAAE,UAAU,QAAQ;AACpE,UAAO;;EAGT,KAAK,gBAAgB;GACnB,MAAM,MAAM,eAAe,IAAI,SAAS,MAAM,UAAW,CAAC;AAC1D,OAAI,CAAC,KAAK;AACR,oBAAgB,KAAK,KAAK,UAAU,SAAS,MAAM,UAAW;AAC9D,WAAO;;AAIT,cAAW,IAAI;AACf,OAAI,IAAI,WAAW,aAAa;AAC9B,cAAU,KAAK,KAAK,KAAK,mBAAmB,IAAI,EAAE,UAAU,QAAQ;AACpE,WAAO;;AAET,aAAU,KAAK,KAAK,KAAK,IAAI,QAAQ,UAAU,QAAQ;AACvD,UAAO;;EAGT,KAAK,gBAAgB;GACnB,MAAM,MAAM,eAAe,IAAI,SAAS,MAAM,UAAW,CAAC;AAC1D,OAAI,CAAC,KAAK;AACR,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASC,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK,SAAS;MAAM;KACzC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,aAAa,CAAC,CAAC;AAChD,WAAO;;AAET,OAAI,IAAI,WAAW,aAAa;AAC9B,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASA,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK,SAAS;MAAM;KACzC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,qBAAqB,CAAC,CAAC;AACxD,WAAO;;AAET,OAAI,IAAI,WAAW,aAAa;AAC9B,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASA,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK,SAAS;MAAM;KACzC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,aAAa,CAAC,CAAC;AAChD,WAAO;;AAET,OAAI,SAAS;AACb,OAAI,KAAK,KAAK;IACZ,4BAAW,IAAI,MAAM,EAAC,aAAa;IACnC,OAAO;IACP,SAAS;IACV,CAAC;AACF,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB,MAAM;IACN,SAASA,+BAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ;KAAK,SAAS;KAAM;IACzC,CAAC;AACF,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,aAAa,CAAC,CAAC;AAChD,UAAO;;EAGT,KAAK,WAAW;GACd,IAAI,WAAW;AACf,OAAI;IACF,MAAM,SAAS,OAAQ,KAAK,MAAM,KAAK,GAA+B,EAAE;AACxE,QAAI,OAAO,OAAO,aAAa,SAAU,YAAW,OAAO;AAC3D,QAAI,OAAO,OAAO,cAAc,SAAU,YAAW,OAAO;WACtD;GAGR,MAAM,SAASC,oBAAO,YAAY;AAKlC,aAAU,KAAK,KAAK,KAJC;IACnB,YAAY,WAAW,MAAM,WAAW,kBAAkB;IAC1D,UAAU,WAAW,MAAM,WAAW,SAAS,OAAO,GAAG;IAC1D,EACsC,UAAU,QAAQ;AACzD,UAAO;;EAGT,KAAK;EACL,KAAK,YAAY;GACf,MAAM,UAAU,MAAM;GACtB,IAAI;AACJ,OAAI;AACF,iBAAa,UAAU,KAAK;YACrB,KAAK;IACZ,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU;AACpD,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASD,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK,SAAS;MAAM;KACzC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IACF,KAAK,UAAU,EACb,OAAO;KACL,SAAS;KACT,MAAM;KACN,MAAM;KACP,EACF,CAAC,CACH;AACD,WAAO;;GAGT,MAAM,eAAsC;IAC1C,OAAO;IACP,UAAU,CAAC;KAAE,MAAM;KAAQ,SAHd,sBAAsB,WAAW,IAGA,KAAK,UAAU,cAAc,EAAE,CAAC;KAAE,CAAC;IACjF,eAAe;IACf,UAAUE,2BAAW,IAAI;IAC1B;GAGD,MAAM,EAAE,SAAS,4BAA4BC,sCAC3C,UACA,cAHkB,QAAQ,6BAA6B,OAAO,EAK9D,SAAS,iBACV;AAED,OAAI,CAAC,SAAS;AAEZ,QADwBC,kCAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;KACnB,MAAM,gBAAgBC,qCAAqB,wBAAwB;AACnE,cAAS,OAAO,MACdC,qCAAqB,IAAI,UAAU,QAAQ,UAAU,wBAAwB,CAC9E;AACD,aAAQ,IAAI;MACV,QAAQ,IAAI,UAAU;MACtB,MAAM;MACN,SAASN,+BAAe,IAAI,QAAQ;MACpC,MAAM;MACN,UAAU;OACR,QAAQ;OACR,SAAS;OACT,GAAGO,oCAAoB,SAAS,QAAQ,IAAI,QAAQ;OACrD;MACF,CAAC;AACF,SAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,SAAI,IACF,KAAK,UAAU,EACb,OAAO;MACL,SAAS;MACT,MAAM;MACN,MAAM;MACP,EACF,CAAC,CACH;AACD,YAAO;;AAET,QAAI,SAAS,QAAQ;KACnB,MAAM,oBAAoB,gBAAgB,UAAU,MAAM,WAAW;AAKrE,SAAI,MAAM,SAAS,gBAcjB;UAbgB,MAAM,6BAA6B;OACjD;OACA;OACA;OACA;OACA;OACA,cAAc,eAAe,SAAS;OACtC;OACA;OACA,UAAU;OACV;OACA;OACD,CAAC,KACc,UAAW,QAAO;YAG7B;MACL,MAAM,UAAU,MAAMC,gCACpB,KACA,KACA,cACA,OACA,eAAe,SAAS,EACxB,UACA,mBACA,KACD;AACD,UAAI,YAAY,kBAAmB,QAAO;AAC1C,UAAI,YAAY,kBAAkB;AAChC,eAAQ,IAAI;QACV,QAAQ,IAAI,UAAU;QACtB,MAAM;QACN,SAASR,+BAAe,IAAI,QAAQ;QACpC,MAAM;QACN,UAAU;SAAE,QAAQ,IAAI,cAAc;SAAK,SAAS;SAAM,QAAQ;SAAS;QAC5E,CAAC;AACF,cAAO;;;;AAKb,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASA,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MACR,QAAQ;MACR,SAAS;MACT,GAAGO,oCAAoB,SAAS,QAAQ,IAAI,QAAQ;MACrD;KACF,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IACF,KAAK,UAAU,EACb,OAAO;KACL,SAAS;KACT,MAAM;KACN,MAAM;KACP,EACF,CAAC,CACH;AACD,WAAO;;AAGT,WAAQ,2BAA2B,SAAS,UAAU,OAAO;GAC7D,MAAM,WAAW,MAAME,gCAAgB,SAAS,aAAa;AAE7D,OAAIC,gCAAgB,SAAS,EAAE;IAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASV,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE;MAAQ;MAAS;KAC9B,CAAC;AACF,0CAAmB,KAAK,QAAQW,uCAAuB,SAAS,EAAE,EAChE,YAAY,SAAS,YACtB,CAAC;AACF,WAAO;;GAGT,IAAI;AACJ,OAAIC,+BAAe,SAAS,CAC1B,WAAW,SAA6B;YAC/BC,gCAAgB,SAAS,CAClC,WAAUC,iCAAe,SAAS;QAC7B;AACL,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASd,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK;MAAS;KACnC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IACF,KAAK,UAAU,EACb,OAAO;KACL,SAAS;KACT,MAAM;KACP,EACF,CAAC,CACH;AACD,WAAO;;AAGT,OAAI,MAAM,SAAS,YAAY;AAC7B,YAAQ,IAAI;KACV,QAAQ,IAAI,UAAU;KACtB,MAAM;KACN,SAASA,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ;MAAK;MAAS;KACnC,CAAC;AACF,QAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,QAAI,IAAI,KAAK,UAAU,QAAQ,CAAC;AAChC,WAAO;;GAGT,MAAM,YAAYC,oBAAO,YAAY;GACrC,MAAM,cAAc,mBAAmB,SAAS,SAAS;GACzD,MAAM,MAAM,KAAK,KAAK;GACtB,MAAM,gBACJ,YAAY,yBAAyB,IAAI,cAAc;GACzD,MAAM,MAAmB;IACvB;IACA;IACA,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,uBAAuB,YAAY;IACnC,sBAAsB,YAAY;IAClC,aAAa;IACb,aAAa,kBAAkB,cAAc,MAAM;IACnD,MAAM,CACJ;KACE,WAAW,IAAI,KAAK,IAAI,CAAC,aAAa;KACtC,OAAO;KACP,SAAS;KACV,CACF;IACD,WAAW;IACZ;AACD,kBAAe,IAAI,SAAS,UAAU,EAAE,IAAI;GAC5C,MAAM,WAAW;IACf,YAAY;IACZ,cAAc,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY;IAChE,YAAY,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY,UAAU;IACxE,YAAY,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY,UAAU;IACxE,gBAAgB,cAAc,IAAI;IACnC;AACD,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB,MAAM;IACN,SAASD,+BAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ;KAAK;KAAS;IACnC,CAAC;AACF,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,SAAS,CAAC;AACjC,UAAO;;;;AAKb,SAAS,UAAU,KAA6C;AAC9D,KAAI,CAAC,IAAI,MAAM,CAAE,QAAO;AACxB,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,KAAK;EACZ,MAAM,SAAS,eAAe,QAAQ,IAAI,UAAU;AACpD,QAAM,IAAI,MAAM,mBAAmB,SAAS;;;AAehD,MAAM,+BAA+B;AAIrC,MAAM,yBAAyB;AAI/B,MAAM,+BAA+B;;;;;;;;;AAgBrC,eAAsB,aAAa,MA0Bd;CACnB,MAAM,EACJ,cACA,YACA,MACA,SACA,iBAAiB,8BACjB,YAAY,wBACZ,mBACA,oBACA,oBACA,WACE;CAEJ,MAAM,WAAW,KAAK,KAAK,GAAG;CAC9B,MAAM,oBAAoBe,8BAAa,mBAAmB,6BAA6B;CAKvF,MAAM,oBACJ,YAAY,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,mBAAmB,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC;CAGtF,MAAM,YAAYC,+BAAmB,cAAc,WAAW;CAC9D,MAAM,YAAY,MAAM,MAAM,WAAW;EACvC,QAAQ;EACR;EACA;EACA,QAAQ,aAAa;EACtB,CAAC;CACF,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,KAAI,CAAC,UAAU,GACb,OAAM,IAAI,MAAM,UAAU,UAAU,OAAO,IAAI,WAAW,MAAM,GAAG,IAAI,GAAG;CAE5E,MAAM,aAAa,iBAAiB,YAAY,SAAS;AAIzD,KAAI,eAAe,QAAQ,OAAO,eAAe,YAAY,MAAM,QAAQ,WAAW,CACpF,OAAM,IAAI,MAAM,uCAAuC;CAEzD,MAAM,MAAM;CACZ,MAAM,oBAAoB,OAAO,IAAI,cAAc,GAAG,CAAC,MAAM;AAC7D,KAAI,CAAC,kBACH,OAAM,IAAI,MAAM,qCAAqC;CAWvD,MAAM,iBAAiB,UAAU;CACjC,MAAM,mBAAmB,OAAgB,OAAe,iBAA8B;AACpF,MAAI,OAAO,UAAU,YAAY,MAC/B,KAAI;GACF,MAAM,SAAS,IAAI,IAAI,MAAM;AAC7B,OAAI,OAAO,WAAW,eAAgB,QAAO;AAC7C,WAAQ,KACN,YAAY,MAAM,UAAU,OAAO,OAAO,oCAAoC,eAAe,iDAC9F;UACK;AACN,WAAQ,KACN,YAAY,MAAM,uBAAuB,MAAM,MAAM,GAAG,IAAI,CAAC,kDAC9D;;AAGL,SAAOA,+BAAmB,cAAc,aAAa;;CAEvD,MAAM,YAAY,gBAChB,IAAI,YACJ,cACA,mBAAmB,kBAAkB,CACtC;CACD,MAAM,YAAY,gBAChB,IAAI,cACJ,gBACA,mBAAmB,kBAAkB,CACtC;AAGD,QAAO,MAAM;AACX,MAAI,KAAK,KAAK,GAAG,SAAU,OAAM,IAAI,MAAM,8BAA8B,UAAU,IAAI;EACvF,MAAM,YAAY,MAAM,MAAM,WAAW;GAAE;GAAS,QAAQ,aAAa;GAAE,CAAC;EAC5E,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,MAAI,CAAC,UAAU,GACb,OAAM,IAAI,MAAM,UAAU,UAAU,OAAO,IAAI,WAAW,MAAM,GAAG,IAAI,GAAG;EAE5E,MAAM,eAAe,iBAAiB,YAAY,SAAS;AAE3D,MAAI,iBAAiB,QAAQ,OAAO,iBAAiB,YAAY,MAAM,QAAQ,aAAa,CAC1F,OAAM,IAAI,MAAM,uCAAuC;EAEzD,MAAM,aAAa;EACnB,MAAM,IAAI,OAAO,WAAW,UAAU,GAAG;AACzC,MAAI,MAAM,YAAa;AACvB,MAAI,MAAM,YAAY,MAAM,WAAW,MAAM,YAC3C,OAAM,IAAI,MAAM,uCAAuC,IAAI;EAE7D,MAAM,YAAY,WAAW,KAAK,KAAK;AAIvC,MAAI,aAAa,EAAG,OAAM,IAAI,MAAM,8BAA8B,UAAU,IAAI;EAChF,MAAM,QAAQ,KAAK,IAAI,gBAAgB,UAAU;AACjD,QAAM,IAAI,SAAe,MAAM,WAAW,GAAG,KAAK,IAAI,GAAG,MAAM,CAAC,CAAC;;CAInE,MAAM,YAAY,MAAM,MAAM,WAAW;EAAE;EAAS,QAAQ,aAAa;EAAE,CAAC;CAC5E,MAAM,aAAa,MAAM,UAAU,MAAM;AACzC,KAAI,CAAC,UAAU,GACb,OAAM,IAAI,MAAM,UAAU,UAAU,OAAO,IAAI,WAAW,MAAM,GAAG,IAAI,GAAG;AAE5E,QAAO,iBAAiB,YAAY,SAAS;;AAG/C,eAAe,6BAA6B,MAYL;CACrC,MAAM,EACJ,KACA,KACA,cACA,SACA,UACA,cACA,MACA,UACA,UACA,UACA,YACE;CAEJ,MAAM,SAAS,SAAS;AACxB,KAAI,CAAC,OAAQ,QAAO;CACpB,MAAM,eAAe,OAAO,UAAU;AACtC,KAAI,CAAC,cAAc;AACjB,WAAS,OAAO,KAAK,+DAA+D;AACpF,SAAO;;AAGT,UAAS,OAAO,KAAK,2CAA2C,eAAe,eAAe;CAE9F,IAAI;AACJ,KAAI;AACF,cAAY,MAAM,aAAa;GAC7B;GACA,YAAY;GACZ;GACA,SAASC,qCAAoB,IAAI;GACjC,gBAAgB,OAAO,KAAK;GAC5B,WAAW,OAAO,KAAK;GACvB,mBAAmB,OAAO;GAC1B,qBAAqB,OAAO,GAAG,QAAQ,YAAY,GAAG;GACtD,qBAAqB,OAAO,GAAG,QAAQ,YAAY;GACnD,QAAQ,SAAS;GAClB,CAAC;UACK,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,WAAS,OAAO,MAAM,gCAAgC,MAAM;AAI5D,MAAI,IAAI,aAAa,IAAI,cAAe,QAAO;AAC/C,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM;GACN,SAASjB,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM,QAAQ;IAAS;GAC1D,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU,EACb,OAAO;GAAE,SAAS,6BAA6B;GAAO,MAAM;GAAe,EAC5E,CAAC,CACH;AACD,SAAO;;CAOT,MAAM,UAAmB;EACvB,OAAOkB,mCAJY,SAAS,mBAC1B,SAAS,iBAAiB,aAAa,GACvC,cAEqC,OAAO;EAC9C,UAAU;GAAE,MAAM;GAAW,QAAQ;GAAK;EAC3C;CACD,MAAM,gBAAgBC,gCAAe;EACnC;EACA,aAAa;EACb,QAAQpB,0BAAU,IAAI;EACtB;EACA;EACA,QAAQ,SAAS;EAClB,CAAC;AAIF,KAAI,cAAc,SAAS,YAAY,CAAC,IAAI,YAC1C,KAAI,UAAU,yBAAyBqB,qCAAoB,cAAc,MAAM,CAAC;CAIlF,MAAM,eAAenB,oBAAO,YAAY;CACxC,MAAM,cAAc,mBAAmB,SAAS,SAAS;CACzD,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,gBACJ,YAAY,yBAAyB,IAAI,cAAc;CACzD,MAAM,MAAmB;EACvB,WAAW;EACX;EACA,QAAQ;EACR,QAAQ;EACR,WAAW;EACX,uBAAuB,YAAY;EACnC,sBAAsB,YAAY;EAClC,aAAa;EACb,aAAa,kBAAkB,cAAc,MAAM;EACnD,MAAM,CAAC;GAAE,WAAW,IAAI,KAAK,IAAI,CAAC,aAAa;GAAE,OAAO;GAAQ,SAAS;GAAiB,CAAC;EAC3F,WAAW;EACZ;AACD,gBAAe,IAAI,SAAS,aAAa,EAAE,IAAI;CAC/C,MAAM,WAAW;EACf,YAAY;EACZ,cAAc,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY;EAChE,YAAY,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY,aAAa;EAC3E,YAAY,WAAW,UAAU,MAAM,GAAG,QAAQ,YAAY,aAAa;EAC3E,gBAAgB,cAAc,IAAI;EACnC;AAKD,KAAI,IAAI,aAAa,IAAI,cAAe,QAAO;AAC/C,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM;EACN,SAASD,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM,QAAQ;GAAS;EAC1D,CAAC;AACF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,SAAS,CAAC;AACjC,QAAO;;AAGT,SAAS,iBAAiB,MAAc,OAAwB;AAC9D,KAAI;AACF,SAAO,KAAK,MAAM,KAAK;SACjB;AACN,QAAM,IAAI,MAAM,GAAG,MAAM,sBAAsB,KAAK,MAAM,GAAG,IAAI,GAAG;;;AAIxE,SAAS,gBAAgB,UAA2B,YAAqC;AACvF,KAAI,CAAC,SAAS,OAAQ,QAAO;AAK7B,KAAI,SAAS,OAAO,UAAU,IAAK,QAAO;AAC1C,QAAO;EACL,GAAG;EACH,QAAQ;GACN,GAAG,SAAS;GACZ,WAAW;IACT,GAAG,SAAS,OAAO;IACnB,KAAK,WAAW;IACjB;GACF;EACF;;AAGH,SAAS,UACP,KACA,KACA,QACA,SACA,UACA,SACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM;EACN,SAASA,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE;GAAQ,SAAS;GAAM;EACpC,CAAC;AACF,KAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AAC7D,KAAI,IAAI,KAAK,UAAU,QAAQ,CAAC;;AAGlC,SAAS,gBACP,KACA,KACA,UACA,SACA,WACM;AACN,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB,MAAM;EACN,SAASA,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;AACF,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IACF,KAAK,UAAU,EACb,OAAO;EAAE,SAAS,WAAW,UAAU;EAAa,MAAM;EAAa,EACxE,CAAC,CACH"}
|
package/dist/fal.d.cts
CHANGED
|
@@ -49,6 +49,14 @@ type HandleFalOutcome = "handled" | "passthrough";
|
|
|
49
49
|
* `/fal/run/...` audio routes.
|
|
50
50
|
*/
|
|
51
51
|
declare function handleFal(req: http.IncomingMessage, res: http.ServerResponse, body: string, pathname: string, fixtures: Fixture[], defaults: HandlerDefaults, journal: Journal): Promise<HandleFalOutcome>;
|
|
52
|
+
/**
|
|
53
|
+
* Walk a fal-shaped queue protocol upstream: POST submit, poll status until
|
|
54
|
+
* COMPLETED, GET final result body. Returns the parsed final body so the caller
|
|
55
|
+
* can persist it as the fixture and seed local queue state.
|
|
56
|
+
*
|
|
57
|
+
* Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`
|
|
58
|
+
* audio path (`fal-audio.ts`) can reuse the same logic.
|
|
59
|
+
*/
|
|
52
60
|
//#endregion
|
|
53
61
|
export { FalQueueStateMap, handleFal };
|
|
54
62
|
//# sourceMappingURL=fal.d.cts.map
|
package/dist/fal.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fal.d.cts","names":[],"sources":["../src/fal.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"fal.d.cts","names":[],"sources":["../src/fal.ts"],"sourcesContent":[],"mappings":";;;;;KA8CK,cAAA;AARuC,UAUlC,WAAA,CAFS;EAET,SAAA,EAAA,MAAW;EAAA,OAAA,EAAA,MAAA;QAGX,EAAA,cAAA;QAWF,EAAA,OAAA;EAAK;EAcA,SAAA,EAAA,MAAA;EAAgB;uBAGT,EAAA,MAAA;;EAUe,oBAAA,EAAA,MAAA;EA+NvB,WAAA,EAAA,MAAgB;EA+EN,WAAA,EAAS,MAAA,GAAA,IAAA;EAAA;MACxB,EA1UC,KA0UI,CAAA;IACL,SAAK,EAAA,MAAA;IAGA,KAAA,EAAA,MAAA;IACA,OAAA,EAAA,MAAA;;WAED,EAAA,MAAA;;;;;;;cAnUE,gBAAA;;oBAGO;wBAUI;;;;;KA+NZ,gBAAA;;;;;;;;;;iBA+EU,SAAA,MACf,IAAA,CAAK,sBACL,IAAA,CAAK,0DAGA,qBACA,0BACD,UACR,QAAQ"}
|
package/dist/fal.d.ts
CHANGED
|
@@ -49,6 +49,14 @@ type HandleFalOutcome = "handled" | "passthrough";
|
|
|
49
49
|
* `/fal/run/...` audio routes.
|
|
50
50
|
*/
|
|
51
51
|
declare function handleFal(req: http.IncomingMessage, res: http.ServerResponse, body: string, pathname: string, fixtures: Fixture[], defaults: HandlerDefaults, journal: Journal): Promise<HandleFalOutcome>;
|
|
52
|
+
/**
|
|
53
|
+
* Walk a fal-shaped queue protocol upstream: POST submit, poll status until
|
|
54
|
+
* COMPLETED, GET final result body. Returns the parsed final body so the caller
|
|
55
|
+
* can persist it as the fixture and seed local queue state.
|
|
56
|
+
*
|
|
57
|
+
* Decoupled from the route layer so the legacy `/fal/queue/submit/{model}`
|
|
58
|
+
* audio path (`fal-audio.ts`) can reuse the same logic.
|
|
59
|
+
*/
|
|
52
60
|
//#endregion
|
|
53
61
|
export { FalQueueStateMap, handleFal };
|
|
54
62
|
//# sourceMappingURL=fal.d.ts.map
|
package/dist/fal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fal.d.ts","names":[],"sources":["../src/fal.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"fal.d.ts","names":[],"sources":["../src/fal.ts"],"sourcesContent":[],"mappings":";;;;;KA8CK,cAAA;AARuC,UAUlC,WAAA,CAFS;EAET,SAAA,EAAA,MAAW;EAAA,OAAA,EAAA,MAAA;QAGX,EAAA,cAAA;QAWF,EAAA,OAAA;EAAK;EAcA,SAAA,EAAA,MAAA;EAAgB;uBAGT,EAAA,MAAA;;EAUe,oBAAA,EAAA,MAAA;EA+NvB,WAAA,EAAA,MAAgB;EA+EN,WAAA,EAAS,MAAA,GAAA,IAAA;EAAA;MACxB,EA1UC,KA0UI,CAAA;IACL,SAAK,EAAA,MAAA;IAGA,KAAA,EAAA,MAAA;IACA,OAAA,EAAA,MAAA;;WAED,EAAA,MAAA;;;;;;;cAnUE,gBAAA;;oBAGO;wBAUI;;;;;KA+NZ,gBAAA;;;;;;;;;;iBA+EU,SAAA,MACf,IAAA,CAAK,sBACL,IAAA,CAAK,0DAGA,qBACA,0BACD,UACR,QAAQ"}
|