@copilotkit/aimock 1.34.0 → 1.35.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 (150) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +2 -1
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/agui-types.d.ts.map +1 -1
  7. package/dist/bedrock-converse.cjs +110 -8
  8. package/dist/bedrock-converse.cjs.map +1 -1
  9. package/dist/bedrock-converse.d.cts.map +1 -1
  10. package/dist/bedrock-converse.d.ts.map +1 -1
  11. package/dist/bedrock-converse.js +111 -9
  12. package/dist/bedrock-converse.js.map +1 -1
  13. package/dist/bedrock.cjs +141 -4
  14. package/dist/bedrock.cjs.map +1 -1
  15. package/dist/bedrock.d.cts.map +1 -1
  16. package/dist/bedrock.d.ts.map +1 -1
  17. package/dist/bedrock.js +142 -5
  18. package/dist/bedrock.js.map +1 -1
  19. package/dist/cohere.cjs +89 -7
  20. package/dist/cohere.cjs.map +1 -1
  21. package/dist/cohere.d.cts.map +1 -1
  22. package/dist/cohere.d.ts.map +1 -1
  23. package/dist/cohere.js +90 -8
  24. package/dist/cohere.js.map +1 -1
  25. package/dist/config-loader.d.cts.map +1 -1
  26. package/dist/fixture-loader.cjs +121 -20
  27. package/dist/fixture-loader.cjs.map +1 -1
  28. package/dist/fixture-loader.d.cts.map +1 -1
  29. package/dist/fixture-loader.d.ts.map +1 -1
  30. package/dist/fixture-loader.js +121 -20
  31. package/dist/fixture-loader.js.map +1 -1
  32. package/dist/gemini-interactions.cjs +97 -60
  33. package/dist/gemini-interactions.cjs.map +1 -1
  34. package/dist/gemini-interactions.d.cts.map +1 -1
  35. package/dist/gemini-interactions.d.ts.map +1 -1
  36. package/dist/gemini-interactions.js +98 -61
  37. package/dist/gemini-interactions.js.map +1 -1
  38. package/dist/gemini.cjs +71 -6
  39. package/dist/gemini.cjs.map +1 -1
  40. package/dist/gemini.d.cts.map +1 -1
  41. package/dist/gemini.d.ts.map +1 -1
  42. package/dist/gemini.js +72 -7
  43. package/dist/gemini.js.map +1 -1
  44. package/dist/grok-video.cjs +834 -0
  45. package/dist/grok-video.cjs.map +1 -0
  46. package/dist/grok-video.d.cts +74 -0
  47. package/dist/grok-video.d.cts.map +1 -0
  48. package/dist/grok-video.d.ts +74 -0
  49. package/dist/grok-video.d.ts.map +1 -0
  50. package/dist/grok-video.js +829 -0
  51. package/dist/grok-video.js.map +1 -0
  52. package/dist/helpers.cjs +181 -2
  53. package/dist/helpers.cjs.map +1 -1
  54. package/dist/helpers.d.cts +33 -3
  55. package/dist/helpers.d.cts.map +1 -1
  56. package/dist/helpers.d.ts +33 -3
  57. package/dist/helpers.d.ts.map +1 -1
  58. package/dist/helpers.js +181 -3
  59. package/dist/helpers.js.map +1 -1
  60. package/dist/index.cjs +11 -0
  61. package/dist/index.d.cts +5 -3
  62. package/dist/index.d.ts +5 -3
  63. package/dist/index.js +4 -2
  64. package/dist/messages.cjs +99 -10
  65. package/dist/messages.cjs.map +1 -1
  66. package/dist/messages.d.cts.map +1 -1
  67. package/dist/messages.d.ts.map +1 -1
  68. package/dist/messages.js +100 -11
  69. package/dist/messages.js.map +1 -1
  70. package/dist/metrics.cjs +11 -1
  71. package/dist/metrics.cjs.map +1 -1
  72. package/dist/metrics.d.cts.map +1 -1
  73. package/dist/metrics.d.ts.map +1 -1
  74. package/dist/metrics.js +8 -1
  75. package/dist/metrics.js.map +1 -1
  76. package/dist/ollama.cjs +74 -4
  77. package/dist/ollama.cjs.map +1 -1
  78. package/dist/ollama.d.cts.map +1 -1
  79. package/dist/ollama.d.ts.map +1 -1
  80. package/dist/ollama.js +75 -5
  81. package/dist/ollama.js.map +1 -1
  82. package/dist/openrouter-video.cjs +19 -138
  83. package/dist/openrouter-video.cjs.map +1 -1
  84. package/dist/openrouter-video.d.cts.map +1 -1
  85. package/dist/openrouter-video.d.ts.map +1 -1
  86. package/dist/openrouter-video.js +1 -120
  87. package/dist/openrouter-video.js.map +1 -1
  88. package/dist/recorder.cjs +12 -9
  89. package/dist/recorder.cjs.map +1 -1
  90. package/dist/recorder.js +12 -9
  91. package/dist/recorder.js.map +1 -1
  92. package/dist/responses.cjs +136 -58
  93. package/dist/responses.cjs.map +1 -1
  94. package/dist/responses.d.cts +2 -2
  95. package/dist/responses.d.cts.map +1 -1
  96. package/dist/responses.d.ts +2 -2
  97. package/dist/responses.d.ts.map +1 -1
  98. package/dist/responses.js +137 -59
  99. package/dist/responses.js.map +1 -1
  100. package/dist/server.cjs +162 -20
  101. package/dist/server.cjs.map +1 -1
  102. package/dist/server.d.cts +4 -0
  103. package/dist/server.d.cts.map +1 -1
  104. package/dist/server.d.ts +4 -0
  105. package/dist/server.d.ts.map +1 -1
  106. package/dist/server.js +164 -22
  107. package/dist/server.js.map +1 -1
  108. package/dist/stream-collapse.cjs +300 -74
  109. package/dist/stream-collapse.cjs.map +1 -1
  110. package/dist/stream-collapse.d.cts +17 -1
  111. package/dist/stream-collapse.d.cts.map +1 -1
  112. package/dist/stream-collapse.d.ts +17 -1
  113. package/dist/stream-collapse.d.ts.map +1 -1
  114. package/dist/stream-collapse.js +300 -74
  115. package/dist/stream-collapse.js.map +1 -1
  116. package/dist/types.d.cts +104 -7
  117. package/dist/types.d.cts.map +1 -1
  118. package/dist/types.d.ts +104 -7
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/veo-video.cjs +794 -0
  121. package/dist/veo-video.cjs.map +1 -0
  122. package/dist/veo-video.d.cts +50 -0
  123. package/dist/veo-video.d.cts.map +1 -0
  124. package/dist/veo-video.d.ts +50 -0
  125. package/dist/veo-video.d.ts.map +1 -0
  126. package/dist/veo-video.js +789 -0
  127. package/dist/veo-video.js.map +1 -0
  128. package/dist/video-proxy-shared.cjs +145 -0
  129. package/dist/video-proxy-shared.cjs.map +1 -0
  130. package/dist/video-proxy-shared.js +140 -0
  131. package/dist/video-proxy-shared.js.map +1 -0
  132. package/dist/ws-gemini-live.cjs +96 -3
  133. package/dist/ws-gemini-live.cjs.map +1 -1
  134. package/dist/ws-gemini-live.d.cts.map +1 -1
  135. package/dist/ws-gemini-live.d.ts.map +1 -1
  136. package/dist/ws-gemini-live.js +97 -4
  137. package/dist/ws-gemini-live.js.map +1 -1
  138. package/dist/ws-realtime.cjs +281 -4
  139. package/dist/ws-realtime.cjs.map +1 -1
  140. package/dist/ws-realtime.d.cts.map +1 -1
  141. package/dist/ws-realtime.d.ts.map +1 -1
  142. package/dist/ws-realtime.js +282 -5
  143. package/dist/ws-realtime.js.map +1 -1
  144. package/dist/ws-responses.cjs +1 -1
  145. package/dist/ws-responses.cjs.map +1 -1
  146. package/dist/ws-responses.js +1 -1
  147. package/dist/ws-responses.js.map +1 -1
  148. package/fixtures/examples/llm/blocks-tool-first.json +13 -0
  149. package/package.json +1 -1
  150. package/skills/write-fixtures/SKILL.md +19 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"veo-video.js","names":[],"sources":["../src/veo-video.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordConfig,\n VideoResponse,\n} from \"./types.js\";\nimport {\n isVideoResponse,\n isErrorResponse,\n serializeErrorResponse,\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 { resolveProgression } from \"./fal.js\";\nimport {\n buildFixtureMatch,\n buildForwardHeaders,\n persistFixture,\n sanitizeHeaderValue,\n} from \"./recorder.js\";\nimport { resolveUpstreamUrl } from \"./url.js\";\nimport { readEnvelopeText, testIdSuffix, upstreamTimeoutSignal } from \"./video-proxy-shared.js\";\n\n/**\n * Google Veo async video lifecycle mock. Submit\n * `POST /v1beta/models/{model}:predictLongRunning` returns\n * `{ name: \"operations/...\" }`; status `GET /v1beta/operations/{name}` polls\n * `done:false → done:true`. The Files-API `uri` is served AS-IS — aimock does\n * NOT proxy or capture video bytes. With `record.providers.veo` configured an\n * unmatched submit becomes a live interactive proxy (submit + poll forwarded\n * 1:1, eager fixture capture on terminal status). Strict mode still wins.\n *\n * ASSUMED Veo create body shape (the `@google/genai` :predictLongRunning\n * envelope from issue #278): `{ instances: [{ prompt, image? }], parameters }`.\n * Match surface is `instances[0].prompt` only.\n */\n\n/** Files-API-shaped placeholder served when a completed fixture omits a uri. */\nconst DEFAULT_VEO_FILES_URI = \"https://generativelanguage.googleapis.com/v1beta/files/placeholder\";\n\n// The `:predictLongRunning` envelope nests the prompt under instances[].\ninterface VeoVideoRequest {\n instances?: Array<{ prompt?: unknown; [key: string]: unknown }>;\n parameters?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\n// ─── VeoVideoJobMap (TTL + bounded) ─────────────────────────────────────────\n\nexport const VEO_VIDEO_MAX_ENTRIES = 10_000;\nconst VEO_VIDEO_TTL_MS = 3_600_000; // 1 hour\n\ntype VeoVideoStatus = \"pending\" | \"in_progress\" | \"completed\" | \"failed\";\n\ninterface VeoVideoReplayJob {\n kind: \"replay\";\n operationName: string;\n status: VeoVideoStatus;\n pollCount: number;\n pollsBeforeInProgress: number;\n pollsBeforeCompleted: number;\n video: VideoResponse[\"video\"];\n /** Latch for the empty-`error` authoring warn on failed polls (once/job). */\n emptyErrorWarned?: boolean;\n}\n\ninterface VeoVideoRecordJob {\n kind: \"record\";\n operationName: string;\n status: VeoVideoStatus;\n upstreamOperationName: string;\n upstreamPollingUrl: string;\n match: Fixture[\"match\"];\n capturing?: boolean;\n}\n\nexport type VeoVideoJob = VeoVideoReplayJob | VeoVideoRecordJob;\n\ninterface VeoVideoEntry {\n job: VeoVideoJob;\n createdAt: number;\n}\n\n/**\n * Per-testId job state for the Veo video handler. Mirrors OpenRouterVideoJobMap\n * (openrouter-video.ts): lazy TTL eviction on `get`, FIFO eviction of the\n * oldest entries on `set` when over capacity, delete-before-set TTL refresh,\n * monotonic world-generation counter for reset-mid-flight detection, no\n * background sweep timer. Keys are `${testId}:${operationName}`.\n */\nexport class VeoVideoJobMap {\n private readonly entries = new Map<string, VeoVideoEntry>();\n private worldGeneration = 0;\n\n get generation(): number {\n return this.worldGeneration;\n }\n\n get(key: string): VeoVideoJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > VEO_VIDEO_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: VeoVideoJob): void {\n this.entries.delete(key);\n this.entries.set(key, { job, createdAt: Date.now() });\n if (this.entries.size > VEO_VIDEO_MAX_ENTRIES) {\n const excess = this.entries.size - VEO_VIDEO_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 this.worldGeneration++;\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n// ─── Job progression ────────────────────────────────────────────────────────\n\n/**\n * Maps the fixture's terminal video status onto the job lifecycle. Anything\n * that is not \"failed\" — including a fixture authored as \"processing\" — is\n * treated as completed, since this surface always drives jobs to a terminal\n * state.\n */\nfunction terminalStatus(job: VeoVideoReplayJob): VeoVideoStatus {\n return job.video.status === \"failed\" ? \"failed\" : \"completed\";\n}\n\n/**\n * Mutates a job in place to advance its state on a status poll.\n * `pending → in_progress → completed | failed` based on poll-count thresholds.\n * No-op once terminal. The in_progress threshold is checked first so a job\n * whose thresholds are equal still spends one poll in in_progress instead of\n * jumping straight to the terminal status (fal advanceJob semantics). The wire\n * value is derived from this internal status (pending/in_progress → done:false,\n * completed/failed → done:true).\n */\nfunction advanceVeoJob(job: VeoVideoReplayJob): void {\n if (job.status === \"completed\" || job.status === \"failed\") return;\n\n job.pollCount += 1;\n if (job.status === \"pending\" && job.pollCount >= job.pollsBeforeInProgress) {\n job.status = \"in_progress\";\n } else if (job.pollCount >= job.pollsBeforeCompleted) {\n job.status = terminalStatus(job);\n }\n}\n\n/** Gemini-style error envelope used by every Veo error response. */\nfunction geminiError(code: number, message: string, status: string): string {\n return JSON.stringify({ error: { code, message, status } });\n}\n\n/**\n * The done:true operation body for a completed replay job — the Files-API uri\n * is served as-is (the fixture's stored `video.url`, or the documented\n * placeholder when omitted).\n */\nfunction completedOperationBody(operationName: string, uri: string): Record<string, unknown> {\n return {\n name: operationName,\n done: true,\n response: {\n generateVideoResponse: {\n generatedSamples: [{ video: { uri } }],\n },\n },\n };\n}\n\n// ─── POST /v1beta/models/{model}:predictLongRunning — submit ─────────────────\n\nexport async function handleVeoVideoCreate(\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 jobs: VeoVideoJobMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? `/v1beta/models/${model}:predictLongRunning`;\n const method = req.method ?? \"POST\";\n\n let videoReq: VeoVideoRequest;\n try {\n videoReq = JSON.parse(raw) as VeoVideoRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(res, 400, geminiError(400, `Malformed JSON: ${detail}`, \"INVALID_ARGUMENT\"));\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object before touching fields.\n if (videoReq === null || typeof videoReq !== \"object\" || Array.isArray(videoReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n geminiError(400, \"Request body must be a JSON object\", \"INVALID_ARGUMENT\"),\n );\n return;\n }\n\n // Prompt lives at instances[0].prompt (the @google/genai envelope).\n const firstInstance = Array.isArray(videoReq.instances) ? videoReq.instances[0] : undefined;\n const prompt = firstInstance?.prompt;\n // Synthesize a structurally valid journal body for field-validation 400s\n // (JournalEntry.body is ChatCompletionRequest | null). Strip reserved\n // underscore-prefixed keys so a request cannot spoof handler discriminators.\n const sanitized = Object.fromEntries(\n Object.entries(videoReq).filter(([key]) => !key.startsWith(\"_\")),\n );\n const parsedBody: ChatCompletionRequest = { ...sanitized, model, messages: [] };\n\n if (typeof prompt !== \"string\" || !prompt) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: parsedBody,\n response: { status: 400, fixture: null },\n });\n const message =\n prompt === undefined\n ? \"Missing required parameter: 'instances[0].prompt'\"\n : \"Invalid type for parameter: 'instances[0].prompt' must be a non-empty string\";\n writeErrorResponse(res, 400, geminiError(400, message, \"INVALID_ARGUMENT\"));\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"video\",\n _videoProvider: \"veo\",\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\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(\n `No fixture matched for Veo request (model=${model}, msg=\"${prompt.slice(0, 80)}\")`,\n );\n }\n\n // Chaos rolls AFTER body validation and fixture matching (mirrors the\n // OpenRouter submit). An unmatched submit is proxied upstream when record\n // mode has a veo provider AND strict would not win — label that roll \"proxy\".\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture\n ? \"fixture\"\n : resolveStrictMode(defaults.strict, req.headers)\n ? \"internal\"\n : defaults.record?.providers.veo\n ? \"proxy\"\n : \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n // Strict mode wins over record: a strict no-match fails loudly with 503.\n if (resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\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(res, 503, geminiError(503, strictMessage, \"UNAVAILABLE\"));\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyVeoVideoSubmit({\n req,\n res,\n raw,\n syntheticReq,\n record: defaults.record,\n journal,\n defaults,\n jobs,\n method,\n path,\n });\n if (outcome === \"handled\") return;\n // outcome === \"no_upstream\" — fall through to 404 (fal convention).\n }\n\n journal.add({\n method,\n path,\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 writeErrorResponse(res, 404, geminiError(404, \"No fixture matched\", \"NOT_FOUND\"));\n return;\n }\n\n // World-generation snapshot (mirrors the record-submit guard): a fixtures\n // reset can land while the fixture's ResponseFactory below is awaited, and\n // the job insertion at the bottom must not seed the NEW world.\n const worldGeneration = jobs.generation;\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n if (res.destroyed || res.writableEnded) return;\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\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;\n }\n\n if (!isVideoResponse(response)) {\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n geminiError(500, \"Fixture response is not a video type\", \"INTERNAL\"),\n );\n return;\n }\n\n if (res.destroyed || res.writableEnded) return;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const fixtureStatus: string = response.video.status;\n if (fixtureStatus === \"processing\") {\n defaults.logger.warn(\n `Veo video fixture has status \"processing\" — treated as completed for the operations lifecycle`,\n );\n } else if (fixtureStatus !== \"completed\" && fixtureStatus !== \"failed\") {\n defaults.logger.warn(\n `Veo video fixture has unknown status \"${fixtureStatus}\" — treating as completed`,\n );\n }\n\n const operationName = `operations/${crypto.randomUUID()}`;\n const progression = resolveProgression(defaults.veoVideo);\n const job: VeoVideoReplayJob = {\n kind: \"replay\",\n operationName,\n status: \"pending\",\n pollCount: 0,\n pollsBeforeInProgress: progression.pollsBeforeInProgress,\n pollsBeforeCompleted: progression.pollsBeforeCompleted,\n video: { ...response.video },\n };\n // Default 0/0 progression seeds the job terminal at submit; the submit\n // envelope still reports a not-done operation.\n if (progression.pollsBeforeCompleted === 0) {\n job.status = terminalStatus(job);\n }\n if (jobs.generation === worldGeneration) {\n jobs.set(`${testId}:${operationName}`, job);\n } else {\n defaults.logger.warn(\n `Veo video submit resolved after a fixtures reset — not inserting operation ${operationName} into the new world (its polls will 404)`,\n );\n }\n\n // Embed the testId in the RETURNED name (not the stored key) so a multi-tenant\n // client can poll the opaque name without an x-test-id header — the testId\n // travels via getTestId's `?testId=` fallback. Mirrors OpenRouter's polling_url\n // treatment; testIdSuffix is \"\" for the default testId, keeping the name bare.\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ name: operationName + testIdSuffix(testId, \"?\") }));\n}\n\n// ─── GET /v1beta/operations/{name} — status poll ─────────────────────────────\n\nexport async function handleVeoVideoStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n operationName: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n jobs: VeoVideoJobMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? `/v1beta/${operationName}`;\n const method = req.method ?? \"GET\";\n\n // Chaos rolls BEFORE the job lookup — the label stays \"internal\" even in\n // record mode, and a chaos-dropped poll never reaches the upstream.\n if (\n applyChaos(\n res,\n null,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n const testId = getTestId(req);\n const key = `${testId}:${operationName}`;\n const job = jobs.get(key);\n\n if (!job) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n writeErrorResponse(\n res,\n 404,\n geminiError(404, `Operation ${operationName} not found`, \"NOT_FOUND\"),\n );\n return;\n }\n\n if (job.kind === \"record\") {\n // Strict means nothing reaches an upstream — a record job's polls are pure\n // upstream proxies, so an effective-strict request is refused with 503.\n if (resolveStrictMode(defaults.strict, req.headers)) {\n defaults.logger.error(\n `STRICT: Veo operation ${operationName} is proxied live upstream (record mode) — refusing the upstream poll`,\n );\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n geminiError(\n 503,\n `Strict mode: Veo operation ${operationName} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`,\n \"UNAVAILABLE\",\n ),\n );\n return;\n }\n await proxyVeoVideoRecordPoll({\n req,\n res,\n job,\n key,\n testId,\n fixtures,\n journal,\n defaults,\n jobs,\n method,\n path,\n });\n return;\n }\n\n // Guard BEFORE advancing or journaling (file convention): a disconnected\n // client consumes no progression step or TTL refresh.\n if (res.destroyed || res.writableEnded) return;\n advanceVeoJob(job);\n // Refresh the TTL on every replay poll (delete-before-set also moves the\n // entry to the back of the FIFO eviction order).\n jobs.set(key, job);\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n\n let body: Record<string, unknown>;\n if (job.status === \"completed\") {\n // Truthy guard (not `??`): an empty-string fixture url must still fall back\n // to the placeholder Files-API uri rather than being served as an empty uri.\n const rawUrl = job.video.url;\n body = completedOperationBody(\n operationName,\n typeof rawUrl === \"string\" && rawUrl ? rawUrl : DEFAULT_VEO_FILES_URI,\n );\n } else if (job.status === \"failed\") {\n if (job.video.error === \"\" && !job.emptyErrorWarned) {\n job.emptyErrorWarned = true;\n defaults.logger.warn(\n `Veo video fixture for operation ${operationName} has an empty error message — using the default`,\n );\n }\n body = {\n name: operationName,\n done: true,\n error: { code: 3, message: job.video.error || \"Video generation failed\" },\n };\n } else {\n body = { name: operationName, done: false };\n }\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n\n// ─── Record mode: live interactive proxy (submit) ───────────────────────────\n\n/**\n * Proxy an unmatched Veo submit to the configured upstream and answer the\n * client with a mock-rewritten envelope: a fresh aimock operation name. The\n * upstream lifecycle is driven interactively by the client's own polls.\n *\n * Returns \"no_upstream\" when record mode has no veo provider URL — the caller\n * falls through to its 404 branch (fal convention).\n */\nasync function proxyVeoVideoSubmit(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n raw: string;\n syntheticReq: ChatCompletionRequest;\n record: RecordConfig;\n journal: Journal;\n defaults: HandlerDefaults;\n jobs: VeoVideoJobMap;\n method: string;\n path: string;\n}): Promise<\"handled\" | \"no_upstream\"> {\n const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;\n\n const upstreamBase = record.providers.veo;\n if (!upstreamBase) {\n defaults.logger.warn(`No upstream URL configured for provider \"veo\" — cannot proxy`);\n return \"no_upstream\";\n }\n\n const proxyError = (msg: string): \"handled\" => {\n defaults.logger.error(`Veo video submit proxy failed: ${msg}`);\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 502,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n return \"handled\";\n };\n\n const submitPath = `/v1beta/models/${syntheticReq.model}:predictLongRunning`;\n let submitUrl: URL;\n let upstreamOrigin: string;\n try {\n submitUrl = resolveUpstreamUrl(upstreamBase, submitPath);\n upstreamOrigin = new URL(upstreamBase).origin;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return proxyError(`Invalid upstream URL: ${upstreamBase} (${msg})`);\n }\n\n defaults.logger.warn(\n `NO FIXTURE MATCH — proxying Veo video submit to ${upstreamBase}${submitPath}`,\n );\n\n // World-generation snapshot: a fixtures reset landing during the upstream\n // fetch clears the job map — the insertion guard compares against this.\n const worldGeneration = jobs.generation;\n\n let fetched: { status: number; contentType: string | null; text: string };\n try {\n const upstreamRes = await fetch(submitUrl, {\n method: \"POST\",\n headers: buildForwardHeaders(req),\n body: raw,\n signal: upstreamTimeoutSignal(record),\n });\n fetched = {\n status: upstreamRes.status,\n contentType: upstreamRes.headers.get(\"content-type\"),\n text: await readEnvelopeText(upstreamRes, record),\n };\n } catch (err) {\n return proxyError(err instanceof Error ? err.message : \"Unknown proxy error\");\n }\n\n if (fetched.status === 401 || fetched.status === 403) {\n // Real-API fidelity: relay an upstream auth rejection verbatim.\n defaults.logger.warn(\n `Upstream rejected the Veo video submit (${fetched.status}) — relaying the upstream status`,\n );\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: fetched.status,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(fetched.status, { \"Content-Type\": fetched.contentType ?? \"application/json\" });\n res.end(fetched.text);\n return \"handled\";\n }\n\n let upstreamOperationName: string;\n {\n if (fetched.status < 200 || fetched.status >= 300) {\n return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fetched.text);\n } catch {\n return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return proxyError(\"Submit response is not a JSON object\");\n }\n upstreamOperationName = String((parsed as Record<string, unknown>).name ?? \"\").trim();\n if (!upstreamOperationName) {\n return proxyError(\"Submit response missing name\");\n }\n }\n\n // Veo has no polling_url field — the poll URL is constructed from the\n // provider base + `/v1beta/{operationName}`. Origin-validate the constructed\n // URL (the client's Authorization travels to it on every poll); fall back to\n // a string on the provider base on any parse failure.\n let upstreamPollingUrl: string;\n try {\n const constructed = resolveUpstreamUrl(\n upstreamBase,\n `/v1beta/${upstreamOperationName.replace(/^\\/+/, \"\")}`,\n );\n if (constructed.origin === upstreamOrigin) {\n upstreamPollingUrl = constructed.toString();\n } else {\n defaults.logger.warn(\n `Constructed Veo poll URL origin ${constructed.origin} differs from the configured provider origin ${upstreamOrigin} — using the provider-origin URL`,\n );\n upstreamPollingUrl = `${upstreamOrigin}/v1beta/${upstreamOperationName.replace(/^\\/+/, \"\")}`;\n }\n } catch {\n defaults.logger.warn(\n `Could not construct the Veo poll URL for ${upstreamOperationName} — using the provider-origin URL`,\n );\n upstreamPollingUrl = `${upstreamOrigin}/v1beta/${upstreamOperationName.replace(/^\\/+/, \"\")}`;\n }\n\n const testId = getTestId(req);\n const matchRequest = defaults.requestTransform\n ? defaults.requestTransform(syntheticReq)\n : syntheticReq;\n const operationName = `operations/${crypto.randomUUID()}`;\n const job: VeoVideoRecordJob = {\n kind: \"record\",\n operationName,\n status: \"pending\",\n upstreamOperationName,\n upstreamPollingUrl,\n match: buildFixtureMatch(matchRequest, record),\n };\n if (jobs.generation === worldGeneration) {\n jobs.set(`${testId}:${operationName}`, job);\n } else {\n defaults.logger.warn(\n `Veo video submit for upstream operation ${upstreamOperationName} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`,\n );\n }\n\n if (res.destroyed || res.writableEnded) return \"handled\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 200,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n // Embed the testId in the RETURNED name (see the replay submit) so record-mode\n // multi-tenant polls resolve via `?testId=`; the upstream poll still uses the\n // bare job.operationName / job.upstreamPollingUrl, untouched by the suffix.\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ name: operationName + testIdSuffix(testId, \"?\") }));\n return \"handled\";\n}\n\n// ─── Record mode: live interactive proxy (poll + eager capture) ─────────────\n\n/**\n * Proxy a status poll for a record-mode Veo job 1:1 to the upstream and relay\n * the result with the operation name rewritten to the mock operation (the\n * Files-API uri inside `response` passes through verbatim — aimock never\n * downloads bytes). When the upstream reports `done:true` with a `response`,\n * the rewritten body is relayed IMMEDIATELY and the eager capture (persist the\n * uri as a fixture, mutate to a terminal replay job) runs DETACHED. A\n * `done:true` with an `error` persists a failed fixture synchronously. Under\n * `record.proxyOnly` nothing is captured, persisted, or mutated. Every\n * post-await map mutation is identity-guarded.\n */\nasync function proxyVeoVideoRecordPoll(args: {\n req: http.IncomingMessage;\n res: http.ServerResponse;\n job: VeoVideoRecordJob;\n key: string;\n testId: string;\n fixtures: Fixture[];\n journal: Journal;\n defaults: HandlerDefaults;\n jobs: VeoVideoJobMap;\n method: string;\n path: string;\n}): Promise<void> {\n const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;\n const logger = defaults.logger;\n\n const journalProxy = (status: number): void => {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: {\n status,\n fixture: null,\n source: \"proxy\",\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n };\n\n const proxyError = (msg: string): void => {\n logger.error(`Veo video poll proxy failed: ${msg}`);\n if (res.destroyed || res.writableEnded) return;\n journalProxy(502);\n writeErrorResponse(\n res,\n 502,\n JSON.stringify({\n error: { message: `Proxy to upstream failed: ${msg}`, type: \"proxy_error\" },\n }),\n );\n };\n\n // Recording can be disabled mid-flight — an orphaned record job must fail\n // loudly before contacting the upstream.\n const record = defaults.record;\n if (!record) {\n proxyError(\"record mode is no longer configured for an in-flight record job\");\n return;\n }\n\n let fetched: { status: number; contentType: string | null; text: string };\n try {\n const upstreamRes = await fetch(job.upstreamPollingUrl, {\n headers: buildForwardHeaders(req),\n signal: upstreamTimeoutSignal(record),\n });\n fetched = {\n status: upstreamRes.status,\n contentType: upstreamRes.headers.get(\"content-type\"),\n text: await readEnvelopeText(upstreamRes, record),\n };\n } catch (err) {\n proxyError(err instanceof Error ? err.message : \"Unknown proxy error\");\n return;\n }\n\n if (fetched.status === 401 || fetched.status === 403) {\n logger.warn(\n `Upstream rejected the Veo status poll for operation ${job.upstreamOperationName} (${fetched.status}) — relaying the upstream status`,\n );\n if (res.destroyed || res.writableEnded) return;\n journalProxy(fetched.status);\n res.writeHead(fetched.status, { \"Content-Type\": fetched.contentType ?? \"application/json\" });\n res.end(fetched.text);\n return;\n }\n\n let upstreamBody: Record<string, unknown>;\n {\n if (fetched.status < 200 || fetched.status >= 300) {\n proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);\n return;\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(fetched.text);\n } catch {\n proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);\n return;\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n proxyError(\"Status response is not a JSON object\");\n return;\n }\n upstreamBody = parsed as Record<string, unknown>;\n }\n\n // Rewrite ONLY the top-level operation name so no upstream identifier\n // escapes — the `response` (carrying the Files-API uri the client needs) and\n // every other field pass through verbatim.\n const relayBody: Record<string, unknown> = { ...upstreamBody, name: job.operationName };\n\n const relayJson = (): void => {\n if (res.destroyed || res.writableEnded) return;\n journalProxy(200);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(relayBody));\n };\n\n const done = upstreamBody.done === true;\n const upstreamError = upstreamBody.error;\n const hasError =\n upstreamError !== undefined && upstreamError !== null && typeof upstreamError === \"object\";\n\n if (!done) {\n // Non-terminal: identity-guarded status write + TTL refresh, relay.\n if (jobs.get(key) === job) {\n if (!job.capturing && job.status !== \"completed\" && job.status !== \"failed\") {\n job.status = \"in_progress\";\n }\n jobs.set(key, job);\n }\n relayJson();\n return;\n }\n\n if (hasError) {\n // Terminal failure: persist a failed fixture synchronously (mirrors the\n // OpenRouter failed branch), then relay.\n if (record.proxyOnly) {\n if (jobs.get(key) === job) {\n job.status = \"failed\";\n jobs.set(key, job);\n }\n relayJson();\n return;\n }\n if (jobs.get(key) !== job) {\n // A concurrent terminal poll already persisted/replaced the entry, or a\n // fixtures reset cleared the world — relay without a duplicate fixture.\n relayJson();\n return;\n }\n const errObj = upstreamError as Record<string, unknown>;\n const rawMessage = errObj.message;\n const error = typeof rawMessage === \"string\" && rawMessage ? rawMessage : undefined;\n const video: VideoResponse[\"video\"] = {\n id: job.upstreamOperationName,\n status: \"failed\",\n ...(error !== undefined ? { error } : {}),\n };\n const persistResult = persistFixture({\n record,\n providerKey: \"veo\",\n testId,\n fixture: { match: job.match, response: { video } },\n fixtures,\n logger,\n });\n if (persistResult.kind === \"failed\" && !res.headersSent) {\n res.setHeader(\"X-AIMock-Record-Error\", sanitizeHeaderValue(persistResult.error));\n }\n jobs.set(key, {\n kind: \"replay\",\n operationName: job.operationName,\n status: \"failed\",\n pollCount: 0,\n pollsBeforeInProgress: 0,\n pollsBeforeCompleted: 0,\n video: { ...video },\n });\n relayJson();\n return;\n }\n\n // Terminal success (done:true with a response).\n if (record.proxyOnly) {\n if (jobs.get(key) === job) {\n job.status = \"completed\";\n jobs.set(key, job);\n }\n relayJson();\n return;\n }\n\n if (job.capturing || jobs.get(key) !== job) {\n // A concurrent poll already entered the capture sequence, or this\n // reference is detached — relay without starting a second capture and\n // never re-insert the stale object.\n if (jobs.get(key) === job) {\n jobs.set(key, job); // TTL refresh\n }\n relayJson();\n return;\n }\n\n // Open the capturing window SYNCHRONOUSLY before the first await.\n job.capturing = true;\n job.status = \"completed\";\n jobs.set(key, job); // TTL refresh — identity-checked above, no await since\n\n // Relay IMMEDIATELY; the persist + replay mutation runs DETACHED.\n relayJson();\n\n void captureVeoVideoRecordFixture({\n job,\n key,\n testId,\n fixtures,\n defaults,\n jobs,\n record,\n upstreamBody,\n });\n}\n\n/**\n * Detached eager-capture for a completed Veo record job. Extracts the\n * Files-API uri from the terminal poll body (NO download), persists a\n * fixture, and mutates the map entry into a terminal replay job. Every failure\n * is handled internally; the returned promise NEVER rejects. World-generation\n * guarded immediately before persist.\n */\nasync function captureVeoVideoRecordFixture(args: {\n job: VeoVideoRecordJob;\n key: string;\n testId: string;\n fixtures: Fixture[];\n defaults: HandlerDefaults;\n jobs: VeoVideoJobMap;\n record: RecordConfig;\n upstreamBody: Record<string, unknown>;\n}): Promise<void> {\n const { job, key, testId, fixtures, defaults, jobs, record, upstreamBody } = args;\n const logger = defaults.logger;\n\n try {\n // Drill into response.generateVideoResponse.generatedSamples[0].video.uri.\n let uri: string | undefined;\n const response = upstreamBody.response;\n if (response !== null && typeof response === \"object\" && !Array.isArray(response)) {\n const gvr = (response as Record<string, unknown>).generateVideoResponse;\n if (gvr !== null && typeof gvr === \"object\" && !Array.isArray(gvr)) {\n const samples = (gvr as Record<string, unknown>).generatedSamples;\n const first = Array.isArray(samples) ? samples[0] : undefined;\n if (first !== null && typeof first === \"object\" && !Array.isArray(first)) {\n const video = (first as Record<string, unknown>).video;\n if (video !== null && typeof video === \"object\" && !Array.isArray(video)) {\n const rawUri = (video as Record<string, unknown>).uri;\n if (typeof rawUri === \"string\" && rawUri) uri = rawUri;\n }\n }\n }\n }\n if (!uri) {\n logger.warn(\n `Upstream Veo operation ${job.upstreamOperationName} completed without a usable Files-API uri — capture skipped, nothing persisted; the next completed poll retries`,\n );\n return;\n }\n\n const video: VideoResponse[\"video\"] = {\n id: job.upstreamOperationName,\n status: \"completed\",\n url: uri,\n };\n\n // World-generation guard: a fixtures reset landing during the await above\n // clears the job map — map identity is a valid proxy for \"same world\".\n if (jobs.get(key) !== job) {\n logger.warn(\n `Veo video capture for operation ${job.upstreamOperationName} discarded: the job map no longer holds this job (fixtures reset or TTL eviction) — nothing persisted`,\n );\n return;\n }\n persistFixture({\n record,\n providerKey: \"veo\",\n testId,\n fixture: { match: job.match, response: { video } },\n fixtures,\n logger,\n });\n\n // Mutate the entry into a terminal replay job: later polls serve locally.\n if (jobs.get(key) === job) {\n jobs.set(key, {\n kind: \"replay\",\n operationName: job.operationName,\n status: \"completed\",\n pollCount: 0,\n pollsBeforeInProgress: 0,\n pollsBeforeCompleted: 0,\n video: { ...video },\n });\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n logger.error(\n `Veo video capture for operation ${job.upstreamOperationName} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`,\n );\n } finally {\n // Close the capturing window when the map still holds this record job (a\n // step threw before the replay mutation). On the success path the entry\n // was just replaced; the detached object deliberately keeps capturing=true.\n if (jobs.get(key) === job) {\n job.capturing = false;\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,145 @@
1
+ const require_constants = require('./constants.cjs');
2
+ const require_recorder = require('./recorder.cjs');
3
+
4
+ //#region src/video-proxy-shared.ts
5
+ /**
6
+ * Shared upstream-proxy helpers for the async video-generation handlers
7
+ * (OpenRouter / Veo / Grok). These are the provider-agnostic primitives every
8
+ * live record-mode proxy needs: request-base derivation behind a forwarding
9
+ * proxy, testId URL-suffix embedding, upstream timeout signals, and bounded
10
+ * idle-based body reads. They were extracted verbatim from openrouter-video.ts
11
+ * so all three handlers share one implementation rather than triplicating it.
12
+ *
13
+ * NOT included here (byte-download only, OpenRouter-specific):
14
+ * `fetchHeadersWithTimeout` and the streaming content relay. Veo serves the
15
+ * Files-API uri as-is and Grok serves `video.url` as-is — neither downloads
16
+ * video bytes — so those stay private to openrouter-video.ts.
17
+ */
18
+ /**
19
+ * First non-empty value of a possibly array-typed, possibly comma-joined
20
+ * header. An empty header value or a leading-comma list (", host") would
21
+ * otherwise yield "" — triggering a spurious rejection warn and discarding
22
+ * valid later entries — so empty segments are skipped.
23
+ */
24
+ function firstForwardedValue(header) {
25
+ const raw = Array.isArray(header) ? header.join(",") : header;
26
+ if (raw === void 0) return void 0;
27
+ for (const segment of raw.split(",")) {
28
+ const trimmed = segment.trim();
29
+ if (trimmed) return trimmed;
30
+ }
31
+ }
32
+ const FORWARDED_HOST_RE = /^[a-zA-Z0-9._-]+(:\d+)?$/;
33
+ const FORWARDED_HOST_IPV6_RE = /^\[[0-9a-fA-F:.]+\](:\d+)?$/;
34
+ function requestBase(req, logger) {
35
+ const candidate = firstForwardedValue(req.headers["x-forwarded-proto"])?.toLowerCase();
36
+ const proto = candidate === "http" || candidate === "https" ? candidate : "http";
37
+ const fwdHost = firstForwardedValue(req.headers["x-forwarded-host"]);
38
+ const rawHost = req.headers.host;
39
+ let host = rawHost !== void 0 && (FORWARDED_HOST_RE.test(rawHost) || FORWARDED_HOST_IPV6_RE.test(rawHost)) ? rawHost : "localhost";
40
+ if (fwdHost !== void 0) if (FORWARDED_HOST_RE.test(fwdHost) || FORWARDED_HOST_IPV6_RE.test(fwdHost)) host = fwdHost;
41
+ else logger.warn(`x-forwarded-host value rejected, falling back to Host header: ${JSON.stringify(fwdHost.slice(0, 100))}`);
42
+ return `${proto}://${host}`;
43
+ }
44
+ /**
45
+ * Query-string suffix embedding the request's testId into generated URLs
46
+ * (polling_url, unsigned_urls). The provider SDK fetches these URLs with
47
+ * standard Authorization but no aimock-specific headers (no x-test-id) — so
48
+ * the testId must travel in the URL for getTestId's `?testId=` fallback to
49
+ * resolve the right job scope. The default testId is omitted to keep
50
+ * single-tenant URLs clean.
51
+ */
52
+ function testIdSuffix(testId, sep) {
53
+ return testId === require_constants.DEFAULT_TEST_ID ? "" : `${sep}testId=${encodeURIComponent(testId)}`;
54
+ }
55
+ /** Default upstream timeout for the live lifecycle proxy (matches recorder.ts). */
56
+ const DEFAULT_UPSTREAM_TIMEOUT_MS = 3e4;
57
+ /**
58
+ * Abort signal for the SMALL-JSON upstream fetches on this surface (submit,
59
+ * status poll, models listing), honoring `record.upstreamTimeoutMs` with the
60
+ * same clamp conventions as recorder.ts (non-finite / non-positive values
61
+ * fall back to the 30s default). Note the nuance (documented on
62
+ * RecordConfig.upstreamTimeoutMs): these envelope-sized fetches use the value
63
+ * as a TOTAL deadline via AbortSignal.timeout — indistinguishable from a
64
+ * socket-idle timeout for small bodies. The byte-bearing content fetches use
65
+ * `fetchHeadersWithTimeout` + `readBodyIdle` instead, which implement true
66
+ * idle semantics. An abort rejects the fetch and surfaces through the
67
+ * caller's existing failure path (502 proxy_error on submit/poll, models
68
+ * synthesis fallback).
69
+ */
70
+ function upstreamTimeoutSignal(record) {
71
+ return AbortSignal.timeout(require_recorder.clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS));
72
+ }
73
+ /**
74
+ * Buffer a fetch Response body with IDLE-based timeout semantics: a timer
75
+ * clamped from `record.bodyTimeoutMs` (30s default — same clamp as
76
+ * recorder.ts) is re-armed for every chunk, so a steadily-dripping body of
77
+ * any total duration completes and only a silent mid-body stall rejects.
78
+ * When `cap` > 0 the byte count is enforced DURING the read: on exceed the
79
+ * stream is cancelled and `{ overCap: true }` is returned with NOTHING
80
+ * oversized retained in memory — the cap is a memory guard as much as a
81
+ * disk guard.
82
+ */
83
+ async function readBodyIdle(res, record, cap = 0) {
84
+ const body = res.body;
85
+ if (!body) return {
86
+ overCap: false,
87
+ buf: Buffer.alloc(0)
88
+ };
89
+ const idleMs = require_recorder.clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);
90
+ const reader = body.getReader();
91
+ const chunks = [];
92
+ let total = 0;
93
+ try {
94
+ for (;;) {
95
+ let idleTimer;
96
+ const readPromise = reader.read();
97
+ readPromise.catch(() => {});
98
+ const result = await Promise.race([readPromise, new Promise((_, reject) => {
99
+ idleTimer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Upstream response body idle for ${idleMs}ms`)), idleMs);
100
+ })]).finally(() => clearTimeout(idleTimer));
101
+ if (result.done) break;
102
+ total += result.value.byteLength;
103
+ if (cap > 0 && total > cap) return {
104
+ overCap: true,
105
+ bytesRead: total
106
+ };
107
+ chunks.push(Buffer.from(result.value));
108
+ }
109
+ } finally {
110
+ reader.cancel().catch(() => {});
111
+ }
112
+ return {
113
+ overCap: false,
114
+ buf: Buffer.concat(chunks)
115
+ };
116
+ }
117
+ /**
118
+ * Cap on the small-JSON upstream envelope bodies (submit, status poll, models
119
+ * listing) buffered before parse/relay. Envelopes are KB-scale in practice —
120
+ * 1 MB is generous headroom even for a large models listing — and they are
121
+ * buffered in full, so an unbounded `text()` would be a memory hole on a
122
+ * hostile upstream.
123
+ */
124
+ const VIDEO_PROXY_ENVELOPE_BODY_CAP = 1024 * 1024;
125
+ /**
126
+ * Bounded read of a small-JSON upstream envelope body (submit, status poll,
127
+ * models listing): readBodyIdle's idle semantics plus the envelope cap. An
128
+ * over-cap body is a hard failure — the throw surfaces through each caller's
129
+ * existing failure path (502 proxy_error on submit/poll, models synthesis
130
+ * fallback) without retaining anything oversized in memory.
131
+ */
132
+ async function readEnvelopeText(res, record) {
133
+ const read = await readBodyIdle(res, record, VIDEO_PROXY_ENVELOPE_BODY_CAP);
134
+ if (read.overCap) throw new Error(`Upstream envelope body exceeded ${VIDEO_PROXY_ENVELOPE_BODY_CAP} bytes (read aborted at ${read.bytesRead})`);
135
+ return read.buf.toString("utf8");
136
+ }
137
+
138
+ //#endregion
139
+ exports.DEFAULT_UPSTREAM_TIMEOUT_MS = DEFAULT_UPSTREAM_TIMEOUT_MS;
140
+ exports.readBodyIdle = readBodyIdle;
141
+ exports.readEnvelopeText = readEnvelopeText;
142
+ exports.requestBase = requestBase;
143
+ exports.testIdSuffix = testIdSuffix;
144
+ exports.upstreamTimeoutSignal = upstreamTimeoutSignal;
145
+ //# sourceMappingURL=video-proxy-shared.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-proxy-shared.cjs","names":["DEFAULT_TEST_ID","clampTimeout"],"sources":["../src/video-proxy-shared.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { RecordConfig } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nimport { clampTimeout } from \"./recorder.js\";\n\n/**\n * Shared upstream-proxy helpers for the async video-generation handlers\n * (OpenRouter / Veo / Grok). These are the provider-agnostic primitives every\n * live record-mode proxy needs: request-base derivation behind a forwarding\n * proxy, testId URL-suffix embedding, upstream timeout signals, and bounded\n * idle-based body reads. They were extracted verbatim from openrouter-video.ts\n * so all three handlers share one implementation rather than triplicating it.\n *\n * NOT included here (byte-download only, OpenRouter-specific):\n * `fetchHeadersWithTimeout` and the streaming content relay. Veo serves the\n * Files-API uri as-is and Grok serves `video.url` as-is — neither downloads\n * video bytes — so those stay private to openrouter-video.ts.\n */\n\n/**\n * First non-empty value of a possibly array-typed, possibly comma-joined\n * header. An empty header value or a leading-comma list (\", host\") would\n * otherwise yield \"\" — triggering a spurious rejection warn and discarding\n * valid later entries — so empty segments are skipped.\n */\nexport function firstForwardedValue(header: string | string[] | undefined): string | undefined {\n const raw = Array.isArray(header) ? header.join(\",\") : header;\n if (raw === undefined) return undefined;\n for (const segment of raw.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed) return trimmed;\n }\n return undefined;\n}\n\n// Conservative host[:port] shape for x-forwarded-host. Spaces, slashes,\n// userinfo, or any other URL-structure character would corrupt (or smuggle\n// paths into) the generated URLs the value is interpolated into. Underscores\n// are admitted: the value feeds URL-string interpolation, not DNS validation,\n// and underscore hostnames are routine in docker-compose/k8s networks\n// (e.g. my_project_aimock:4010).\nconst FORWARDED_HOST_RE = /^[a-zA-Z0-9._-]+(:\\d+)?$/;\n// Bracketed IPv6 literal host[:port], e.g. [::1] or [::1]:8080 — the bare\n// RE above cannot admit \":\" inside the host without also admitting junk.\nconst FORWARDED_HOST_IPV6_RE = /^\\[[0-9a-fA-F:.]+\\](:\\d+)?$/;\n\nexport function requestBase(req: http.IncomingMessage, logger: Logger): string {\n // Honor x-forwarded-proto and x-forwarded-host so generated URLs survive a\n // TLS-terminating or host-rewriting proxy in front of the mock. First\n // non-empty value wins on comma-joined lists.\n const candidate = firstForwardedValue(req.headers[\"x-forwarded-proto\"])?.toLowerCase();\n // Allowlist http/https — any other value (ws, junk header data) falls back.\n const proto = candidate === \"http\" || candidate === \"https\" ? candidate : \"http\";\n // Like the proto allowlist, a forwarded host that doesn't look like a bare\n // host[:port] (or a bracketed IPv6 literal) falls back to the Host header —\n // with a warn, so a misconfigured proxy isn't silently ignored.\n const fwdHost = firstForwardedValue(req.headers[\"x-forwarded-host\"]);\n // The Host fallback gets the same host[:port] validation — a junk Host\n // (e.g. \"evil.com/path\") could otherwise smuggle URL structure into the\n // generated URLs. No warn: a missing/odd Host is transport-level noise,\n // unlike a misconfigured proxy's x-forwarded-host.\n const rawHost = req.headers.host;\n let host =\n rawHost !== undefined &&\n (FORWARDED_HOST_RE.test(rawHost) || FORWARDED_HOST_IPV6_RE.test(rawHost))\n ? rawHost\n : \"localhost\";\n if (fwdHost !== undefined) {\n if (FORWARDED_HOST_RE.test(fwdHost) || FORWARDED_HOST_IPV6_RE.test(fwdHost)) {\n host = fwdHost;\n } else {\n logger.warn(\n `x-forwarded-host value rejected, falling back to Host header: ${JSON.stringify(fwdHost.slice(0, 100))}`,\n );\n }\n }\n return `${proto}://${host}`;\n}\n\n/**\n * Query-string suffix embedding the request's testId into generated URLs\n * (polling_url, unsigned_urls). The provider SDK fetches these URLs with\n * standard Authorization but no aimock-specific headers (no x-test-id) — so\n * the testId must travel in the URL for getTestId's `?testId=` fallback to\n * resolve the right job scope. The default testId is omitted to keep\n * single-tenant URLs clean.\n */\nexport function testIdSuffix(testId: string, sep: \"?\" | \"&\"): string {\n return testId === DEFAULT_TEST_ID ? \"\" : `${sep}testId=${encodeURIComponent(testId)}`;\n}\n\n/** Default upstream timeout for the live lifecycle proxy (matches recorder.ts). */\nexport const DEFAULT_UPSTREAM_TIMEOUT_MS = 30_000;\n\n/**\n * Abort signal for the SMALL-JSON upstream fetches on this surface (submit,\n * status poll, models listing), honoring `record.upstreamTimeoutMs` with the\n * same clamp conventions as recorder.ts (non-finite / non-positive values\n * fall back to the 30s default). Note the nuance (documented on\n * RecordConfig.upstreamTimeoutMs): these envelope-sized fetches use the value\n * as a TOTAL deadline via AbortSignal.timeout — indistinguishable from a\n * socket-idle timeout for small bodies. The byte-bearing content fetches use\n * `fetchHeadersWithTimeout` + `readBodyIdle` instead, which implement true\n * idle semantics. An abort rejects the fetch and surfaces through the\n * caller's existing failure path (502 proxy_error on submit/poll, models\n * synthesis fallback).\n */\nexport function upstreamTimeoutSignal(record: RecordConfig | undefined): AbortSignal {\n return AbortSignal.timeout(clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS));\n}\n\n/**\n * Buffer a fetch Response body with IDLE-based timeout semantics: a timer\n * clamped from `record.bodyTimeoutMs` (30s default — same clamp as\n * recorder.ts) is re-armed for every chunk, so a steadily-dripping body of\n * any total duration completes and only a silent mid-body stall rejects.\n * When `cap` > 0 the byte count is enforced DURING the read: on exceed the\n * stream is cancelled and `{ overCap: true }` is returned with NOTHING\n * oversized retained in memory — the cap is a memory guard as much as a\n * disk guard.\n */\nexport async function readBodyIdle(\n res: Response,\n record: RecordConfig | undefined,\n cap = 0,\n): Promise<{ overCap: false; buf: Buffer } | { overCap: true; bytesRead: number }> {\n const body = res.body;\n if (!body) return { overCap: false, buf: Buffer.alloc(0) };\n const idleMs = clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);\n const reader = body.getReader();\n const chunks: Buffer[] = [];\n let total = 0;\n try {\n for (;;) {\n let idleTimer: NodeJS.Timeout | undefined;\n // A stream error landing AFTER the idle timeout has already won the\n // race must never become an unhandledRejection (process crash) — attach\n // a no-op rejection handler to the read promise BEFORE racing.\n // Promise.race subscribes to its inputs too, so this is deliberate\n // defense-in-depth pinning the invariant against a refactor that races\n // the read differently; the race below still observes the rejection\n // normally when the read loses first.\n const readPromise = reader.read();\n readPromise.catch(() => {});\n const result = await Promise.race([\n readPromise,\n new Promise<never>((_, reject) => {\n idleTimer = setTimeout(\n () => reject(new Error(`Upstream response body idle for ${idleMs}ms`)),\n idleMs,\n );\n }),\n ]).finally(() => clearTimeout(idleTimer));\n if (result.done) break;\n total += result.value.byteLength;\n if (cap > 0 && total > cap) {\n return { overCap: true, bytesRead: total };\n }\n chunks.push(Buffer.from(result.value));\n }\n } finally {\n // Idle expiry and the over-cap early return leave the stream open —\n // release it. After a normal completion this is a no-op.\n void reader.cancel().catch(() => {});\n }\n return { overCap: false, buf: Buffer.concat(chunks) };\n}\n\n/**\n * Cap on the small-JSON upstream envelope bodies (submit, status poll, models\n * listing) buffered before parse/relay. Envelopes are KB-scale in practice —\n * 1 MB is generous headroom even for a large models listing — and they are\n * buffered in full, so an unbounded `text()` would be a memory hole on a\n * hostile upstream.\n */\nexport const VIDEO_PROXY_ENVELOPE_BODY_CAP = 1024 * 1024;\n\n/**\n * Bounded read of a small-JSON upstream envelope body (submit, status poll,\n * models listing): readBodyIdle's idle semantics plus the envelope cap. An\n * over-cap body is a hard failure — the throw surfaces through each caller's\n * existing failure path (502 proxy_error on submit/poll, models synthesis\n * fallback) without retaining anything oversized in memory.\n */\nexport async function readEnvelopeText(\n res: Response,\n record: RecordConfig | undefined,\n): Promise<string> {\n const read = await readBodyIdle(res, record, VIDEO_PROXY_ENVELOPE_BODY_CAP);\n if (read.overCap) {\n throw new Error(\n `Upstream envelope body exceeded ${VIDEO_PROXY_ENVELOPE_BODY_CAP} bytes (read aborted at ${read.bytesRead})`,\n );\n }\n return read.buf.toString(\"utf8\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,oBAAoB,QAA2D;CAC7F,MAAM,MAAM,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK,IAAI,GAAG;AACvD,KAAI,QAAQ,OAAW,QAAO;AAC9B,MAAK,MAAM,WAAW,IAAI,MAAM,IAAI,EAAE;EACpC,MAAM,UAAU,QAAQ,MAAM;AAC9B,MAAI,QAAS,QAAO;;;AAWxB,MAAM,oBAAoB;AAG1B,MAAM,yBAAyB;AAE/B,SAAgB,YAAY,KAA2B,QAAwB;CAI7E,MAAM,YAAY,oBAAoB,IAAI,QAAQ,qBAAqB,EAAE,aAAa;CAEtF,MAAM,QAAQ,cAAc,UAAU,cAAc,UAAU,YAAY;CAI1E,MAAM,UAAU,oBAAoB,IAAI,QAAQ,oBAAoB;CAKpE,MAAM,UAAU,IAAI,QAAQ;CAC5B,IAAI,OACF,YAAY,WACX,kBAAkB,KAAK,QAAQ,IAAI,uBAAuB,KAAK,QAAQ,IACpE,UACA;AACN,KAAI,YAAY,OACd,KAAI,kBAAkB,KAAK,QAAQ,IAAI,uBAAuB,KAAK,QAAQ,CACzE,QAAO;KAEP,QAAO,KACL,iEAAiE,KAAK,UAAU,QAAQ,MAAM,GAAG,IAAI,CAAC,GACvG;AAGL,QAAO,GAAG,MAAM,KAAK;;;;;;;;;;AAWvB,SAAgB,aAAa,QAAgB,KAAwB;AACnE,QAAO,WAAWA,oCAAkB,KAAK,GAAG,IAAI,SAAS,mBAAmB,OAAO;;;AAIrF,MAAa,8BAA8B;;;;;;;;;;;;;;AAe3C,SAAgB,sBAAsB,QAA+C;AACnF,QAAO,YAAY,QAAQC,8BAAa,QAAQ,mBAAmB,4BAA4B,CAAC;;;;;;;;;;;;AAalG,eAAsB,aACpB,KACA,QACA,MAAM,GAC2E;CACjF,MAAM,OAAO,IAAI;AACjB,KAAI,CAAC,KAAM,QAAO;EAAE,SAAS;EAAO,KAAK,OAAO,MAAM,EAAE;EAAE;CAC1D,MAAM,SAASA,8BAAa,QAAQ,eAAe,4BAA4B;CAC/E,MAAM,SAAS,KAAK,WAAW;CAC/B,MAAM,SAAmB,EAAE;CAC3B,IAAI,QAAQ;AACZ,KAAI;AACF,WAAS;GACP,IAAI;GAQJ,MAAM,cAAc,OAAO,MAAM;AACjC,eAAY,YAAY,GAAG;GAC3B,MAAM,SAAS,MAAM,QAAQ,KAAK,CAChC,aACA,IAAI,SAAgB,GAAG,WAAW;AAChC,gBAAY,iBACJ,uBAAO,IAAI,MAAM,mCAAmC,OAAO,IAAI,CAAC,EACtE,OACD;KACD,CACH,CAAC,CAAC,cAAc,aAAa,UAAU,CAAC;AACzC,OAAI,OAAO,KAAM;AACjB,YAAS,OAAO,MAAM;AACtB,OAAI,MAAM,KAAK,QAAQ,IACrB,QAAO;IAAE,SAAS;IAAM,WAAW;IAAO;AAE5C,UAAO,KAAK,OAAO,KAAK,OAAO,MAAM,CAAC;;WAEhC;AAGR,EAAK,OAAO,QAAQ,CAAC,YAAY,GAAG;;AAEtC,QAAO;EAAE,SAAS;EAAO,KAAK,OAAO,OAAO,OAAO;EAAE;;;;;;;;;AAUvD,MAAa,gCAAgC,OAAO;;;;;;;;AASpD,eAAsB,iBACpB,KACA,QACiB;CACjB,MAAM,OAAO,MAAM,aAAa,KAAK,QAAQ,8BAA8B;AAC3E,KAAI,KAAK,QACP,OAAM,IAAI,MACR,mCAAmC,8BAA8B,0BAA0B,KAAK,UAAU,GAC3G;AAEH,QAAO,KAAK,IAAI,SAAS,OAAO"}
@@ -0,0 +1,140 @@
1
+ import { DEFAULT_TEST_ID } from "./constants.js";
2
+ import { clampTimeout } from "./recorder.js";
3
+
4
+ //#region src/video-proxy-shared.ts
5
+ /**
6
+ * Shared upstream-proxy helpers for the async video-generation handlers
7
+ * (OpenRouter / Veo / Grok). These are the provider-agnostic primitives every
8
+ * live record-mode proxy needs: request-base derivation behind a forwarding
9
+ * proxy, testId URL-suffix embedding, upstream timeout signals, and bounded
10
+ * idle-based body reads. They were extracted verbatim from openrouter-video.ts
11
+ * so all three handlers share one implementation rather than triplicating it.
12
+ *
13
+ * NOT included here (byte-download only, OpenRouter-specific):
14
+ * `fetchHeadersWithTimeout` and the streaming content relay. Veo serves the
15
+ * Files-API uri as-is and Grok serves `video.url` as-is — neither downloads
16
+ * video bytes — so those stay private to openrouter-video.ts.
17
+ */
18
+ /**
19
+ * First non-empty value of a possibly array-typed, possibly comma-joined
20
+ * header. An empty header value or a leading-comma list (", host") would
21
+ * otherwise yield "" — triggering a spurious rejection warn and discarding
22
+ * valid later entries — so empty segments are skipped.
23
+ */
24
+ function firstForwardedValue(header) {
25
+ const raw = Array.isArray(header) ? header.join(",") : header;
26
+ if (raw === void 0) return void 0;
27
+ for (const segment of raw.split(",")) {
28
+ const trimmed = segment.trim();
29
+ if (trimmed) return trimmed;
30
+ }
31
+ }
32
+ const FORWARDED_HOST_RE = /^[a-zA-Z0-9._-]+(:\d+)?$/;
33
+ const FORWARDED_HOST_IPV6_RE = /^\[[0-9a-fA-F:.]+\](:\d+)?$/;
34
+ function requestBase(req, logger) {
35
+ const candidate = firstForwardedValue(req.headers["x-forwarded-proto"])?.toLowerCase();
36
+ const proto = candidate === "http" || candidate === "https" ? candidate : "http";
37
+ const fwdHost = firstForwardedValue(req.headers["x-forwarded-host"]);
38
+ const rawHost = req.headers.host;
39
+ let host = rawHost !== void 0 && (FORWARDED_HOST_RE.test(rawHost) || FORWARDED_HOST_IPV6_RE.test(rawHost)) ? rawHost : "localhost";
40
+ if (fwdHost !== void 0) if (FORWARDED_HOST_RE.test(fwdHost) || FORWARDED_HOST_IPV6_RE.test(fwdHost)) host = fwdHost;
41
+ else logger.warn(`x-forwarded-host value rejected, falling back to Host header: ${JSON.stringify(fwdHost.slice(0, 100))}`);
42
+ return `${proto}://${host}`;
43
+ }
44
+ /**
45
+ * Query-string suffix embedding the request's testId into generated URLs
46
+ * (polling_url, unsigned_urls). The provider SDK fetches these URLs with
47
+ * standard Authorization but no aimock-specific headers (no x-test-id) — so
48
+ * the testId must travel in the URL for getTestId's `?testId=` fallback to
49
+ * resolve the right job scope. The default testId is omitted to keep
50
+ * single-tenant URLs clean.
51
+ */
52
+ function testIdSuffix(testId, sep) {
53
+ return testId === DEFAULT_TEST_ID ? "" : `${sep}testId=${encodeURIComponent(testId)}`;
54
+ }
55
+ /** Default upstream timeout for the live lifecycle proxy (matches recorder.ts). */
56
+ const DEFAULT_UPSTREAM_TIMEOUT_MS = 3e4;
57
+ /**
58
+ * Abort signal for the SMALL-JSON upstream fetches on this surface (submit,
59
+ * status poll, models listing), honoring `record.upstreamTimeoutMs` with the
60
+ * same clamp conventions as recorder.ts (non-finite / non-positive values
61
+ * fall back to the 30s default). Note the nuance (documented on
62
+ * RecordConfig.upstreamTimeoutMs): these envelope-sized fetches use the value
63
+ * as a TOTAL deadline via AbortSignal.timeout — indistinguishable from a
64
+ * socket-idle timeout for small bodies. The byte-bearing content fetches use
65
+ * `fetchHeadersWithTimeout` + `readBodyIdle` instead, which implement true
66
+ * idle semantics. An abort rejects the fetch and surfaces through the
67
+ * caller's existing failure path (502 proxy_error on submit/poll, models
68
+ * synthesis fallback).
69
+ */
70
+ function upstreamTimeoutSignal(record) {
71
+ return AbortSignal.timeout(clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS));
72
+ }
73
+ /**
74
+ * Buffer a fetch Response body with IDLE-based timeout semantics: a timer
75
+ * clamped from `record.bodyTimeoutMs` (30s default — same clamp as
76
+ * recorder.ts) is re-armed for every chunk, so a steadily-dripping body of
77
+ * any total duration completes and only a silent mid-body stall rejects.
78
+ * When `cap` > 0 the byte count is enforced DURING the read: on exceed the
79
+ * stream is cancelled and `{ overCap: true }` is returned with NOTHING
80
+ * oversized retained in memory — the cap is a memory guard as much as a
81
+ * disk guard.
82
+ */
83
+ async function readBodyIdle(res, record, cap = 0) {
84
+ const body = res.body;
85
+ if (!body) return {
86
+ overCap: false,
87
+ buf: Buffer.alloc(0)
88
+ };
89
+ const idleMs = clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);
90
+ const reader = body.getReader();
91
+ const chunks = [];
92
+ let total = 0;
93
+ try {
94
+ for (;;) {
95
+ let idleTimer;
96
+ const readPromise = reader.read();
97
+ readPromise.catch(() => {});
98
+ const result = await Promise.race([readPromise, new Promise((_, reject) => {
99
+ idleTimer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Upstream response body idle for ${idleMs}ms`)), idleMs);
100
+ })]).finally(() => clearTimeout(idleTimer));
101
+ if (result.done) break;
102
+ total += result.value.byteLength;
103
+ if (cap > 0 && total > cap) return {
104
+ overCap: true,
105
+ bytesRead: total
106
+ };
107
+ chunks.push(Buffer.from(result.value));
108
+ }
109
+ } finally {
110
+ reader.cancel().catch(() => {});
111
+ }
112
+ return {
113
+ overCap: false,
114
+ buf: Buffer.concat(chunks)
115
+ };
116
+ }
117
+ /**
118
+ * Cap on the small-JSON upstream envelope bodies (submit, status poll, models
119
+ * listing) buffered before parse/relay. Envelopes are KB-scale in practice —
120
+ * 1 MB is generous headroom even for a large models listing — and they are
121
+ * buffered in full, so an unbounded `text()` would be a memory hole on a
122
+ * hostile upstream.
123
+ */
124
+ const VIDEO_PROXY_ENVELOPE_BODY_CAP = 1024 * 1024;
125
+ /**
126
+ * Bounded read of a small-JSON upstream envelope body (submit, status poll,
127
+ * models listing): readBodyIdle's idle semantics plus the envelope cap. An
128
+ * over-cap body is a hard failure — the throw surfaces through each caller's
129
+ * existing failure path (502 proxy_error on submit/poll, models synthesis
130
+ * fallback) without retaining anything oversized in memory.
131
+ */
132
+ async function readEnvelopeText(res, record) {
133
+ const read = await readBodyIdle(res, record, VIDEO_PROXY_ENVELOPE_BODY_CAP);
134
+ if (read.overCap) throw new Error(`Upstream envelope body exceeded ${VIDEO_PROXY_ENVELOPE_BODY_CAP} bytes (read aborted at ${read.bytesRead})`);
135
+ return read.buf.toString("utf8");
136
+ }
137
+
138
+ //#endregion
139
+ export { DEFAULT_UPSTREAM_TIMEOUT_MS, readBodyIdle, readEnvelopeText, requestBase, testIdSuffix, upstreamTimeoutSignal };
140
+ //# sourceMappingURL=video-proxy-shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-proxy-shared.js","names":[],"sources":["../src/video-proxy-shared.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { RecordConfig } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nimport { clampTimeout } from \"./recorder.js\";\n\n/**\n * Shared upstream-proxy helpers for the async video-generation handlers\n * (OpenRouter / Veo / Grok). These are the provider-agnostic primitives every\n * live record-mode proxy needs: request-base derivation behind a forwarding\n * proxy, testId URL-suffix embedding, upstream timeout signals, and bounded\n * idle-based body reads. They were extracted verbatim from openrouter-video.ts\n * so all three handlers share one implementation rather than triplicating it.\n *\n * NOT included here (byte-download only, OpenRouter-specific):\n * `fetchHeadersWithTimeout` and the streaming content relay. Veo serves the\n * Files-API uri as-is and Grok serves `video.url` as-is — neither downloads\n * video bytes — so those stay private to openrouter-video.ts.\n */\n\n/**\n * First non-empty value of a possibly array-typed, possibly comma-joined\n * header. An empty header value or a leading-comma list (\", host\") would\n * otherwise yield \"\" — triggering a spurious rejection warn and discarding\n * valid later entries — so empty segments are skipped.\n */\nexport function firstForwardedValue(header: string | string[] | undefined): string | undefined {\n const raw = Array.isArray(header) ? header.join(\",\") : header;\n if (raw === undefined) return undefined;\n for (const segment of raw.split(\",\")) {\n const trimmed = segment.trim();\n if (trimmed) return trimmed;\n }\n return undefined;\n}\n\n// Conservative host[:port] shape for x-forwarded-host. Spaces, slashes,\n// userinfo, or any other URL-structure character would corrupt (or smuggle\n// paths into) the generated URLs the value is interpolated into. Underscores\n// are admitted: the value feeds URL-string interpolation, not DNS validation,\n// and underscore hostnames are routine in docker-compose/k8s networks\n// (e.g. my_project_aimock:4010).\nconst FORWARDED_HOST_RE = /^[a-zA-Z0-9._-]+(:\\d+)?$/;\n// Bracketed IPv6 literal host[:port], e.g. [::1] or [::1]:8080 — the bare\n// RE above cannot admit \":\" inside the host without also admitting junk.\nconst FORWARDED_HOST_IPV6_RE = /^\\[[0-9a-fA-F:.]+\\](:\\d+)?$/;\n\nexport function requestBase(req: http.IncomingMessage, logger: Logger): string {\n // Honor x-forwarded-proto and x-forwarded-host so generated URLs survive a\n // TLS-terminating or host-rewriting proxy in front of the mock. First\n // non-empty value wins on comma-joined lists.\n const candidate = firstForwardedValue(req.headers[\"x-forwarded-proto\"])?.toLowerCase();\n // Allowlist http/https — any other value (ws, junk header data) falls back.\n const proto = candidate === \"http\" || candidate === \"https\" ? candidate : \"http\";\n // Like the proto allowlist, a forwarded host that doesn't look like a bare\n // host[:port] (or a bracketed IPv6 literal) falls back to the Host header —\n // with a warn, so a misconfigured proxy isn't silently ignored.\n const fwdHost = firstForwardedValue(req.headers[\"x-forwarded-host\"]);\n // The Host fallback gets the same host[:port] validation — a junk Host\n // (e.g. \"evil.com/path\") could otherwise smuggle URL structure into the\n // generated URLs. No warn: a missing/odd Host is transport-level noise,\n // unlike a misconfigured proxy's x-forwarded-host.\n const rawHost = req.headers.host;\n let host =\n rawHost !== undefined &&\n (FORWARDED_HOST_RE.test(rawHost) || FORWARDED_HOST_IPV6_RE.test(rawHost))\n ? rawHost\n : \"localhost\";\n if (fwdHost !== undefined) {\n if (FORWARDED_HOST_RE.test(fwdHost) || FORWARDED_HOST_IPV6_RE.test(fwdHost)) {\n host = fwdHost;\n } else {\n logger.warn(\n `x-forwarded-host value rejected, falling back to Host header: ${JSON.stringify(fwdHost.slice(0, 100))}`,\n );\n }\n }\n return `${proto}://${host}`;\n}\n\n/**\n * Query-string suffix embedding the request's testId into generated URLs\n * (polling_url, unsigned_urls). The provider SDK fetches these URLs with\n * standard Authorization but no aimock-specific headers (no x-test-id) — so\n * the testId must travel in the URL for getTestId's `?testId=` fallback to\n * resolve the right job scope. The default testId is omitted to keep\n * single-tenant URLs clean.\n */\nexport function testIdSuffix(testId: string, sep: \"?\" | \"&\"): string {\n return testId === DEFAULT_TEST_ID ? \"\" : `${sep}testId=${encodeURIComponent(testId)}`;\n}\n\n/** Default upstream timeout for the live lifecycle proxy (matches recorder.ts). */\nexport const DEFAULT_UPSTREAM_TIMEOUT_MS = 30_000;\n\n/**\n * Abort signal for the SMALL-JSON upstream fetches on this surface (submit,\n * status poll, models listing), honoring `record.upstreamTimeoutMs` with the\n * same clamp conventions as recorder.ts (non-finite / non-positive values\n * fall back to the 30s default). Note the nuance (documented on\n * RecordConfig.upstreamTimeoutMs): these envelope-sized fetches use the value\n * as a TOTAL deadline via AbortSignal.timeout — indistinguishable from a\n * socket-idle timeout for small bodies. The byte-bearing content fetches use\n * `fetchHeadersWithTimeout` + `readBodyIdle` instead, which implement true\n * idle semantics. An abort rejects the fetch and surfaces through the\n * caller's existing failure path (502 proxy_error on submit/poll, models\n * synthesis fallback).\n */\nexport function upstreamTimeoutSignal(record: RecordConfig | undefined): AbortSignal {\n return AbortSignal.timeout(clampTimeout(record?.upstreamTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS));\n}\n\n/**\n * Buffer a fetch Response body with IDLE-based timeout semantics: a timer\n * clamped from `record.bodyTimeoutMs` (30s default — same clamp as\n * recorder.ts) is re-armed for every chunk, so a steadily-dripping body of\n * any total duration completes and only a silent mid-body stall rejects.\n * When `cap` > 0 the byte count is enforced DURING the read: on exceed the\n * stream is cancelled and `{ overCap: true }` is returned with NOTHING\n * oversized retained in memory — the cap is a memory guard as much as a\n * disk guard.\n */\nexport async function readBodyIdle(\n res: Response,\n record: RecordConfig | undefined,\n cap = 0,\n): Promise<{ overCap: false; buf: Buffer } | { overCap: true; bytesRead: number }> {\n const body = res.body;\n if (!body) return { overCap: false, buf: Buffer.alloc(0) };\n const idleMs = clampTimeout(record?.bodyTimeoutMs, DEFAULT_UPSTREAM_TIMEOUT_MS);\n const reader = body.getReader();\n const chunks: Buffer[] = [];\n let total = 0;\n try {\n for (;;) {\n let idleTimer: NodeJS.Timeout | undefined;\n // A stream error landing AFTER the idle timeout has already won the\n // race must never become an unhandledRejection (process crash) — attach\n // a no-op rejection handler to the read promise BEFORE racing.\n // Promise.race subscribes to its inputs too, so this is deliberate\n // defense-in-depth pinning the invariant against a refactor that races\n // the read differently; the race below still observes the rejection\n // normally when the read loses first.\n const readPromise = reader.read();\n readPromise.catch(() => {});\n const result = await Promise.race([\n readPromise,\n new Promise<never>((_, reject) => {\n idleTimer = setTimeout(\n () => reject(new Error(`Upstream response body idle for ${idleMs}ms`)),\n idleMs,\n );\n }),\n ]).finally(() => clearTimeout(idleTimer));\n if (result.done) break;\n total += result.value.byteLength;\n if (cap > 0 && total > cap) {\n return { overCap: true, bytesRead: total };\n }\n chunks.push(Buffer.from(result.value));\n }\n } finally {\n // Idle expiry and the over-cap early return leave the stream open —\n // release it. After a normal completion this is a no-op.\n void reader.cancel().catch(() => {});\n }\n return { overCap: false, buf: Buffer.concat(chunks) };\n}\n\n/**\n * Cap on the small-JSON upstream envelope bodies (submit, status poll, models\n * listing) buffered before parse/relay. Envelopes are KB-scale in practice —\n * 1 MB is generous headroom even for a large models listing — and they are\n * buffered in full, so an unbounded `text()` would be a memory hole on a\n * hostile upstream.\n */\nexport const VIDEO_PROXY_ENVELOPE_BODY_CAP = 1024 * 1024;\n\n/**\n * Bounded read of a small-JSON upstream envelope body (submit, status poll,\n * models listing): readBodyIdle's idle semantics plus the envelope cap. An\n * over-cap body is a hard failure — the throw surfaces through each caller's\n * existing failure path (502 proxy_error on submit/poll, models synthesis\n * fallback) without retaining anything oversized in memory.\n */\nexport async function readEnvelopeText(\n res: Response,\n record: RecordConfig | undefined,\n): Promise<string> {\n const read = await readBodyIdle(res, record, VIDEO_PROXY_ENVELOPE_BODY_CAP);\n if (read.overCap) {\n throw new Error(\n `Upstream envelope body exceeded ${VIDEO_PROXY_ENVELOPE_BODY_CAP} bytes (read aborted at ${read.bytesRead})`,\n );\n }\n return read.buf.toString(\"utf8\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,oBAAoB,QAA2D;CAC7F,MAAM,MAAM,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK,IAAI,GAAG;AACvD,KAAI,QAAQ,OAAW,QAAO;AAC9B,MAAK,MAAM,WAAW,IAAI,MAAM,IAAI,EAAE;EACpC,MAAM,UAAU,QAAQ,MAAM;AAC9B,MAAI,QAAS,QAAO;;;AAWxB,MAAM,oBAAoB;AAG1B,MAAM,yBAAyB;AAE/B,SAAgB,YAAY,KAA2B,QAAwB;CAI7E,MAAM,YAAY,oBAAoB,IAAI,QAAQ,qBAAqB,EAAE,aAAa;CAEtF,MAAM,QAAQ,cAAc,UAAU,cAAc,UAAU,YAAY;CAI1E,MAAM,UAAU,oBAAoB,IAAI,QAAQ,oBAAoB;CAKpE,MAAM,UAAU,IAAI,QAAQ;CAC5B,IAAI,OACF,YAAY,WACX,kBAAkB,KAAK,QAAQ,IAAI,uBAAuB,KAAK,QAAQ,IACpE,UACA;AACN,KAAI,YAAY,OACd,KAAI,kBAAkB,KAAK,QAAQ,IAAI,uBAAuB,KAAK,QAAQ,CACzE,QAAO;KAEP,QAAO,KACL,iEAAiE,KAAK,UAAU,QAAQ,MAAM,GAAG,IAAI,CAAC,GACvG;AAGL,QAAO,GAAG,MAAM,KAAK;;;;;;;;;;AAWvB,SAAgB,aAAa,QAAgB,KAAwB;AACnE,QAAO,WAAW,kBAAkB,KAAK,GAAG,IAAI,SAAS,mBAAmB,OAAO;;;AAIrF,MAAa,8BAA8B;;;;;;;;;;;;;;AAe3C,SAAgB,sBAAsB,QAA+C;AACnF,QAAO,YAAY,QAAQ,aAAa,QAAQ,mBAAmB,4BAA4B,CAAC;;;;;;;;;;;;AAalG,eAAsB,aACpB,KACA,QACA,MAAM,GAC2E;CACjF,MAAM,OAAO,IAAI;AACjB,KAAI,CAAC,KAAM,QAAO;EAAE,SAAS;EAAO,KAAK,OAAO,MAAM,EAAE;EAAE;CAC1D,MAAM,SAAS,aAAa,QAAQ,eAAe,4BAA4B;CAC/E,MAAM,SAAS,KAAK,WAAW;CAC/B,MAAM,SAAmB,EAAE;CAC3B,IAAI,QAAQ;AACZ,KAAI;AACF,WAAS;GACP,IAAI;GAQJ,MAAM,cAAc,OAAO,MAAM;AACjC,eAAY,YAAY,GAAG;GAC3B,MAAM,SAAS,MAAM,QAAQ,KAAK,CAChC,aACA,IAAI,SAAgB,GAAG,WAAW;AAChC,gBAAY,iBACJ,uBAAO,IAAI,MAAM,mCAAmC,OAAO,IAAI,CAAC,EACtE,OACD;KACD,CACH,CAAC,CAAC,cAAc,aAAa,UAAU,CAAC;AACzC,OAAI,OAAO,KAAM;AACjB,YAAS,OAAO,MAAM;AACtB,OAAI,MAAM,KAAK,QAAQ,IACrB,QAAO;IAAE,SAAS;IAAM,WAAW;IAAO;AAE5C,UAAO,KAAK,OAAO,KAAK,OAAO,MAAM,CAAC;;WAEhC;AAGR,EAAK,OAAO,QAAQ,CAAC,YAAY,GAAG;;AAEtC,QAAO;EAAE,SAAS;EAAO,KAAK,OAAO,OAAO,OAAO;EAAE;;;;;;;;;AAUvD,MAAa,gCAAgC,OAAO;;;;;;;;AASpD,eAAsB,iBACpB,KACA,QACiB;CACjB,MAAM,OAAO,MAAM,aAAa,KAAK,QAAQ,8BAA8B;AAC3E,KAAI,KAAK,QACP,OAAM,IAAI,MACR,mCAAmC,8BAA8B,0BAA0B,KAAK,UAAU,GAC3G;AAEH,QAAO,KAAK,IAAI,SAAS,OAAO"}
@@ -307,7 +307,100 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
307
307
  fixture
308
308
  }
309
309
  });
310
- const content = response.content;
310
+ if (response.blocks && response.blocks.length > 0) {
311
+ const resolvedBlocks = require_helpers.resolveFixtureBlocks(response.blocks);
312
+ const interruption = require_interruption.createInterruptionSignal(fixture);
313
+ const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
314
+ const { recordedTimings } = fixture;
315
+ let chunkIndex = 0;
316
+ let interrupted = false;
317
+ let historyContent = "";
318
+ const historyToolCalls = [];
319
+ outer: for (const block of resolvedBlocks) if (block.type === "toolCall") {
320
+ if (ws.isClosed) break;
321
+ const tcDelay = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
322
+ if (tcDelay > 0) await require_sse_writer.delay(tcDelay, interruption?.signal);
323
+ if (interruption?.signal.aborted) {
324
+ interrupted = true;
325
+ break;
326
+ }
327
+ if (ws.isClosed) break;
328
+ const resolvedId = block.id ?? require_helpers.generateToolCallId();
329
+ let argsObj;
330
+ try {
331
+ argsObj = JSON.parse(block.arguments || "{}");
332
+ } catch {
333
+ defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${block.name}": ${block.arguments}`);
334
+ argsObj = {};
335
+ }
336
+ try {
337
+ ws.send(JSON.stringify({ toolCall: { functionCalls: [{
338
+ name: block.name,
339
+ args: argsObj,
340
+ id: resolvedId
341
+ }] } }));
342
+ } catch (err) {
343
+ defaults.logger.debug("[gemini-live] send failed during blocks streaming, closing", err);
344
+ break;
345
+ }
346
+ chunkIndex++;
347
+ historyToolCalls.push({
348
+ id: resolvedId,
349
+ type: "function",
350
+ function: {
351
+ name: block.name,
352
+ arguments: block.arguments
353
+ }
354
+ });
355
+ interruption?.tick();
356
+ if (interruption?.signal.aborted) {
357
+ interrupted = true;
358
+ break;
359
+ }
360
+ } else {
361
+ const text = block.text;
362
+ historyContent += text;
363
+ if (text.length === 0) continue;
364
+ for (let i = 0; i < text.length; i += chunkSize) {
365
+ if (ws.isClosed) break outer;
366
+ const chunkDelay = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
367
+ if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
368
+ if (interruption?.signal.aborted) {
369
+ interrupted = true;
370
+ break outer;
371
+ }
372
+ if (ws.isClosed) break outer;
373
+ try {
374
+ ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: text.slice(i, i + chunkSize) }] } } }));
375
+ } catch (err) {
376
+ defaults.logger.debug("[gemini-live] send failed during blocks streaming, closing", err);
377
+ break outer;
378
+ }
379
+ chunkIndex++;
380
+ interruption?.tick();
381
+ if (interruption?.signal.aborted) {
382
+ interrupted = true;
383
+ break outer;
384
+ }
385
+ }
386
+ }
387
+ if (interrupted) {
388
+ ws.destroy();
389
+ journalEntry.response.interrupted = true;
390
+ journalEntry.response.interruptReason = interruption?.reason();
391
+ interruption?.cleanup();
392
+ return;
393
+ }
394
+ interruption?.cleanup();
395
+ if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
396
+ session.conversationHistory.push({
397
+ role: "assistant",
398
+ content: historyContent || null,
399
+ ...historyToolCalls.length > 0 ? { tool_calls: historyToolCalls } : {}
400
+ });
401
+ return;
402
+ }
403
+ const content = response.content ?? "";
311
404
  const chunkList = [];
312
405
  for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
313
406
  const interruption = require_interruption.createInterruptionSignal(fixture);
@@ -344,7 +437,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
344
437
  interruption?.cleanup();
345
438
  return;
346
439
  }
347
- const resolvedToolCalls = response.toolCalls.map((tc) => ({
440
+ const resolvedToolCalls = (response.toolCalls ?? []).map((tc) => ({
348
441
  ...tc,
349
442
  resolvedId: tc.id ?? require_helpers.generateToolCallId()
350
443
  }));
@@ -489,7 +582,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
489
582
  interruption?.cleanup();
490
583
  return;
491
584
  }
492
- const resolvedToolCalls = response.toolCalls.map((tc) => ({
585
+ const resolvedToolCalls = (response.toolCalls ?? []).map((tc) => ({
493
586
  ...tc,
494
587
  resolvedId: tc.id ?? require_helpers.generateToolCallId()
495
588
  }));