@copilotkit/aimock 1.29.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.
Files changed (207) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +34 -0
  4. package/README.md +1 -1
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/bedrock-converse.cjs +63 -31
  7. package/dist/bedrock-converse.cjs.map +1 -1
  8. package/dist/bedrock-converse.d.cts.map +1 -1
  9. package/dist/bedrock-converse.d.ts.map +1 -1
  10. package/dist/bedrock-converse.js +65 -33
  11. package/dist/bedrock-converse.js.map +1 -1
  12. package/dist/bedrock.cjs +95 -33
  13. package/dist/bedrock.cjs.map +1 -1
  14. package/dist/bedrock.d.cts.map +1 -1
  15. package/dist/bedrock.d.ts.map +1 -1
  16. package/dist/bedrock.js +97 -35
  17. package/dist/bedrock.js.map +1 -1
  18. package/dist/cli.cjs +18 -1
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +18 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/cohere.cjs +49 -15
  23. package/dist/cohere.cjs.map +1 -1
  24. package/dist/cohere.d.cts.map +1 -1
  25. package/dist/cohere.d.ts.map +1 -1
  26. package/dist/cohere.js +51 -17
  27. package/dist/cohere.js.map +1 -1
  28. package/dist/config-loader.d.ts.map +1 -1
  29. package/dist/elevenlabs-audio.cjs +8 -4
  30. package/dist/elevenlabs-audio.cjs.map +1 -1
  31. package/dist/elevenlabs-audio.d.cts.map +1 -1
  32. package/dist/elevenlabs-audio.d.ts.map +1 -1
  33. package/dist/elevenlabs-audio.js +10 -6
  34. package/dist/elevenlabs-audio.js.map +1 -1
  35. package/dist/embeddings.cjs +4 -3
  36. package/dist/embeddings.cjs.map +1 -1
  37. package/dist/embeddings.d.cts.map +1 -1
  38. package/dist/embeddings.d.ts.map +1 -1
  39. package/dist/embeddings.js +6 -5
  40. package/dist/embeddings.js.map +1 -1
  41. package/dist/fal-audio.cjs +18 -8
  42. package/dist/fal-audio.cjs.map +1 -1
  43. package/dist/fal-audio.d.cts.map +1 -1
  44. package/dist/fal-audio.d.ts.map +1 -1
  45. package/dist/fal-audio.js +22 -12
  46. package/dist/fal-audio.js.map +1 -1
  47. package/dist/fal.cjs +55 -46
  48. package/dist/fal.cjs.map +1 -1
  49. package/dist/fal.d.cts +8 -0
  50. package/dist/fal.d.cts.map +1 -1
  51. package/dist/fal.d.ts +8 -0
  52. package/dist/fal.d.ts.map +1 -1
  53. package/dist/fal.js +58 -49
  54. package/dist/fal.js.map +1 -1
  55. package/dist/gemini-embeddings.cjs +4 -3
  56. package/dist/gemini-embeddings.cjs.map +1 -1
  57. package/dist/gemini-embeddings.js +6 -5
  58. package/dist/gemini-embeddings.js.map +1 -1
  59. package/dist/gemini-interactions.cjs +3 -3
  60. package/dist/gemini-interactions.cjs.map +1 -1
  61. package/dist/gemini-interactions.d.cts.map +1 -1
  62. package/dist/gemini-interactions.d.ts.map +1 -1
  63. package/dist/gemini-interactions.js +5 -5
  64. package/dist/gemini-interactions.js.map +1 -1
  65. package/dist/gemini.cjs +55 -24
  66. package/dist/gemini.cjs.map +1 -1
  67. package/dist/gemini.d.cts.map +1 -1
  68. package/dist/gemini.d.ts.map +1 -1
  69. package/dist/gemini.js +57 -26
  70. package/dist/gemini.js.map +1 -1
  71. package/dist/helpers.cjs +120 -2
  72. package/dist/helpers.cjs.map +1 -1
  73. package/dist/helpers.d.cts +43 -3
  74. package/dist/helpers.d.cts.map +1 -1
  75. package/dist/helpers.d.ts +43 -3
  76. package/dist/helpers.d.ts.map +1 -1
  77. package/dist/helpers.js +117 -3
  78. package/dist/helpers.js.map +1 -1
  79. package/dist/images.cjs +12 -6
  80. package/dist/images.cjs.map +1 -1
  81. package/dist/images.d.cts.map +1 -1
  82. package/dist/images.d.ts.map +1 -1
  83. package/dist/images.js +14 -8
  84. package/dist/images.js.map +1 -1
  85. package/dist/index.cjs +13 -2
  86. package/dist/index.d.cts +4 -3
  87. package/dist/index.d.ts +4 -3
  88. package/dist/index.js +6 -5
  89. package/dist/llmock.cjs +2 -1
  90. package/dist/llmock.cjs.map +1 -1
  91. package/dist/llmock.d.cts.map +1 -1
  92. package/dist/llmock.d.ts.map +1 -1
  93. package/dist/llmock.js +2 -1
  94. package/dist/llmock.js.map +1 -1
  95. package/dist/messages.cjs +325 -85
  96. package/dist/messages.cjs.map +1 -1
  97. package/dist/messages.d.cts.map +1 -1
  98. package/dist/messages.d.ts.map +1 -1
  99. package/dist/messages.js +327 -87
  100. package/dist/messages.js.map +1 -1
  101. package/dist/metrics.cjs +9 -0
  102. package/dist/metrics.cjs.map +1 -1
  103. package/dist/metrics.d.cts.map +1 -1
  104. package/dist/metrics.d.ts.map +1 -1
  105. package/dist/metrics.js +7 -1
  106. package/dist/metrics.js.map +1 -1
  107. package/dist/model-utils.cjs +68 -0
  108. package/dist/model-utils.cjs.map +1 -1
  109. package/dist/model-utils.js +68 -1
  110. package/dist/model-utils.js.map +1 -1
  111. package/dist/ollama.cjs +58 -21
  112. package/dist/ollama.cjs.map +1 -1
  113. package/dist/ollama.d.cts.map +1 -1
  114. package/dist/ollama.d.ts.map +1 -1
  115. package/dist/ollama.js +60 -23
  116. package/dist/ollama.js.map +1 -1
  117. package/dist/openrouter-video.cjs +1609 -0
  118. package/dist/openrouter-video.cjs.map +1 -0
  119. package/dist/openrouter-video.d.cts +154 -0
  120. package/dist/openrouter-video.d.cts.map +1 -0
  121. package/dist/openrouter-video.d.ts +154 -0
  122. package/dist/openrouter-video.d.ts.map +1 -0
  123. package/dist/openrouter-video.js +1601 -0
  124. package/dist/openrouter-video.js.map +1 -0
  125. package/dist/recorder.cjs +97 -15
  126. package/dist/recorder.cjs.map +1 -1
  127. package/dist/recorder.d.cts.map +1 -1
  128. package/dist/recorder.d.ts.map +1 -1
  129. package/dist/recorder.js +96 -17
  130. package/dist/recorder.js.map +1 -1
  131. package/dist/responses.cjs +26 -12
  132. package/dist/responses.cjs.map +1 -1
  133. package/dist/responses.d.cts +1 -1
  134. package/dist/responses.d.cts.map +1 -1
  135. package/dist/responses.d.ts +1 -1
  136. package/dist/responses.d.ts.map +1 -1
  137. package/dist/responses.js +28 -14
  138. package/dist/responses.js.map +1 -1
  139. package/dist/router.cjs +37 -8
  140. package/dist/router.cjs.map +1 -1
  141. package/dist/router.d.cts +30 -1
  142. package/dist/router.d.cts.map +1 -1
  143. package/dist/router.d.ts +30 -1
  144. package/dist/router.d.ts.map +1 -1
  145. package/dist/router.js +37 -9
  146. package/dist/router.js.map +1 -1
  147. package/dist/server.cjs +173 -19
  148. package/dist/server.cjs.map +1 -1
  149. package/dist/server.d.cts +2 -0
  150. package/dist/server.d.cts.map +1 -1
  151. package/dist/server.d.ts +2 -0
  152. package/dist/server.d.ts.map +1 -1
  153. package/dist/server.js +176 -22
  154. package/dist/server.js.map +1 -1
  155. package/dist/speech.cjs +4 -2
  156. package/dist/speech.cjs.map +1 -1
  157. package/dist/speech.d.cts.map +1 -1
  158. package/dist/speech.d.ts.map +1 -1
  159. package/dist/speech.js +6 -4
  160. package/dist/speech.js.map +1 -1
  161. package/dist/stream-collapse.cjs +44 -1
  162. package/dist/stream-collapse.cjs.map +1 -1
  163. package/dist/stream-collapse.d.cts +28 -0
  164. package/dist/stream-collapse.d.cts.map +1 -1
  165. package/dist/stream-collapse.d.ts +28 -0
  166. package/dist/stream-collapse.d.ts.map +1 -1
  167. package/dist/stream-collapse.js +44 -2
  168. package/dist/stream-collapse.js.map +1 -1
  169. package/dist/transcription.cjs +4 -2
  170. package/dist/transcription.cjs.map +1 -1
  171. package/dist/transcription.d.cts.map +1 -1
  172. package/dist/transcription.d.ts.map +1 -1
  173. package/dist/transcription.js +6 -4
  174. package/dist/transcription.js.map +1 -1
  175. package/dist/types.d.cts +123 -8
  176. package/dist/types.d.cts.map +1 -1
  177. package/dist/types.d.ts +123 -8
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/vector-types.d.cts.map +1 -1
  180. package/dist/vector-types.d.ts.map +1 -1
  181. package/dist/video.cjs +4 -2
  182. package/dist/video.cjs.map +1 -1
  183. package/dist/video.d.cts.map +1 -1
  184. package/dist/video.d.ts.map +1 -1
  185. package/dist/video.js +6 -4
  186. package/dist/video.js.map +1 -1
  187. package/dist/ws-gemini-live.cjs +4 -3
  188. package/dist/ws-gemini-live.cjs.map +1 -1
  189. package/dist/ws-gemini-live.d.cts +2 -2
  190. package/dist/ws-gemini-live.d.cts.map +1 -1
  191. package/dist/ws-gemini-live.d.ts.map +1 -1
  192. package/dist/ws-gemini-live.js +6 -5
  193. package/dist/ws-gemini-live.js.map +1 -1
  194. package/dist/ws-realtime.cjs +4 -3
  195. package/dist/ws-realtime.cjs.map +1 -1
  196. package/dist/ws-realtime.d.cts +2 -2
  197. package/dist/ws-realtime.d.cts.map +1 -1
  198. package/dist/ws-realtime.d.ts.map +1 -1
  199. package/dist/ws-realtime.js +6 -5
  200. package/dist/ws-realtime.js.map +1 -1
  201. package/dist/ws-responses.cjs +8 -6
  202. package/dist/ws-responses.cjs.map +1 -1
  203. package/dist/ws-responses.d.cts.map +1 -1
  204. package/dist/ws-responses.d.ts.map +1 -1
  205. package/dist/ws-responses.js +10 -8
  206. package/dist/ws-responses.js.map +1 -1
  207. package/package.json +2 -2
