@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,829 @@
1
+ import { flattenHeaders, getContext, getTestId, isErrorResponse, isVideoResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
+ import { matchFixtureDiagnostic } from "./router.js";
3
+ import { writeErrorResponse } from "./sse-writer.js";
4
+ import { applyChaos } from "./chaos.js";
5
+ import { resolveUpstreamUrl } from "./url.js";
6
+ import { buildFixtureMatch, buildForwardHeaders, persistFixture, sanitizeHeaderValue } from "./recorder.js";
7
+ import { handleVideoStatus } from "./video.js";
8
+ import { resolveProgression } from "./fal.js";
9
+ import { readEnvelopeText, testIdSuffix, upstreamTimeoutSignal } from "./video-proxy-shared.js";
10
+ import crypto from "node:crypto";
11
+
12
+ //#region src/grok-video.ts
13
+ const DEFAULT_GROK_VIDEO_MODEL = "grok-imagine-video";
14
+ const GROK_USD_TICKS_PER_USD = 1e10;
15
+ const GROK_DEFAULT_VIDEO_URL = "https://cdn.x.ai/aimock-placeholder-video.mp4";
16
+ const GROK_VIDEO_MAX_ENTRIES = 1e4;
17
+ const GROK_VIDEO_TTL_MS = 36e5;
18
+ /**
19
+ * Per-testId job state for the Grok video handler. Mirrors OpenRouterVideoJobMap
20
+ * (openrouter-video.ts): lazy TTL eviction on `get`, FIFO eviction of the
21
+ * oldest entries on `set` when over capacity, delete-before-set TTL refresh,
22
+ * monotonic world-generation counter for reset-mid-flight detection, no
23
+ * background sweep timer. Keys are `${testId}:${requestId}`.
24
+ */
25
+ var GrokVideoJobMap = class {
26
+ entries = /* @__PURE__ */ new Map();
27
+ worldGeneration = 0;
28
+ get generation() {
29
+ return this.worldGeneration;
30
+ }
31
+ get(key) {
32
+ const entry = this.entries.get(key);
33
+ if (!entry) return void 0;
34
+ if (Date.now() - entry.createdAt > GROK_VIDEO_TTL_MS) {
35
+ this.entries.delete(key);
36
+ return;
37
+ }
38
+ return entry.job;
39
+ }
40
+ set(key, job) {
41
+ this.entries.delete(key);
42
+ this.entries.set(key, {
43
+ job,
44
+ createdAt: Date.now()
45
+ });
46
+ if (this.entries.size > GROK_VIDEO_MAX_ENTRIES) {
47
+ const excess = this.entries.size - GROK_VIDEO_MAX_ENTRIES;
48
+ const iter = this.entries.keys();
49
+ for (let i = 0; i < excess; i++) {
50
+ const next = iter.next();
51
+ if (!next.done) this.entries.delete(next.value);
52
+ }
53
+ }
54
+ }
55
+ delete(key) {
56
+ return this.entries.delete(key);
57
+ }
58
+ clear() {
59
+ this.entries.clear();
60
+ this.worldGeneration++;
61
+ }
62
+ get size() {
63
+ return this.entries.size;
64
+ }
65
+ };
66
+ /**
67
+ * Maps the fixture's terminal video status onto the job lifecycle. Anything
68
+ * that is not "failed" — including a fixture authored as "processing" — is
69
+ * treated as completed, since this surface always drives jobs to a terminal
70
+ * state.
71
+ */
72
+ function terminalStatus(job) {
73
+ return job.video.status === "failed" ? "failed" : "completed";
74
+ }
75
+ /**
76
+ * Mutates a job in place to advance its state on a status poll.
77
+ * `pending → in_progress → completed | failed` based on poll-count thresholds.
78
+ * No-op once terminal. The in_progress threshold is checked first so a job
79
+ * whose thresholds are equal still spends one poll in in_progress instead of
80
+ * jumping straight to the terminal status (fal advanceJob semantics).
81
+ */
82
+ function advanceJob(job) {
83
+ if (job.status === "completed" || job.status === "failed") return;
84
+ job.pollCount += 1;
85
+ if (job.status === "pending" && job.pollCount >= job.pollsBeforeInProgress) job.status = "in_progress";
86
+ else if (job.pollCount >= job.pollsBeforeCompleted) job.status = terminalStatus(job);
87
+ }
88
+ /**
89
+ * Synthesize the Grok wire `progress` (0..100) from poll-count progress toward
90
+ * the completed threshold. A terminal-completed job is always 100 (the only way
91
+ * to reach 100 — it is the wire signal for "done"). Every other state reports
92
+ * its progress-at-failure/at-poll, capped at 99. A seed-terminal job with a
93
+ * non-positive completion target has done zero polls toward completion, so it
94
+ * reports 0 (a failed seed job is 0%, never 100).
95
+ */
96
+ function grokProgress(job) {
97
+ if (job.status === "completed") return 100;
98
+ const target = job.pollsBeforeCompleted;
99
+ if (target <= 0) return 0;
100
+ const ratio = Math.min(1, job.pollCount / target);
101
+ return Math.min(99, Math.round(ratio * 100));
102
+ }
103
+ /** Map the internal job status onto the Grok wire `status` token. */
104
+ function grokWireStatus(status) {
105
+ if (status === "completed") return "done";
106
+ if (status === "failed") return "failed";
107
+ return "pending";
108
+ }
109
+ /**
110
+ * Synthesizes a structurally valid journal body for field-validation 400s.
111
+ * Mirrors openrouter-video's validationJournalBody: `model` stays a string and
112
+ * `messages` is an empty array; underscore-prefixed keys are stripped so a
113
+ * request cannot spoof handler-set discriminators.
114
+ */
115
+ function validationJournalBody(videoReq) {
116
+ const rawModel = videoReq.model;
117
+ const model = typeof rawModel === "string" ? rawModel : rawModel === void 0 ? "" : JSON.stringify(rawModel);
118
+ return {
119
+ ...Object.fromEntries(Object.entries(videoReq).filter(([key]) => !key.startsWith("_"))),
120
+ model,
121
+ messages: []
122
+ };
123
+ }
124
+ async function handleGrokVideoCreate(req, res, raw, fixtures, journal, defaults, setCorsHeaders, jobs) {
125
+ setCorsHeaders(res);
126
+ const path = req.url ?? "/v1/videos/generations";
127
+ const method = req.method ?? "POST";
128
+ if (((Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"]) ?? "").toLowerCase().includes("multipart/form-data")) {
129
+ journal.add({
130
+ method,
131
+ path,
132
+ headers: flattenHeaders(req.headers),
133
+ body: null,
134
+ response: {
135
+ status: 400,
136
+ fixture: null
137
+ }
138
+ });
139
+ writeErrorResponse(res, 400, JSON.stringify({
140
+ code: "invalid_request",
141
+ error: "multipart/form-data is not supported — the Grok video API is JSON-only"
142
+ }));
143
+ return;
144
+ }
145
+ let videoReq;
146
+ try {
147
+ videoReq = JSON.parse(raw);
148
+ } catch (parseErr) {
149
+ const detail = parseErr instanceof Error ? parseErr.message : "unknown";
150
+ journal.add({
151
+ method,
152
+ path,
153
+ headers: flattenHeaders(req.headers),
154
+ body: null,
155
+ response: {
156
+ status: 400,
157
+ fixture: null
158
+ }
159
+ });
160
+ writeErrorResponse(res, 400, JSON.stringify({
161
+ code: "invalid_json",
162
+ error: `Malformed JSON: ${detail}`
163
+ }));
164
+ return;
165
+ }
166
+ if (videoReq === null || typeof videoReq !== "object" || Array.isArray(videoReq)) {
167
+ journal.add({
168
+ method,
169
+ path,
170
+ headers: flattenHeaders(req.headers),
171
+ body: null,
172
+ response: {
173
+ status: 400,
174
+ fixture: null
175
+ }
176
+ });
177
+ writeErrorResponse(res, 400, JSON.stringify({
178
+ code: "invalid_request",
179
+ error: "Request body must be a JSON object"
180
+ }));
181
+ return;
182
+ }
183
+ const parsedBody = validationJournalBody(videoReq);
184
+ if (typeof videoReq.prompt !== "string" || !videoReq.prompt) {
185
+ journal.add({
186
+ method,
187
+ path,
188
+ headers: flattenHeaders(req.headers),
189
+ body: parsedBody,
190
+ response: {
191
+ status: 400,
192
+ fixture: null
193
+ }
194
+ });
195
+ const message = videoReq.prompt === void 0 ? "Missing required parameter: 'prompt'" : "Invalid type for parameter: 'prompt' must be a non-empty string";
196
+ writeErrorResponse(res, 400, JSON.stringify({
197
+ code: "invalid_request",
198
+ error: message
199
+ }));
200
+ return;
201
+ }
202
+ if (videoReq.model !== void 0 && (typeof videoReq.model !== "string" || !videoReq.model)) {
203
+ journal.add({
204
+ method,
205
+ path,
206
+ headers: flattenHeaders(req.headers),
207
+ body: parsedBody,
208
+ response: {
209
+ status: 400,
210
+ fixture: null
211
+ }
212
+ });
213
+ writeErrorResponse(res, 400, JSON.stringify({
214
+ code: "invalid_request",
215
+ error: "Invalid type for parameter: 'model' must be a non-empty string"
216
+ }));
217
+ return;
218
+ }
219
+ const syntheticReq = {
220
+ model: videoReq.model ?? DEFAULT_GROK_VIDEO_MODEL,
221
+ messages: [{
222
+ role: "user",
223
+ content: videoReq.prompt
224
+ }],
225
+ _endpointType: "video",
226
+ _videoProvider: "grok",
227
+ _context: getContext(req)
228
+ };
229
+ const testId = getTestId(req);
230
+ const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
231
+ if (fixture) {
232
+ journal.incrementFixtureMatchCount(fixture, fixtures, testId);
233
+ defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
234
+ } else {
235
+ const snippet = videoReq.prompt.slice(0, 80);
236
+ defaults.logger.debug(`No fixture matched for request (model=${syntheticReq.model}, msg="${snippet}")`);
237
+ }
238
+ if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
239
+ method,
240
+ path,
241
+ headers: flattenHeaders(req.headers),
242
+ body: syntheticReq
243
+ }, fixture ? "fixture" : resolveStrictMode(defaults.strict, req.headers) ? "internal" : defaults.record?.providers.grok ? "proxy" : "internal", defaults.registry, defaults.logger)) return;
244
+ if (!fixture) {
245
+ if (resolveStrictMode(defaults.strict, req.headers)) {
246
+ const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
247
+ defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
248
+ journal.add({
249
+ method,
250
+ path,
251
+ headers: flattenHeaders(req.headers),
252
+ body: syntheticReq,
253
+ response: {
254
+ status: 503,
255
+ fixture: null,
256
+ ...strictOverrideField(defaults.strict, req.headers)
257
+ }
258
+ });
259
+ writeErrorResponse(res, 503, JSON.stringify({
260
+ code: "no_fixture_match",
261
+ error: strictMessage
262
+ }));
263
+ return;
264
+ }
265
+ if (defaults.record) {
266
+ if (await proxyGrokVideoSubmit({
267
+ req,
268
+ res,
269
+ raw,
270
+ syntheticReq,
271
+ record: defaults.record,
272
+ journal,
273
+ defaults,
274
+ jobs,
275
+ method,
276
+ path
277
+ }) === "handled") return;
278
+ }
279
+ journal.add({
280
+ method,
281
+ path,
282
+ headers: flattenHeaders(req.headers),
283
+ body: syntheticReq,
284
+ response: {
285
+ status: 404,
286
+ fixture: null,
287
+ ...strictOverrideField(defaults.strict, req.headers)
288
+ }
289
+ });
290
+ writeErrorResponse(res, 404, JSON.stringify({
291
+ code: "not_found",
292
+ error: "No fixture matched"
293
+ }));
294
+ return;
295
+ }
296
+ const worldGeneration = jobs.generation;
297
+ const response = await resolveResponse(fixture, syntheticReq);
298
+ if (isErrorResponse(response)) {
299
+ if (res.destroyed || res.writableEnded) return;
300
+ const status = response.status ?? 500;
301
+ journal.add({
302
+ method,
303
+ path,
304
+ headers: flattenHeaders(req.headers),
305
+ body: syntheticReq,
306
+ response: {
307
+ status,
308
+ fixture
309
+ }
310
+ });
311
+ writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
312
+ return;
313
+ }
314
+ if (!isVideoResponse(response)) {
315
+ if (res.destroyed || res.writableEnded) return;
316
+ journal.add({
317
+ method,
318
+ path,
319
+ headers: flattenHeaders(req.headers),
320
+ body: syntheticReq,
321
+ response: {
322
+ status: 500,
323
+ fixture
324
+ }
325
+ });
326
+ writeErrorResponse(res, 500, JSON.stringify({ error: {
327
+ message: "Fixture response is not a video type",
328
+ type: "server_error"
329
+ } }));
330
+ return;
331
+ }
332
+ if (res.destroyed || res.writableEnded) return;
333
+ journal.add({
334
+ method,
335
+ path,
336
+ headers: flattenHeaders(req.headers),
337
+ body: syntheticReq,
338
+ response: {
339
+ status: 200,
340
+ fixture
341
+ }
342
+ });
343
+ const fixtureStatus = response.video.status;
344
+ if (fixtureStatus === "processing") defaults.logger.warn(`Video fixture has status "processing" — treated as completed for /v1/videos/generations jobs`);
345
+ else if (fixtureStatus !== "completed" && fixtureStatus !== "failed") defaults.logger.warn(`Video fixture has unknown status "${fixtureStatus}" — treating as completed for /v1/videos/generations jobs`);
346
+ const requestId = crypto.randomUUID();
347
+ const progression = resolveProgression(defaults.grokVideo);
348
+ const job = {
349
+ kind: "replay",
350
+ requestId,
351
+ status: "pending",
352
+ pollCount: 0,
353
+ pollsBeforeInProgress: progression.pollsBeforeInProgress,
354
+ pollsBeforeCompleted: progression.pollsBeforeCompleted,
355
+ video: { ...response.video }
356
+ };
357
+ if (progression.pollsBeforeCompleted === 0) job.status = terminalStatus(job);
358
+ if (jobs.generation === worldGeneration) jobs.set(`${testId}:${requestId}`, job);
359
+ else defaults.logger.warn(`Grok video submit resolved after a fixtures reset — not inserting job ${requestId} into the new world (its polls will 404)`);
360
+ res.writeHead(200, { "Content-Type": "application/json" });
361
+ res.end(JSON.stringify({ request_id: requestId + testIdSuffix(testId, "?") }));
362
+ }
363
+ async function handleGrokVideoStatus(req, res, id, fixtures, journal, defaults, setCorsHeaders, grokJobs, videoStates) {
364
+ const testId = getTestId(req);
365
+ const key = `${testId}:${id}`;
366
+ const job = grokJobs.get(key);
367
+ if (!job) {
368
+ handleVideoStatus(req, res, id, journal, defaults, setCorsHeaders, videoStates);
369
+ return;
370
+ }
371
+ setCorsHeaders(res);
372
+ const path = req.url ?? `/v1/videos/${id}`;
373
+ const method = req.method ?? "GET";
374
+ if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
375
+ method,
376
+ path,
377
+ headers: flattenHeaders(req.headers),
378
+ body: null
379
+ }, "internal", defaults.registry, defaults.logger)) return;
380
+ if (job.kind === "record") {
381
+ if (resolveStrictMode(defaults.strict, req.headers)) {
382
+ defaults.logger.error(`STRICT: video job ${id} is proxied live upstream (record mode) — refusing the upstream poll`);
383
+ journal.add({
384
+ method,
385
+ path,
386
+ headers: flattenHeaders(req.headers),
387
+ body: null,
388
+ response: {
389
+ status: 503,
390
+ fixture: null,
391
+ ...strictOverrideField(defaults.strict, req.headers)
392
+ }
393
+ });
394
+ writeErrorResponse(res, 503, JSON.stringify({
395
+ code: "no_fixture_match",
396
+ error: `Strict mode: video job ${id} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`
397
+ }));
398
+ return;
399
+ }
400
+ await proxyGrokVideoRecordPoll({
401
+ req,
402
+ res,
403
+ job,
404
+ key,
405
+ testId,
406
+ fixtures,
407
+ journal,
408
+ defaults,
409
+ jobs: grokJobs,
410
+ method,
411
+ path
412
+ });
413
+ return;
414
+ }
415
+ if (res.destroyed || res.writableEnded) return;
416
+ advanceJob(job);
417
+ grokJobs.set(key, job);
418
+ journal.add({
419
+ method,
420
+ path,
421
+ headers: flattenHeaders(req.headers),
422
+ body: null,
423
+ response: {
424
+ status: 200,
425
+ fixture: null
426
+ }
427
+ });
428
+ const body = serializeGrokReplay(job, defaults);
429
+ res.writeHead(200, { "Content-Type": "application/json" });
430
+ res.end(JSON.stringify(body));
431
+ }
432
+ /**
433
+ * Serialize a replay job into the Grok wire body. STORED `video.status` is the
434
+ * 3-value VideoResponse union; the wire `status` is derived here
435
+ * (completed → "done"). cost_in_usd_ticks is reconstructed from the stored USD
436
+ * `video.cost` (round(cost * 1e10)).
437
+ */
438
+ function serializeGrokReplay(job, defaults) {
439
+ const body = {
440
+ request_id: job.requestId,
441
+ status: grokWireStatus(job.status),
442
+ progress: grokProgress(job)
443
+ };
444
+ if (job.status === "completed") {
445
+ body.video = {
446
+ url: job.video.url || GROK_DEFAULT_VIDEO_URL,
447
+ duration: job.video.duration ?? 0
448
+ };
449
+ body.usage = { cost_in_usd_ticks: Math.round((job.video.cost ?? 0) * GROK_USD_TICKS_PER_USD) };
450
+ } else if (job.status === "failed") {
451
+ if (job.video.error === "" && !job.emptyErrorWarned) {
452
+ job.emptyErrorWarned = true;
453
+ defaults.logger.warn(`Video fixture for job ${job.requestId} has an empty error message — using the default`);
454
+ }
455
+ body.code = "generation_failed";
456
+ body.error = job.video.error || "Video generation failed";
457
+ }
458
+ return body;
459
+ }
460
+ const GROK_VIDEO_SUBMIT_PATH = "/v1/videos/generations";
461
+ /**
462
+ * Proxy an unmatched Grok video submit to the configured upstream and answer
463
+ * the client with a mock-rewritten `{ request_id }`. The upstream lifecycle is
464
+ * then driven interactively by the client's own polls. Returns "no_upstream"
465
+ * when record mode has no grok provider URL — the caller falls through to 404.
466
+ */
467
+ async function proxyGrokVideoSubmit(args) {
468
+ const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;
469
+ const upstreamBase = record.providers.grok;
470
+ if (!upstreamBase) {
471
+ defaults.logger.warn(`No upstream URL configured for provider "grok" — cannot proxy`);
472
+ return "no_upstream";
473
+ }
474
+ const proxyError = (msg) => {
475
+ defaults.logger.error(`Grok video submit proxy failed: ${msg}`);
476
+ if (res.destroyed || res.writableEnded) return "handled";
477
+ journal.add({
478
+ method,
479
+ path,
480
+ headers: flattenHeaders(req.headers),
481
+ body: syntheticReq,
482
+ response: {
483
+ status: 502,
484
+ fixture: null,
485
+ source: "proxy",
486
+ ...strictOverrideField(defaults.strict, req.headers)
487
+ }
488
+ });
489
+ writeErrorResponse(res, 502, JSON.stringify({
490
+ code: "proxy_error",
491
+ error: `Proxy to upstream failed: ${msg}`
492
+ }));
493
+ return "handled";
494
+ };
495
+ let submitUrl;
496
+ try {
497
+ submitUrl = resolveUpstreamUrl(upstreamBase, GROK_VIDEO_SUBMIT_PATH);
498
+ new URL(upstreamBase).origin;
499
+ } catch (err) {
500
+ return proxyError(`Invalid upstream URL: ${upstreamBase} (${err instanceof Error ? err.message : String(err)})`);
501
+ }
502
+ defaults.logger.warn(`NO FIXTURE MATCH — proxying video submit to ${upstreamBase}${GROK_VIDEO_SUBMIT_PATH}`);
503
+ const worldGeneration = jobs.generation;
504
+ let fetched;
505
+ try {
506
+ const upstreamRes = await fetch(submitUrl, {
507
+ method: "POST",
508
+ headers: buildForwardHeaders(req),
509
+ body: raw,
510
+ signal: upstreamTimeoutSignal(record)
511
+ });
512
+ fetched = {
513
+ status: upstreamRes.status,
514
+ contentType: upstreamRes.headers.get("content-type"),
515
+ text: await readEnvelopeText(upstreamRes, record)
516
+ };
517
+ } catch (err) {
518
+ return proxyError(err instanceof Error ? err.message : "Unknown proxy error");
519
+ }
520
+ if (fetched.status === 401 || fetched.status === 403) {
521
+ defaults.logger.warn(`Upstream rejected the video submit (${fetched.status}) — relaying the upstream status`);
522
+ if (res.destroyed || res.writableEnded) return "handled";
523
+ journal.add({
524
+ method,
525
+ path,
526
+ headers: flattenHeaders(req.headers),
527
+ body: syntheticReq,
528
+ response: {
529
+ status: fetched.status,
530
+ fixture: null,
531
+ source: "proxy",
532
+ ...strictOverrideField(defaults.strict, req.headers)
533
+ }
534
+ });
535
+ res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
536
+ res.end(fetched.text);
537
+ return "handled";
538
+ }
539
+ let upstreamRequestId;
540
+ {
541
+ if (fetched.status < 200 || fetched.status >= 300) return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);
542
+ let parsed;
543
+ try {
544
+ parsed = JSON.parse(fetched.text);
545
+ } catch {
546
+ return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);
547
+ }
548
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return proxyError("Submit response is not a JSON object");
549
+ const envelope = parsed;
550
+ upstreamRequestId = String(envelope.request_id ?? "").trim();
551
+ if (!upstreamRequestId) return proxyError("Submit response missing request_id");
552
+ }
553
+ const upstreamPollingUrl = resolveUpstreamUrl(upstreamBase, `/v1/videos/${encodeURIComponent(upstreamRequestId)}`).toString();
554
+ const testId = getTestId(req);
555
+ const matchRequest = defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq;
556
+ const requestId = crypto.randomUUID();
557
+ const job = {
558
+ kind: "record",
559
+ requestId,
560
+ status: "pending",
561
+ upstreamRequestId,
562
+ upstreamPollingUrl,
563
+ match: buildFixtureMatch(matchRequest, record)
564
+ };
565
+ if (jobs.generation === worldGeneration) jobs.set(`${testId}:${requestId}`, job);
566
+ else defaults.logger.warn(`Grok video submit for upstream request ${upstreamRequestId} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`);
567
+ if (res.destroyed || res.writableEnded) return "handled";
568
+ journal.add({
569
+ method,
570
+ path,
571
+ headers: flattenHeaders(req.headers),
572
+ body: syntheticReq,
573
+ response: {
574
+ status: 200,
575
+ fixture: null,
576
+ source: "proxy",
577
+ ...strictOverrideField(defaults.strict, req.headers)
578
+ }
579
+ });
580
+ res.writeHead(200, { "Content-Type": "application/json" });
581
+ res.end(JSON.stringify({ request_id: requestId + testIdSuffix(testId, "?") }));
582
+ return "handled";
583
+ }
584
+ /**
585
+ * Proxy a status poll for a record-mode job 1:1 to the upstream and relay the
586
+ * result with the mock requestId substituted. On `done` the rewritten body is
587
+ * relayed IMMEDIATELY and an eager capture runs (synchronously here — there is
588
+ * NO byte download, only the url/duration/cost from the terminal poll body —
589
+ * but structured to mirror openrouter-video's terminal path). `failed`
590
+ * persists a failed fixture synchronously. `expired` (and any unrepresentable
591
+ * status) pass through with a warn and keep proxying. Every post-await map
592
+ * mutation is identity-guarded. Each successful proxied poll refreshes the TTL.
593
+ */
594
+ async function proxyGrokVideoRecordPoll(args) {
595
+ const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;
596
+ const logger = defaults.logger;
597
+ const journalProxy = (status) => {
598
+ journal.add({
599
+ method,
600
+ path,
601
+ headers: flattenHeaders(req.headers),
602
+ body: null,
603
+ response: {
604
+ status,
605
+ fixture: null,
606
+ source: "proxy",
607
+ ...strictOverrideField(defaults.strict, req.headers)
608
+ }
609
+ });
610
+ };
611
+ const proxyError = (msg) => {
612
+ logger.error(`Grok video poll proxy failed: ${msg}`);
613
+ if (res.destroyed || res.writableEnded) return;
614
+ journalProxy(502);
615
+ writeErrorResponse(res, 502, JSON.stringify({
616
+ code: "proxy_error",
617
+ error: `Proxy to upstream failed: ${msg}`
618
+ }));
619
+ };
620
+ const record = defaults.record;
621
+ if (!record) {
622
+ proxyError("record mode is no longer configured for an in-flight record job");
623
+ return;
624
+ }
625
+ let fetched;
626
+ try {
627
+ const upstreamRes = await fetch(job.upstreamPollingUrl, {
628
+ headers: buildForwardHeaders(req),
629
+ signal: upstreamTimeoutSignal(record)
630
+ });
631
+ fetched = {
632
+ status: upstreamRes.status,
633
+ contentType: upstreamRes.headers.get("content-type"),
634
+ text: await readEnvelopeText(upstreamRes, record)
635
+ };
636
+ } catch (err) {
637
+ proxyError(err instanceof Error ? err.message : "Unknown proxy error");
638
+ return;
639
+ }
640
+ if (fetched.status === 401 || fetched.status === 403) {
641
+ logger.warn(`Upstream rejected the status poll for job ${job.upstreamRequestId} (${fetched.status}) — relaying the upstream status`);
642
+ if (res.destroyed || res.writableEnded) return;
643
+ journalProxy(fetched.status);
644
+ res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
645
+ res.end(fetched.text);
646
+ return;
647
+ }
648
+ let upstreamBody;
649
+ {
650
+ if (fetched.status < 200 || fetched.status >= 300) {
651
+ proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);
652
+ return;
653
+ }
654
+ let parsed;
655
+ try {
656
+ parsed = JSON.parse(fetched.text);
657
+ } catch {
658
+ proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);
659
+ return;
660
+ }
661
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
662
+ proxyError("Status response is not a JSON object");
663
+ return;
664
+ }
665
+ upstreamBody = parsed;
666
+ }
667
+ const relayBody = {
668
+ ...upstreamBody,
669
+ request_id: job.requestId
670
+ };
671
+ const relayJson = (body) => {
672
+ if (res.destroyed || res.writableEnded) return;
673
+ journalProxy(200);
674
+ res.writeHead(200, { "Content-Type": "application/json" });
675
+ res.end(JSON.stringify(body));
676
+ };
677
+ const upstreamStatus = String(upstreamBody.status ?? "");
678
+ if (upstreamStatus === "pending" || upstreamStatus === "in_progress") {
679
+ if (jobs.get(key) === job) {
680
+ if (!job.capturing && job.status !== "completed" && job.status !== "failed") job.status = upstreamStatus === "in_progress" ? "in_progress" : "pending";
681
+ jobs.set(key, job);
682
+ }
683
+ relayJson(relayBody);
684
+ return;
685
+ }
686
+ if (upstreamStatus === "done") {
687
+ if (record.proxyOnly) {
688
+ if (jobs.get(key) === job) {
689
+ job.status = "completed";
690
+ jobs.set(key, job);
691
+ }
692
+ relayJson(relayBody);
693
+ return;
694
+ }
695
+ if (job.capturing || jobs.get(key) !== job) {
696
+ if (jobs.get(key) === job) jobs.set(key, job);
697
+ relayJson(relayBody);
698
+ return;
699
+ }
700
+ job.capturing = true;
701
+ job.status = "completed";
702
+ jobs.set(key, job);
703
+ relayJson(relayBody);
704
+ captureGrokVideoRecordFixture({
705
+ job,
706
+ key,
707
+ testId,
708
+ fixtures,
709
+ defaults,
710
+ jobs,
711
+ record,
712
+ upstreamBody
713
+ });
714
+ return;
715
+ }
716
+ if (upstreamStatus === "failed") {
717
+ if (record.proxyOnly) {
718
+ if (jobs.get(key) === job) {
719
+ job.status = "failed";
720
+ jobs.set(key, job);
721
+ }
722
+ relayJson(relayBody);
723
+ return;
724
+ }
725
+ if (jobs.get(key) !== job) {
726
+ relayJson(relayBody);
727
+ return;
728
+ }
729
+ const rawError = upstreamBody.error;
730
+ let error;
731
+ if (typeof rawError === "string" && rawError) error = rawError;
732
+ else if (rawError !== null && typeof rawError === "object" && !Array.isArray(rawError)) {
733
+ const message = rawError.message;
734
+ if (typeof message === "string" && message) error = message;
735
+ }
736
+ if (error === void 0 && rawError !== void 0 && rawError !== null) logger.warn(`Upstream video job ${job.upstreamRequestId} failed with an unusable error value (${JSON.stringify(rawError).slice(0, 100)}) — recording the fixture without an error message (replay serves the default)`);
737
+ const video = {
738
+ id: job.upstreamRequestId,
739
+ status: "failed",
740
+ ...error !== void 0 ? { error } : {}
741
+ };
742
+ const persistResult = persistFixture({
743
+ record,
744
+ providerKey: "grok",
745
+ testId,
746
+ fixture: {
747
+ match: job.match,
748
+ response: { video }
749
+ },
750
+ fixtures,
751
+ logger
752
+ });
753
+ if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", sanitizeHeaderValue(persistResult.error));
754
+ jobs.set(key, {
755
+ kind: "replay",
756
+ requestId: job.requestId,
757
+ status: "failed",
758
+ pollCount: 0,
759
+ pollsBeforeInProgress: 0,
760
+ pollsBeforeCompleted: 0,
761
+ video: { ...video }
762
+ });
763
+ relayJson(relayBody);
764
+ return;
765
+ }
766
+ logger.warn(`Upstream video job ${job.upstreamRequestId} reported status "${upstreamStatus}" — not representable in fixture video.status; passing through without recording`);
767
+ if (jobs.get(key) === job) jobs.set(key, job);
768
+ relayJson(relayBody);
769
+ }
770
+ /**
771
+ * Capture a completed Grok record job into a fixture (PERSIST only — NO byte
772
+ * download): builds `video` from the terminal poll body (id = upstream
773
+ * request_id, status = stored "completed", url/duration/cost), persists it, and
774
+ * mutates the map entry into a terminal replay job. World-generation guard
775
+ * before persist (a fixtures reset mid-flight invalidates the world).
776
+ */
777
+ function captureGrokVideoRecordFixture(args) {
778
+ const { job, key, testId, fixtures, defaults, jobs, record, upstreamBody } = args;
779
+ const logger = defaults.logger;
780
+ try {
781
+ const upstreamVideo = upstreamBody.video !== null && typeof upstreamBody.video === "object" && !Array.isArray(upstreamBody.video) ? upstreamBody.video : void 0;
782
+ const rawUrl = upstreamVideo?.url;
783
+ const url = typeof rawUrl === "string" && rawUrl ? rawUrl : GROK_DEFAULT_VIDEO_URL;
784
+ const rawDuration = upstreamVideo?.duration;
785
+ const duration = typeof rawDuration === "number" ? rawDuration : void 0;
786
+ const rawTicks = (upstreamBody.usage !== null && typeof upstreamBody.usage === "object" && !Array.isArray(upstreamBody.usage) ? upstreamBody.usage : void 0)?.cost_in_usd_ticks;
787
+ const cost = typeof rawTicks === "number" ? rawTicks / GROK_USD_TICKS_PER_USD : void 0;
788
+ const video = {
789
+ id: job.upstreamRequestId,
790
+ status: "completed",
791
+ url,
792
+ ...duration !== void 0 ? { duration } : {},
793
+ ...cost !== void 0 ? { cost } : {}
794
+ };
795
+ if (jobs.get(key) !== job) {
796
+ logger.warn(`Grok video capture for job ${job.upstreamRequestId} discarded: the job map no longer holds this job (fixtures reset or TTL eviction) — nothing persisted`);
797
+ return;
798
+ }
799
+ persistFixture({
800
+ record,
801
+ providerKey: "grok",
802
+ testId,
803
+ fixture: {
804
+ match: job.match,
805
+ response: { video }
806
+ },
807
+ fixtures,
808
+ logger
809
+ });
810
+ if (jobs.get(key) === job) jobs.set(key, {
811
+ kind: "replay",
812
+ requestId: job.requestId,
813
+ status: "completed",
814
+ pollCount: 0,
815
+ pollsBeforeInProgress: 0,
816
+ pollsBeforeCompleted: 0,
817
+ video: { ...video }
818
+ });
819
+ } catch (err) {
820
+ const msg = err instanceof Error ? err.message : String(err);
821
+ logger.error(`Grok video capture for job ${job.upstreamRequestId} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`);
822
+ } finally {
823
+ job.capturing = false;
824
+ }
825
+ }
826
+
827
+ //#endregion
828
+ export { GROK_VIDEO_MAX_ENTRIES, GrokVideoJobMap, handleGrokVideoCreate, handleGrokVideoStatus };
829
+ //# sourceMappingURL=grok-video.js.map