package/dist/fal.js CHANGED
@@ -1,8 +1,8 @@
1
- import { flattenHeaders, getContext, getTestId, isAudioResponse, isErrorResponse, isJSONResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
2
- import { matchFixture } from "./router.js";
1
+ import { flattenHeaders, getContext, getTestId, isAudioResponse, isErrorResponse, isJSONResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
+ import { matchFixtureDiagnostic } from "./router.js";
3
3
  import { writeErrorResponse } from "./sse-writer.js";
4
4
  import { resolveUpstreamUrl } from "./url.js";
5
- import { buildFixtureMatch, persistFixture, proxyAndRecord } from "./recorder.js";
5
+ import { buildFixtureMatch, buildForwardHeaders, clampTimeout, persistFixture, proxyAndRecord, sanitizeHeaderValue } from "./recorder.js";
6
6
  import { audioToFalFile } from "./fal-audio.js";
7
7
  import crypto from "node:crypto";
8
8
 
@@ -102,11 +102,13 @@ function videoResponseToFalJson(response) {
102
102
  };
103
103
  }
104
104
  function resolveProgression(config) {
105
- const pollsBeforeInProgress = config?.pollsBeforeInProgress ?? 0;
106
- const explicitCompleted = config?.pollsBeforeCompleted;
105
+ const sanitize = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : void 0;
106
+ const explicitInProgress = sanitize(config?.pollsBeforeInProgress);
107
+ const explicitCompleted = sanitize(config?.pollsBeforeCompleted);
108
+ const pollsBeforeInProgress = explicitInProgress ?? 0;
107
109
  let pollsBeforeCompleted;
108
- if (explicitCompleted != null) pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);
109
- else if (config?.pollsBeforeInProgress != null) pollsBeforeCompleted = pollsBeforeInProgress + 1;
110
+ if (explicitCompleted !== void 0) pollsBeforeCompleted = Math.max(pollsBeforeInProgress, explicitCompleted);
111
+ else if (explicitInProgress !== void 0) pollsBeforeCompleted = pollsBeforeInProgress + 1;
110
112
  else pollsBeforeCompleted = 0;
111
113
  return {
112
114
  pollsBeforeInProgress,
@@ -403,9 +405,11 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
403
405
  _endpointType: "fal",
404
406
  _context: getContext(req)
405
407
  };
406
- const fixture = matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
408
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
407
409
  if (!fixture) {
408
410
  if (resolveStrictMode(defaults.strict, req.headers)) {
411
+ const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
412
+ defaults.logger.error(strictNoMatchLogLine(req.method ?? "POST", pathname, skippedBySequenceOrTurn));
409
413
  journal.add({
410
414
  method: req.method ?? "POST",
411
415
  path: pathname,
@@ -419,7 +423,7 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
419
423
  });
420
424
  res.writeHead(503, { "Content-Type": "application/json" });
421
425
  res.end(JSON.stringify({ error: {
422
- message: "Strict mode: no fixture matched",
426
+ message: strictMessage,
423
427
  type: "invalid_request_error",
424
428
  code: "no_fixture_match"
425
429
  } }));
@@ -588,29 +592,7 @@ function parseBody(raw) {
588
592
  }
589
593
  const DEFAULT_FAL_POLL_INTERVAL_MS = 1e3;
590
594
  const DEFAULT_FAL_TIMEOUT_MS = 9e5;
591
- const FAL_STRIP_FORWARD_HEADERS = new Set([
592
- "connection",
593
- "keep-alive",
594
- "transfer-encoding",
595
- "te",
596
- "trailer",
597
- "upgrade",
598
- "proxy-authorization",
599
- "proxy-authenticate",
600
- "host",
601
- "content-length",
602
- "cookie",
603
- "accept-encoding"
604
- ]);
605
- function buildFalForwardHeaders(req) {
606
- const out = {};
607
- for (const [name, val] of Object.entries(req.headers)) {
608
- if (val === void 0) continue;
609
- if (FAL_STRIP_FORWARD_HEADERS.has(name.toLowerCase())) continue;
610
- out[name] = Array.isArray(val) ? val.join(", ") : val;
611
- }
612
- return out;
613
- }
595
+ const DEFAULT_FAL_FETCH_TIMEOUT_MS = 3e4;
614
596
  /**
615
597
  * Walk a fal-shaped queue protocol upstream: POST submit, poll status until
616
598
  * COMPLETED, GET final result body. Returns the parsed final body so the caller
@@ -620,38 +602,60 @@ function buildFalForwardHeaders(req) {
620
602
  * audio path (`fal-audio.ts`) can reuse the same logic.
621
603
  */
622
604
  async function walkFalQueue(args) {
623
- const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, fallbackStatusPath, fallbackResultPath } = args;
605
+ const { upstreamBase, submitPath, body, headers, pollIntervalMs = DEFAULT_FAL_POLL_INTERVAL_MS, timeoutMs = DEFAULT_FAL_TIMEOUT_MS, upstreamTimeoutMs, fallbackStatusPath, fallbackResultPath, logger } = args;
624
606
  const deadline = Date.now() + timeoutMs;
607
+ const perFetchTimeoutMs = clampTimeout(upstreamTimeoutMs, DEFAULT_FAL_FETCH_TIMEOUT_MS);
608
+ const fetchSignal = () => AbortSignal.timeout(Math.max(1, Math.min(perFetchTimeoutMs, deadline - Date.now())));
625
609
  const submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);
626
610
  const submitRes = await fetch(submitUrl, {
627
611
  method: "POST",
628
612
  headers,
629
- body
613
+ body,
614
+ signal: fetchSignal()
630
615
  });
631
616
  const submitText = await submitRes.text();
632
617
  if (!submitRes.ok) throw new Error(`Submit ${submitRes.status}: ${submitText.slice(0, 200)}`);
633
- const env = parseJsonOrThrow(submitText, "Submit");
618
+ const submitJson = parseJsonOrThrow(submitText, "Submit");
619
+ if (submitJson === null || typeof submitJson !== "object" || Array.isArray(submitJson)) throw new Error("Submit response is not a JSON object");
620
+ const env = submitJson;
634
621
  const upstreamRequestId = String(env.request_id ?? "").trim();
635
622
  if (!upstreamRequestId) throw new Error("Submit response missing request_id");
636
- const envStatusUrl = env.status_url;
637
- const envResponseUrl = env.response_url;
638
- const statusUrl = typeof envStatusUrl === "string" && envStatusUrl ? new URL(envStatusUrl) : resolveUpstreamUrl(upstreamBase, fallbackStatusPath(upstreamRequestId));
639
- const resultUrl = typeof envResponseUrl === "string" && envResponseUrl ? new URL(envResponseUrl) : resolveUpstreamUrl(upstreamBase, fallbackResultPath(upstreamRequestId));
623
+ const upstreamOrigin = submitUrl.origin;
624
+ const adoptSameOrigin = (value, label, fallbackPath) => {
625
+ if (typeof value === "string" && value) try {
626
+ const parsed = new URL(value);
627
+ if (parsed.origin === upstreamOrigin) return parsed;
628
+ logger?.warn(`Upstream ${label} origin ${parsed.origin} differs from the upstream origin ${upstreamOrigin} — using the constructed canonical path instead`);
629
+ } catch {
630
+ logger?.warn(`Upstream ${label} is not a valid URL (${value.slice(0, 100)}) — using the constructed canonical path instead`);
631
+ }
632
+ return resolveUpstreamUrl(upstreamBase, fallbackPath);
633
+ };
634
+ const statusUrl = adoptSameOrigin(env.status_url, "status_url", fallbackStatusPath(upstreamRequestId));
635
+ const resultUrl = adoptSameOrigin(env.response_url, "response_url", fallbackResultPath(upstreamRequestId));
640
636
  while (true) {
641
637
  if (Date.now() > deadline) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
642
- const statusRes = await fetch(statusUrl, { headers });
638
+ const statusRes = await fetch(statusUrl, {
639
+ headers,
640
+ signal: fetchSignal()
641
+ });
643
642
  const statusText = await statusRes.text();
644
643
  if (!statusRes.ok) throw new Error(`Status ${statusRes.status}: ${statusText.slice(0, 200)}`);
645
- const statusJson = parseJsonOrThrow(statusText, "Status");
644
+ const statusParsed = parseJsonOrThrow(statusText, "Status");
645
+ if (statusParsed === null || typeof statusParsed !== "object" || Array.isArray(statusParsed)) throw new Error("Status response is not a JSON object");
646
+ const statusJson = statusParsed;
646
647
  const s = String(statusJson.status ?? "");
647
648
  if (s === "COMPLETED") break;
648
649
  if (s === "FAILED" || s === "ERROR" || s === "CANCELLED") throw new Error(`Upstream job terminated with status ${s}`);
649
650
  const remaining = deadline - Date.now();
650
- const sleep = Math.min(pollIntervalMs, Math.max(0, remaining));
651
- if (sleep <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
652
- await new Promise((r) => setTimeout(r, sleep));
651
+ if (remaining <= 0) throw new Error(`Queue walk timed out after ${timeoutMs}ms`);
652
+ const sleep = Math.min(pollIntervalMs, remaining);
653
+ await new Promise((r) => setTimeout(r, Math.max(0, sleep)));
653
654
  }
654
- const resultRes = await fetch(resultUrl, { headers });
655
+ const resultRes = await fetch(resultUrl, {
656
+ headers,
657
+ signal: fetchSignal()
658
+ });
655
659
  const resultText = await resultRes.text();
656
660
  if (!resultRes.ok) throw new Error(`Result ${resultRes.status}: ${resultText.slice(0, 200)}`);
657
661
  return parseJsonOrThrow(resultText, "Result");
@@ -672,15 +676,18 @@ async function proxyAndRecordFalQueueSubmit(args) {
672
676
  upstreamBase,
673
677
  submitPath: strippedPath,
674
678
  body,
675
- headers: buildFalForwardHeaders(req),
679
+ headers: buildForwardHeaders(req),
676
680
  pollIntervalMs: record.fal?.pollIntervalMs,
677
681
  timeoutMs: record.fal?.timeoutMs,
682
+ upstreamTimeoutMs: record.upstreamTimeoutMs,
678
683
  fallbackStatusPath: (id) => `${modelId}/requests/${id}/status`,
679
- fallbackResultPath: (id) => `${modelId}/requests/${id}`
684
+ fallbackResultPath: (id) => `${modelId}/requests/${id}`,
685
+ logger: defaults.logger
680
686
  });
681
687
  } catch (err) {
682
688
  const msg = err instanceof Error ? err.message : "Unknown queue-walk error";
683
689
  defaults.logger.error(`fal queue-walk proxy failed: ${msg}`);
690
+ if (res.destroyed || res.writableEnded) return "handled";
684
691
  journal.add({
685
692
  method: req.method ?? "POST",
686
693
  path: pathname,
@@ -706,7 +713,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
706
713
  status: 200
707
714
  }
708
715
  };
709
- persistFixture({
716
+ const persistResult = persistFixture({
710
717
  record,
711
718
  providerKey: "fal",
712
719
  testId: getTestId(req),
@@ -714,6 +721,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
714
721
  fixtures,
715
722
  logger: defaults.logger
716
723
  });
724
+ if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
717
725
  const newRequestId = crypto.randomUUID();
718
726
  const progression = resolveProgression(defaults.falQueue);
719
727
  const now = Date.now();
@@ -743,6 +751,7 @@ async function proxyAndRecordFalQueueSubmit(args) {
743
751
  cancel_url: `https://${FAL_HOSTS.queue}/${modelId}/requests/${newRequestId}/cancel`,
744
752
  queue_position: queuePosition(job)
745
753
  };
754
+ if (res.destroyed || res.writableEnded) return "handled";
746
755
  journal.add({
747
756
  method: req.method ?? "POST",
748
757
  path: pathname,
@@ -812,5 +821,5 @@ function respondNotFound(req, res, pathname, journal, requestId) {
812
821
  }
813
822
 
814
823
  //#endregion
815
- export { FalQueueStateMap, buildFalForwardHeaders, falQueueStates, handleFal, imageResponseToFalJson, videoResponseToFalJson, walkFalQueue };
824
+ export { FalQueueStateMap, falQueueStates, handleFal, imageResponseToFalJson, resolveProgression, videoResponseToFalJson, walkFalQueue };
816
825
  //# sourceMappingURL=fal.js.map
package/dist/fal.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fal.js","names":[],"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} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixture } 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 = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\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: \"Strict mode: no fixture matched\",\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.js","names":[],"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":"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"}
@@ -40,7 +40,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
40
40
  _context: require_helpers.getContext(req)
41
41
  };
42
42
  const testId = require_helpers.getTestId(req);
43
- const fixture = require_router.matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
43
+ const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
44
44
  const path = req.url ?? `/v1beta/models/${model}:embedContent`;
45
45
  if (fixture) {
46
46
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
@@ -108,7 +108,8 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
108
108
  return;
109
109
  }
110
110
  if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
111
- logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${path}`);
111
+ const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
112
+ logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", path, skippedBySequenceOrTurn));
112
113
  journal.add({
113
114
  method: req.method ?? "POST",
114
115
  path,
@@ -121,7 +122,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
121
122
  }
122
123
  });
123
124
  require_sse_writer.writeErrorResponse(res, 503, JSON.stringify({ error: {
124
- message: "Strict mode: no fixture matched",
125
+ message: strictMessage,
125
126
  code: 503,
126
127
  status: "UNAVAILABLE"
127
128
  } }));
@@ -1 +1 @@
1
- {"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","getContext","getTestId","matchFixture","applyChaos","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n logger.error(`STRICT: No fixture matched for ${req.method ?? \"POST\"} ${path}`);\n journal.add({\n method: req.method ?? \"POST\",\n path,\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 writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","getContext","getTestId","matchFixtureDiagnostic","applyChaos","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\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 writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, resolveResponse, resolveStrictMode, strictOverrideField } from "./helpers.js";
2
- import { matchFixture } from "./router.js";
1
+ import { flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
+ import { matchFixtureDiagnostic } from "./router.js";
3
3
  import { writeErrorResponse } from "./sse-writer.js";
4
4
  import { applyChaos } from "./chaos.js";
5
5
  import { proxyAndRecord } from "./recorder.js";
@@ -40,7 +40,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
40
40
  _context: getContext(req)
41
41
  };
42
42
  const testId = getTestId(req);
43
- const fixture = matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
43
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
44
44
  const path = req.url ?? `/v1beta/models/${model}:embedContent`;
45
45
  if (fixture) {
46
46
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
@@ -108,7 +108,8 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
108
108
  return;
109
109
  }
110
110
  if (resolveStrictMode(defaults.strict, req.headers)) {
111
- logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${path}`);
111
+ const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
112
+ logger.error(strictNoMatchLogLine(req.method ?? "POST", path, skippedBySequenceOrTurn));
112
113
  journal.add({
113
114
  method: req.method ?? "POST",
114
115
  path,
@@ -121,7 +122,7 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
121
122
  }
122
123
  });
123
124
  writeErrorResponse(res, 503, JSON.stringify({ error: {
124
- message: "Strict mode: no fixture matched",
125
+ message: strictMessage,
125
126
  code: 503,
126
127
  status: "UNAVAILABLE"
127
128
  } }));