@copilotkit/aimock 1.39.0 → 1.41.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 (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +4 -2
  5. package/dist/agui-stub.d.cts +2 -2
  6. package/dist/agui-stub.d.ts +2 -2
  7. package/dist/agui-types.d.cts +66 -3
  8. package/dist/agui-types.d.cts.map +1 -1
  9. package/dist/agui-types.d.ts +66 -3
  10. package/dist/agui-types.d.ts.map +1 -1
  11. package/dist/byteplus-video.cjs +954 -0
  12. package/dist/byteplus-video.cjs.map +1 -0
  13. package/dist/byteplus-video.d.cts +176 -0
  14. package/dist/byteplus-video.d.cts.map +1 -0
  15. package/dist/byteplus-video.d.ts +176 -0
  16. package/dist/byteplus-video.d.ts.map +1 -0
  17. package/dist/byteplus-video.js +947 -0
  18. package/dist/byteplus-video.js.map +1 -0
  19. package/dist/cli.cjs +5 -1
  20. package/dist/cli.cjs.map +1 -1
  21. package/dist/cli.js +5 -1
  22. package/dist/cli.js.map +1 -1
  23. package/dist/helpers.cjs +8 -4
  24. package/dist/helpers.cjs.map +1 -1
  25. package/dist/helpers.d.cts.map +1 -1
  26. package/dist/helpers.d.ts.map +1 -1
  27. package/dist/helpers.js +8 -4
  28. package/dist/helpers.js.map +1 -1
  29. package/dist/images.cjs +2 -2
  30. package/dist/images.cjs.map +1 -1
  31. package/dist/images.d.cts +11 -3
  32. package/dist/images.d.cts.map +1 -1
  33. package/dist/images.d.ts +11 -3
  34. package/dist/images.d.ts.map +1 -1
  35. package/dist/images.js +2 -2
  36. package/dist/images.js.map +1 -1
  37. package/dist/index.cjs +7 -0
  38. package/dist/index.d.cts +3 -2
  39. package/dist/index.d.ts +3 -2
  40. package/dist/index.js +2 -1
  41. package/dist/metrics.cjs +20 -0
  42. package/dist/metrics.cjs.map +1 -1
  43. package/dist/metrics.d.cts.map +1 -1
  44. package/dist/metrics.d.ts.map +1 -1
  45. package/dist/metrics.js +19 -1
  46. package/dist/metrics.js.map +1 -1
  47. package/dist/openrouter-chat.cjs +41 -0
  48. package/dist/openrouter-chat.cjs.map +1 -1
  49. package/dist/openrouter-chat.js +41 -0
  50. package/dist/openrouter-chat.js.map +1 -1
  51. package/dist/provider-auth.cjs +2 -0
  52. package/dist/provider-auth.cjs.map +1 -1
  53. package/dist/provider-auth.js +2 -0
  54. package/dist/provider-auth.js.map +1 -1
  55. package/dist/recorder.cjs +77 -8
  56. package/dist/recorder.cjs.map +1 -1
  57. package/dist/recorder.d.cts.map +1 -1
  58. package/dist/recorder.d.ts.map +1 -1
  59. package/dist/recorder.js +77 -8
  60. package/dist/recorder.js.map +1 -1
  61. package/dist/router.cjs +1 -0
  62. package/dist/router.cjs.map +1 -1
  63. package/dist/router.d.cts.map +1 -1
  64. package/dist/router.d.ts.map +1 -1
  65. package/dist/router.js +1 -0
  66. package/dist/router.js.map +1 -1
  67. package/dist/server.cjs +137 -6
  68. package/dist/server.cjs.map +1 -1
  69. package/dist/server.d.cts +2 -0
  70. package/dist/server.d.cts.map +1 -1
  71. package/dist/server.d.ts +2 -0
  72. package/dist/server.d.ts.map +1 -1
  73. package/dist/server.js +138 -7
  74. package/dist/server.js.map +1 -1
  75. package/dist/stream-collapse.cjs +21 -6
  76. package/dist/stream-collapse.cjs.map +1 -1
  77. package/dist/stream-collapse.d.cts +16 -0
  78. package/dist/stream-collapse.d.cts.map +1 -1
  79. package/dist/stream-collapse.d.ts +16 -0
  80. package/dist/stream-collapse.d.ts.map +1 -1
  81. package/dist/stream-collapse.js +21 -6
  82. package/dist/stream-collapse.js.map +1 -1
  83. package/dist/types.d.cts +41 -6
  84. package/dist/types.d.cts.map +1 -1
  85. package/dist/types.d.ts +41 -6
  86. package/dist/types.d.ts.map +1 -1
  87. package/dist/vector-types.d.cts.map +1 -1
  88. package/package.json +4 -3
@@ -0,0 +1,954 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_helpers = require('./helpers.cjs');
3
+ const require_router = require('./router.cjs');
4
+ const require_sse_writer = require('./sse-writer.cjs');
5
+ const require_chaos = require('./chaos.cjs');
6
+ const require_url = require('./url.cjs');
7
+ const require_recorder = require('./recorder.cjs');
8
+ const require_fal = require('./fal.cjs');
9
+ const require_video_proxy_shared = require('./video-proxy-shared.cjs');
10
+ let node_crypto = require("node:crypto");
11
+ node_crypto = require_runtime.__toESM(node_crypto);
12
+
13
+ //#region src/byteplus-video.ts
14
+ /**
15
+ * BytePlus Ark (ModelArk / Seedance) async video task lifecycle mock. Submit
16
+ * `POST /api/v3/contents/generations/tasks` returns `{ id }`; status
17
+ * `GET /api/v3/contents/generations/tasks/{id}` polls
18
+ * `queued → running → <recorded terminal status>`.
19
+ *
20
+ * THE FIXTURE FORMAT IS THE ARK TASK ENVELOPE ITSELF, stored as a
21
+ * `RawJSONResponse` (`{ response: { json: … } }`) — NOT a `VideoResponse`. That
22
+ * is the central decision of this surface and it is what lets `cancelled` and
23
+ * `expired` (terminal states with no representation in `VideoResponse.status`)
24
+ * record and replay natively, lets a recorded `error` object replay without
25
+ * inventing a code, and lets `usage`, `framespersecond`, `draft_task_id` and any
26
+ * field the vendor adds later survive because they were recorded rather than
27
+ * enumerated.
28
+ *
29
+ * PROVENANCE OF EVERY WIRE FACT IN THIS FILE. One source for the TASK shapes —
30
+ * create, poll, the `succeeded`/`failed` envelopes, the status tokens, the
31
+ * unknown-task 404: the client library `@tanstack/ai-byteplus@0.3.4` — its
32
+ * `wire-types.ts`, its request builders, and its author's note of live calls
33
+ * made on 2026-07-31. That is a SECONDARY source. BytePlus's own documentation
34
+ * is a client-rendered SPA and was never read. Exactly one layer is firsthand:
35
+ * the keyless canary in `src/__tests__/drift/byteplus-video.drift.ts` does call
36
+ * live Ark, and observed its AUTH-layer 401 error envelope on 2026-09-10 (see
37
+ * the `OBSERVED_*` constants there). No SUCCESS or resource-layer response from
38
+ * Ark has ever been observed here — the authenticated unknown-task canary is
39
+ * still `skipIf`-gated for want of an `ARK_API_KEY` in repo secrets. So "the
40
+ * client library's six status tokens" is a claim this repo can back, and "the
41
+ * vendor's documented six" is not — comments below say the former. Where a fact is not even secondhand, it is named as unverified
42
+ * rather than softened. A verification nobody performed is worse than an
43
+ * admitted gap: it stops the next person from looking.
44
+ *
45
+ * The governing rule: THE MOCK NEVER AUTHORS A WIRE VALUE IT DID NOT OBSERVE.
46
+ * On replay exactly three things are synthesized — the `id` rewrite, the
47
+ * `queued`/`running` token on a NON-terminal poll, and withholding
48
+ * `content`/`error`/`usage` until terminal. Nothing else. There is no default
49
+ * error code, no default error message, no fabricated `usage`, and no
50
+ * placeholder video url: a terminal fixture missing `content.video_url` gets a
51
+ * named warn, not a substituted URL, because the consumer's error naming the
52
+ * real problem beats a download of a URL aimock made up.
53
+ *
54
+ * `video.url` is served AS-IS — aimock does NOT proxy or capture video bytes.
55
+ * Ark's `content.video_url` expires 24h after `updated_at`; replay warns once
56
+ * when a fixture is served past that instant and still serves it.
57
+ *
58
+ * A fixture for this surface MUST carry `match.endpoint: "video"`. Without it
59
+ * `router.ts`'s endpoint/response compatibility gate skips it (a `{ json }`
60
+ * response is not a `VideoResponse`) before this handler ever sees it.
61
+ */
62
+ /**
63
+ * Ark's data plane lives under an `/api/v3` prefix ON THE ORIGIN, so
64
+ * `record.providers.byteplus` is configured as an ORIGIN ONLY
65
+ * (`https://ark.ap-southeast.bytepluses.com`) and this handler owns the whole
66
+ * path — the same split `openrouter-video.ts` uses with `OPENROUTER_VIDEOS_PATH`.
67
+ */
68
+ const BYTEPLUS_VIDEO_TASKS_PATH = "/api/v3/contents/generations/tasks";
69
+ /**
70
+ * The two NON-terminal task states, per `@tanstack/ai-byteplus@0.3.4`'s
71
+ * `wire-types.ts:38-44` — the client library's status union, not a BytePlus
72
+ * document. This is the primitive: "terminal" is derived as everything else,
73
+ * so a status token the client library does not list (whether BytePlus added
74
+ * it or the library simply never had it) records instead of proxying forever.
75
+ * See `isBytePlusTerminalStatus`.
76
+ */
77
+ const BYTEPLUS_NON_TERMINAL_STATUSES = new Set(["queued", "running"]);
78
+ /**
79
+ * The six task states the client library declares. Anything outside this set is
80
+ * a surprise — an authoring error, or a token the library does not cover.
81
+ */
82
+ const BYTEPLUS_TASK_STATUSES = new Set([
83
+ ...BYTEPLUS_NON_TERMINAL_STATUSES,
84
+ "succeeded",
85
+ "failed",
86
+ "cancelled",
87
+ "expired"
88
+ ]);
89
+ /**
90
+ * Terminal is the COMPLEMENT of the two non-terminal tokens, never a second
91
+ * hardcoded list of the four terminal ones. An unrecognized status is treated
92
+ * as terminal deliberately: a status absent from the client library's union is
93
+ * far more likely to be an end state than an intermediate one, and guessing wrong
94
+ * in that direction costs a captured fixture the operator can inspect, while
95
+ * guessing wrong the other way costs a record run that polls until the client
96
+ * times out and writes nothing.
97
+ */
98
+ function isBytePlusTerminalStatus(status) {
99
+ return !BYTEPLUS_NON_TERMINAL_STATUSES.has(status);
100
+ }
101
+ /** Ark output URLs expire 24h after the task produced them, anchored on `updated_at`. */
102
+ const BYTEPLUS_URL_TTL_SECONDS = 1440 * 60;
103
+ const BYTEPLUS_VIDEO_MAX_ENTRIES = 1e4;
104
+ const BYTEPLUS_VIDEO_TTL_MS = 36e5;
105
+ /**
106
+ * Warn latches keyed on the ENVELOPE OBJECT, not the job. A suite that replays
107
+ * one aged fixture across a hundred tests mints a hundred jobs off the SAME
108
+ * envelope object (the fixture's `response.json`), and a per-job latch would
109
+ * emit a hundred identical lines — noise, not signal. The envelope is the thing
110
+ * being complained about, so it owns the latch. WeakMap so a fixtures reset
111
+ * drops the latches with the fixtures.
112
+ */
113
+ const bytePlusWarnLatches = /* @__PURE__ */ new WeakMap();
114
+ function warnLatchesFor(envelope) {
115
+ const existing = bytePlusWarnLatches.get(envelope);
116
+ if (existing !== void 0) return existing;
117
+ const fresh = {};
118
+ bytePlusWarnLatches.set(envelope, fresh);
119
+ return fresh;
120
+ }
121
+ /**
122
+ * Per-testId job state. Mirrors GrokVideoJobMap / OpenRouterVideoJobMap: lazy
123
+ * TTL eviction on `get`, FIFO eviction of the oldest entries on `set` when over
124
+ * capacity, delete-before-set TTL refresh, monotonic world-generation counter
125
+ * for reset-mid-flight detection, no background sweep timer.
126
+ * Keys are `${testId}:${id}`.
127
+ */
128
+ var BytePlusVideoJobMap = class {
129
+ entries = /* @__PURE__ */ new Map();
130
+ worldGeneration = 0;
131
+ get generation() {
132
+ return this.worldGeneration;
133
+ }
134
+ get(key) {
135
+ const entry = this.entries.get(key);
136
+ if (!entry) return void 0;
137
+ if (Date.now() - entry.createdAt > BYTEPLUS_VIDEO_TTL_MS) {
138
+ this.entries.delete(key);
139
+ return;
140
+ }
141
+ return entry.job;
142
+ }
143
+ set(key, job) {
144
+ this.entries.delete(key);
145
+ this.entries.set(key, {
146
+ job,
147
+ createdAt: Date.now()
148
+ });
149
+ if (this.entries.size > BYTEPLUS_VIDEO_MAX_ENTRIES) {
150
+ const excess = this.entries.size - BYTEPLUS_VIDEO_MAX_ENTRIES;
151
+ const iter = this.entries.keys();
152
+ for (let i = 0; i < excess; i++) {
153
+ const next = iter.next();
154
+ if (!next.done) this.entries.delete(next.value);
155
+ }
156
+ }
157
+ }
158
+ delete(key) {
159
+ return this.entries.delete(key);
160
+ }
161
+ clear() {
162
+ this.entries.clear();
163
+ this.worldGeneration++;
164
+ }
165
+ get size() {
166
+ return this.entries.size;
167
+ }
168
+ };
169
+ /** The `{ url }` wrapper each media part carries, keyed by the part's `type`. */
170
+ function mediaUrlOf(part) {
171
+ const holder = part.type === "image_url" ? part.image_url : part.type === "video_url" ? part.video_url : part.type === "audio_url" ? part.audio_url : void 0;
172
+ if (holder === null || typeof holder !== "object") return void 0;
173
+ const url = holder.url;
174
+ return typeof url === "string" ? url : void 0;
175
+ }
176
+ function digest12(value) {
177
+ return node_crypto.default.createHash("sha256").update(value).digest("hex").slice(0, 12);
178
+ }
179
+ /** Stable-enough serialisation of a part the media table could not resolve. */
180
+ function unresolvedPartToken(raw) {
181
+ if (raw === null || typeof raw !== "object") return `part:${digest12(String(raw))}`;
182
+ const part = raw;
183
+ const role = typeof part.type === "string" && part.type ? part.type : "part";
184
+ let serialised;
185
+ try {
186
+ serialised = JSON.stringify(raw) ?? String(raw);
187
+ } catch {
188
+ serialised = String(raw);
189
+ }
190
+ return `${role}:${digest12(serialised)}`;
191
+ }
192
+ /**
193
+ * The synthetic user message a submit is matched on.
194
+ *
195
+ * Concatenated text of every `type: "text"` part, then ALWAYS
196
+ * `[media: <role>:<sha256 first 12 hex>, …]` — one entry per non-text part in
197
+ * array order, `[media: ]` when there are none. The separating newline is
198
+ * present only when there IS text: a key with no text part is the bare
199
+ * `[media: …]` suffix, unprefixed.
200
+ *
201
+ * WHY the digest: a Seedance image-to-video job carries NO text part, so a
202
+ * text-only key would record `{ endpoint, model }` — a wildcard matching every
203
+ * image-to-video job in the suite for that model. `persistFixture`'s
204
+ * defective-fixture guard would not catch it either, because its `isEmptyMatch`
205
+ * requires `endpoint` to be undefined too. Image-to-video, last-frame and
206
+ * reference-media are first-class Seedance modes, so two recordings with the
207
+ * same prompt and different first frames must stay distinguishable.
208
+ *
209
+ * WHY the marker is UNCONDITIONAL: this key lands in `match.userMessage`, which
210
+ * the router SUBSTRING-matches by default (`useExactMatch = !!requestTransform`).
211
+ * A conditional suffix makes the text-only key `"a guitar"` a prefix of the
212
+ * image-to-video key `"a guitar\n[media: …]"`, so a t2v recording silently
213
+ * serves its own video_url to an i2v request that shares the prompt — a 200
214
+ * with the wrong bytes. Emitting `[media: ]` for the text-only case makes
215
+ * neither key a substring of the other. Prefixing would NOT fix it: the plain
216
+ * text is still contained in the prefixed form.
217
+ *
218
+ * WHY every unrecognised part still contributes: `mediaUrlOf` resolves exactly
219
+ * `image_url` / `video_url` / `audio_url` with a `{ url: string }` wrapper, and
220
+ * the whole wire contract here is unverified against BytePlus. Dropping the
221
+ * parts it cannot resolve reopened both failures above — an all-unrecognised
222
+ * content array returned `""` and minted the very wildcard this key exists to
223
+ * prevent, and two different unrecognised parts collapsed onto one key. They
224
+ * are digested over their own JSON instead, so they stay distinguishable
225
+ * without the key ever carrying the part's payload.
226
+ *
227
+ * The digest is taken over the url string whether that is a public URL or a
228
+ * multi-megabyte `data:` URI, and only 12 hex are kept — the match key stays
229
+ * short and the fixture never grows a data URI.
230
+ *
231
+ * `role` is the declared role, else `first_frame` for an image (the default
232
+ * `@tanstack/ai-byteplus@0.3.4` applies when a request omits the role — not a
233
+ * BytePlus-documented default; nothing here was read from BytePlus), else the
234
+ * part's `type` token, which is read off the request rather than drawn from any
235
+ * role vocabulary.
236
+ */
237
+ function buildBytePlusMatchText(content) {
238
+ const texts = [];
239
+ const media = [];
240
+ for (const raw of content) {
241
+ const part = raw !== null && typeof raw === "object" ? raw : void 0;
242
+ if (part?.type === "text") {
243
+ if (typeof part.text === "string" && part.text) texts.push(part.text);
244
+ continue;
245
+ }
246
+ const url = part === void 0 ? void 0 : mediaUrlOf(part);
247
+ if (part === void 0 || url === void 0) {
248
+ media.push(unresolvedPartToken(raw));
249
+ continue;
250
+ }
251
+ const role = typeof part.role === "string" && part.role ? part.role : part.type === "image_url" ? "first_frame" : String(part.type);
252
+ media.push(`${role}:${digest12(url)}`);
253
+ }
254
+ const suffix = `[media: ${media.join(", ")}]`;
255
+ const text = texts.join("\n");
256
+ return text ? `${text}\n${suffix}` : suffix;
257
+ }
258
+ /**
259
+ * Advance a replay job one poll. `queued → running → terminal` on poll-count
260
+ * thresholds; no-op once terminal, which is what makes the extra post-terminal
261
+ * poll the client library makes (getVideoStatus then getVideoUrl both hit this
262
+ * endpoint) byte-identical to the one before it.
263
+ */
264
+ function advanceJob(job) {
265
+ if (job.phase === "terminal") return;
266
+ job.pollCount += 1;
267
+ if (job.phase === "queued" && job.pollCount >= job.pollsBeforeRunning) job.phase = "running";
268
+ if (job.pollCount >= job.pollsBeforeTerminal) job.phase = "terminal";
269
+ }
270
+ /**
271
+ * Ark's error envelope SHAPE, carrying a message only — never an invented code.
272
+ *
273
+ * KNOWN, DELIBERATE, UNRESOLVED DIVERGENCE. The live canary in
274
+ * `src/__tests__/drift/byteplus-video.drift.ts` asserts that real Ark supplies a
275
+ * string `error.code`, so a consumer that reads `error.code` off an aimock-
276
+ * authored error (404 unknown task, 404 no fixture, 400 validation, 502 bad
277
+ * envelope, 503 strict) sees `undefined` where live Ark would give it a token.
278
+ *
279
+ * We omit it rather than close the gap because we do not know the value FOR
280
+ * THESE CLASSES. The canary is keyless and does run — it observed Ark's
281
+ * AUTH-layer error body live on 2026-09-10 (`code: "AuthenticationError"`,
282
+ * `type: "Unauthorized"`, `param: ""`; see the `OBSERVED_*` constants in the
283
+ * drift file). None of the five error classes above is an auth error, so that
284
+ * observation does not supply their codes, and `@tanstack/ai-byteplus` is not
285
+ * installed here, so nobody has verified what the client does with a missing
286
+ * `code` either. Minting one would put an aimock-authored token into a consumer's
287
+ * assertions and into any fixture recorded past it, which is the failure this
288
+ * module exists to avoid; the governing rule is that the mock never authors a
289
+ * wire value it did not observe, and an honest absence beats a plausible
290
+ * invention. Recorded fixtures are unaffected: a real `error.code` captured
291
+ * from upstream replays verbatim.
292
+ *
293
+ * Also omitted, for the same reason and on the same terms: `type` and `param`.
294
+ * Both ARE observed on the auth envelope, and neither is known for the five
295
+ * classes above.
296
+ *
297
+ * To close it: observe a real Ark 404/400/5xx for each class (a real key is
298
+ * needed to reach them), then carry the OBSERVED code per error class. Do not
299
+ * guess one.
300
+ */
301
+ function arkErrorBody(message) {
302
+ return JSON.stringify({ error: { message } });
303
+ }
304
+ function asRecord(value) {
305
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
306
+ }
307
+ /**
308
+ * Build the poll body for a replay job.
309
+ *
310
+ * Non-terminal: `{ id, model?, status, created_at? }` — the status token is the
311
+ * ONLY synthesized value, and `content`/`error`/`usage`/`updated_at` are
312
+ * withheld. `created_at` is a property of the task and is true at every phase;
313
+ * `updated_at` on a recorded envelope is the timestamp of the TERMINAL
314
+ * transition, so emitting it mid-flight would assert a field COMBINATION this
315
+ * design has never seen from Ark and cannot source from the client library
316
+ * either (and a consumer diffing it across polls would see it constant).
317
+ *
318
+ * Terminal: the stored envelope VERBATIM with `id` rewritten. Its status is
319
+ * whatever was recorded, including `cancelled` / `expired`.
320
+ *
321
+ * Returns `null` when the envelope cannot produce a poll body at all (not an
322
+ * object, or no `status`) — an authoring error the caller answers with a 502.
323
+ */
324
+ function serializeBytePlusVideoTask(job, logger) {
325
+ const envelope = job.envelope;
326
+ const status = envelope.status;
327
+ if (typeof status !== "string" || !status) return null;
328
+ if (job.phase !== "terminal") {
329
+ const body = {
330
+ id: job.id,
331
+ status: job.phase
332
+ };
333
+ if (envelope.model !== void 0) body.model = envelope.model;
334
+ if (envelope.created_at !== void 0) body.created_at = envelope.created_at;
335
+ return body;
336
+ }
337
+ const warned = warnLatchesFor(envelope);
338
+ if (!isBytePlusTerminalStatus(status) && !warned.nonTerminalEnvelope) {
339
+ warned.nonTerminalEnvelope = true;
340
+ logger.warn(`BytePlus video fixture for job ${job.id} has reached its terminal poll but its envelope carries the NON-TERMINAL status "${status}" — every later poll returns this same body, so the client will poll it forever and fail on its own timeout. Re-record the fixture, or author a terminal status (succeeded, failed, cancelled, expired)`);
341
+ }
342
+ if (!BYTEPLUS_TASK_STATUSES.has(status) && !warned.unknownStatus) {
343
+ warned.unknownStatus = true;
344
+ logger.warn(`BytePlus video fixture for job ${job.id} carries status "${status}", which is not one of the six the BytePlus client library declares (queued, running, succeeded, failed, cancelled, expired) — serving it as authored, but that library's mapStatus() THROWS on a status it does not recognize`);
345
+ }
346
+ const videoUrl = asRecord(envelope.content)?.video_url;
347
+ if (status === "succeeded" && typeof videoUrl !== "string" && !warned.missingUrl) {
348
+ warned.missingUrl = true;
349
+ logger.warn(`BytePlus video fixture for job ${job.id} reports status "succeeded" but carries no content.video_url — the client will throw "Video is not ready for download. Check status first." Add content.video_url to the fixture envelope`);
350
+ }
351
+ const updatedAt = envelope.updated_at;
352
+ if (status === "succeeded" && typeof videoUrl === "string" && typeof updatedAt === "number" && Number.isFinite(updatedAt) && !warned.expiredUrl) {
353
+ const expiresAt = updatedAt + BYTEPLUS_URL_TTL_SECONDS;
354
+ if (expiresAt * 1e3 < Date.now()) {
355
+ warned.expiredUrl = true;
356
+ logger.warn(`BytePlus video fixture for job ${job.id} has a content.video_url that expired at ${(/* @__PURE__ */ new Date(expiresAt * 1e3)).toISOString()} (Ark output urls live 24h from updated_at) — serving it anyway; re-record the fixture to get a live url`);
357
+ }
358
+ }
359
+ return {
360
+ ...envelope,
361
+ id: job.id
362
+ };
363
+ }
364
+ /**
365
+ * Structurally valid journal body for a field-validation 400. Mirrors
366
+ * grok-video / openrouter-video: `model` stays a string and `messages` is an
367
+ * empty array; underscore-prefixed keys are stripped so a request cannot spoof
368
+ * handler-set discriminators.
369
+ */
370
+ function validationJournalBody(videoReq) {
371
+ const rawModel = videoReq.model;
372
+ const model = typeof rawModel === "string" ? rawModel : rawModel === void 0 ? "" : JSON.stringify(rawModel);
373
+ return {
374
+ ...Object.fromEntries(Object.entries(videoReq).filter(([key]) => !key.startsWith("_"))),
375
+ model,
376
+ messages: []
377
+ };
378
+ }
379
+ async function handleBytePlusVideoCreate(req, res, raw, fixtures, journal, defaults, setCorsHeaders, jobs) {
380
+ setCorsHeaders(res);
381
+ const path = req.url ?? BYTEPLUS_VIDEO_TASKS_PATH;
382
+ const method = req.method ?? "POST";
383
+ const fail = (status, message, body) => {
384
+ journal.add({
385
+ method,
386
+ path,
387
+ headers: require_helpers.flattenHeaders(req.headers),
388
+ body,
389
+ response: {
390
+ status,
391
+ fixture: null
392
+ }
393
+ });
394
+ require_sse_writer.writeErrorResponse(res, status, arkErrorBody(message));
395
+ };
396
+ let videoReq;
397
+ try {
398
+ videoReq = JSON.parse(raw);
399
+ } catch (parseErr) {
400
+ fail(400, `Malformed JSON: ${parseErr instanceof Error ? parseErr.message : "unknown"}`, null);
401
+ return;
402
+ }
403
+ if (videoReq === null || typeof videoReq !== "object" || Array.isArray(videoReq)) {
404
+ fail(400, "Request body must be a JSON object", null);
405
+ return;
406
+ }
407
+ const parsedBody = validationJournalBody(videoReq);
408
+ if (typeof videoReq.model !== "string" || !videoReq.model) {
409
+ fail(400, videoReq.model === void 0 ? "Missing required parameter: 'model'" : "Invalid type for parameter: 'model' must be a non-empty string", parsedBody);
410
+ return;
411
+ }
412
+ if (!Array.isArray(videoReq.content) || videoReq.content.length === 0) {
413
+ fail(400, videoReq.content === void 0 ? "Missing required parameter: 'content'" : "Invalid type for parameter: 'content' must be a non-empty array", parsedBody);
414
+ return;
415
+ }
416
+ const syntheticReq = {
417
+ model: videoReq.model,
418
+ messages: [{
419
+ role: "user",
420
+ content: buildBytePlusMatchText(videoReq.content)
421
+ }],
422
+ _endpointType: "video",
423
+ _videoProvider: "byteplus",
424
+ _context: require_helpers.getContext(req)
425
+ };
426
+ const testId = require_helpers.getTestId(req);
427
+ const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
428
+ if (fixture) {
429
+ journal.incrementFixtureMatchCount(fixture, fixtures, testId);
430
+ defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
431
+ }
432
+ if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
433
+ method,
434
+ path,
435
+ headers: require_helpers.flattenHeaders(req.headers),
436
+ body: syntheticReq
437
+ }, fixture ? "fixture" : require_helpers.resolveStrictMode(defaults.strict, req.headers) ? "internal" : defaults.record?.providers.byteplus ? "proxy" : "internal", defaults.registry, defaults.logger)) return;
438
+ if (!fixture) {
439
+ if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
440
+ const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
441
+ defaults.logger.error(require_helpers.strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
442
+ journal.add({
443
+ method,
444
+ path,
445
+ headers: require_helpers.flattenHeaders(req.headers),
446
+ body: syntheticReq,
447
+ response: {
448
+ status: 503,
449
+ fixture: null,
450
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
451
+ }
452
+ });
453
+ require_sse_writer.writeErrorResponse(res, 503, arkErrorBody(strictMessage));
454
+ return;
455
+ }
456
+ if (defaults.record) {
457
+ if (await proxyBytePlusVideoSubmit({
458
+ req,
459
+ res,
460
+ raw,
461
+ syntheticReq,
462
+ record: defaults.record,
463
+ journal,
464
+ defaults,
465
+ jobs,
466
+ method,
467
+ path
468
+ }) === "handled") return;
469
+ }
470
+ journal.add({
471
+ method,
472
+ path,
473
+ headers: require_helpers.flattenHeaders(req.headers),
474
+ body: syntheticReq,
475
+ response: {
476
+ status: 404,
477
+ fixture: null,
478
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
479
+ }
480
+ });
481
+ require_sse_writer.writeErrorResponse(res, 404, arkErrorBody("No byteplus fixture matched. A byteplus video fixture must carry match.endpoint: \"video\" and a { json: <Ark task envelope> } response"));
482
+ return;
483
+ }
484
+ const worldGeneration = jobs.generation;
485
+ const response = await require_helpers.resolveResponse(fixture, syntheticReq);
486
+ if (require_helpers.isErrorResponse(response)) {
487
+ if (res.destroyed || res.writableEnded) return;
488
+ const status = response.status ?? 500;
489
+ journal.add({
490
+ method,
491
+ path,
492
+ headers: require_helpers.flattenHeaders(req.headers),
493
+ body: syntheticReq,
494
+ response: {
495
+ status,
496
+ fixture
497
+ }
498
+ });
499
+ require_sse_writer.writeErrorResponse(res, status, require_helpers.serializeErrorResponse(response), { retryAfter: response.retryAfter });
500
+ return;
501
+ }
502
+ if (!require_helpers.isJSONResponse(response)) {
503
+ if (res.destroyed || res.writableEnded) return;
504
+ defaults.logger.warn("BytePlus video fixture matched but its response is not a { json: … } payload — this surface stores the Ark task envelope verbatim, NOT a VideoResponse. Treating it as a no-match");
505
+ journal.add({
506
+ method,
507
+ path,
508
+ headers: require_helpers.flattenHeaders(req.headers),
509
+ body: syntheticReq,
510
+ response: {
511
+ status: 404,
512
+ fixture: null
513
+ }
514
+ });
515
+ require_sse_writer.writeErrorResponse(res, 404, arkErrorBody("Matched byteplus fixture has a non-JSON response; expected { json: <Ark task envelope> }"));
516
+ return;
517
+ }
518
+ const envelope = asRecord(response.json);
519
+ if (!envelope) {
520
+ if (res.destroyed || res.writableEnded) return;
521
+ defaults.logger.warn("BytePlus video fixture's json payload is not a JSON object — expected an Ark task envelope");
522
+ journal.add({
523
+ method,
524
+ path,
525
+ headers: require_helpers.flattenHeaders(req.headers),
526
+ body: syntheticReq,
527
+ response: {
528
+ status: 404,
529
+ fixture: null
530
+ }
531
+ });
532
+ require_sse_writer.writeErrorResponse(res, 404, arkErrorBody("Matched byteplus fixture's json is not an object"));
533
+ return;
534
+ }
535
+ if (res.destroyed || res.writableEnded) return;
536
+ journal.add({
537
+ method,
538
+ path,
539
+ headers: require_helpers.flattenHeaders(req.headers),
540
+ body: syntheticReq,
541
+ response: {
542
+ status: 200,
543
+ fixture
544
+ }
545
+ });
546
+ const id = `cgt-${node_crypto.default.randomUUID()}`;
547
+ const progression = require_fal.resolveProgression(defaults.bytePlusVideo);
548
+ const job = {
549
+ kind: "replay",
550
+ id,
551
+ phase: "queued",
552
+ pollCount: 0,
553
+ pollsBeforeRunning: progression.pollsBeforeInProgress,
554
+ pollsBeforeTerminal: progression.pollsBeforeCompleted,
555
+ envelope
556
+ };
557
+ if (progression.pollsBeforeCompleted === 0) job.phase = "terminal";
558
+ if (jobs.generation === worldGeneration) jobs.set(`${testId}:${id}`, job);
559
+ else defaults.logger.warn(`BytePlus video submit resolved after a fixtures reset — not inserting job ${id} into the new world (its polls will 404)`);
560
+ res.writeHead(200, { "Content-Type": "application/json" });
561
+ res.end(JSON.stringify({ id }));
562
+ }
563
+ async function handleBytePlusVideoStatus(req, res, id, fixtures, journal, defaults, setCorsHeaders, jobs) {
564
+ setCorsHeaders(res);
565
+ const path = req.url ?? `${BYTEPLUS_VIDEO_TASKS_PATH}/${id}`;
566
+ const method = req.method ?? "GET";
567
+ const testId = require_helpers.getTestId(req);
568
+ const key = `${testId}:${id}`;
569
+ if (require_chaos.applyChaos(res, null, defaults.chaos, req.headers, journal, {
570
+ method,
571
+ path,
572
+ headers: require_helpers.flattenHeaders(req.headers),
573
+ body: null
574
+ }, "internal", defaults.registry, defaults.logger)) return;
575
+ const job = jobs.get(key);
576
+ if (!job) {
577
+ journal.add({
578
+ method,
579
+ path,
580
+ headers: require_helpers.flattenHeaders(req.headers),
581
+ body: null,
582
+ response: {
583
+ status: 404,
584
+ fixture: null
585
+ }
586
+ });
587
+ require_sse_writer.writeErrorResponse(res, 404, arkErrorBody(`Unknown video task id "${id}" — no such job for this test scope. It was never created, it belongs to a different x-test-id, or it aged out of aimock's 1h job TTL`));
588
+ return;
589
+ }
590
+ if (job.kind === "record") {
591
+ if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
592
+ defaults.logger.error(`STRICT: video task ${id} is proxied live upstream (record mode) — refusing the upstream poll`);
593
+ journal.add({
594
+ method,
595
+ path,
596
+ headers: require_helpers.flattenHeaders(req.headers),
597
+ body: null,
598
+ response: {
599
+ status: 503,
600
+ fixture: null,
601
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
602
+ }
603
+ });
604
+ require_sse_writer.writeErrorResponse(res, 503, arkErrorBody(`Strict mode: video task ${id} is proxied live upstream (record mode) — nothing reaches an upstream under strict mode`));
605
+ return;
606
+ }
607
+ await proxyBytePlusVideoRecordPoll({
608
+ req,
609
+ res,
610
+ job,
611
+ key,
612
+ testId,
613
+ fixtures,
614
+ journal,
615
+ defaults,
616
+ jobs,
617
+ method,
618
+ path
619
+ });
620
+ return;
621
+ }
622
+ if (res.destroyed || res.writableEnded) return;
623
+ advanceJob(job);
624
+ jobs.set(key, job);
625
+ const body = serializeBytePlusVideoTask(job, defaults.logger);
626
+ if (!body) {
627
+ defaults.logger.warn(`BytePlus video fixture for job ${id} has an envelope that is not a usable Ark task body (missing or non-string "status") — a fixture that cannot produce a poll body is an authoring error`);
628
+ journal.add({
629
+ method,
630
+ path,
631
+ headers: require_helpers.flattenHeaders(req.headers),
632
+ body: null,
633
+ response: {
634
+ status: 502,
635
+ fixture: null
636
+ }
637
+ });
638
+ require_sse_writer.writeErrorResponse(res, 502, arkErrorBody(`Fixture envelope for video task ${id} carries no "status" — expected an Ark task envelope`));
639
+ return;
640
+ }
641
+ journal.add({
642
+ method,
643
+ path,
644
+ headers: require_helpers.flattenHeaders(req.headers),
645
+ body: null,
646
+ response: {
647
+ status: 200,
648
+ fixture: null
649
+ }
650
+ });
651
+ res.writeHead(200, { "Content-Type": "application/json" });
652
+ res.end(JSON.stringify(body));
653
+ }
654
+ /**
655
+ * Proxy an unmatched submit to the configured upstream and answer the client
656
+ * with a mock-rewritten `{ id }`. The upstream lifecycle is then driven
657
+ * interactively by the client's own polls. Returns "no_upstream" when record
658
+ * mode has no byteplus provider URL — the caller falls through to 404.
659
+ */
660
+ async function proxyBytePlusVideoSubmit(args) {
661
+ const { req, res, raw, syntheticReq, record, journal, defaults, jobs, method, path } = args;
662
+ const upstreamBase = record.providers.byteplus;
663
+ if (!upstreamBase) {
664
+ defaults.logger.warn(`No upstream URL configured for provider "byteplus" — cannot proxy`);
665
+ return "no_upstream";
666
+ }
667
+ const proxyError = (msg) => {
668
+ defaults.logger.error(`BytePlus video submit proxy failed: ${msg}`);
669
+ if (res.destroyed || res.writableEnded) return "handled";
670
+ journal.add({
671
+ method,
672
+ path,
673
+ headers: require_helpers.flattenHeaders(req.headers),
674
+ body: syntheticReq,
675
+ response: {
676
+ status: 502,
677
+ fixture: null,
678
+ source: "proxy",
679
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
680
+ }
681
+ });
682
+ require_sse_writer.writeErrorResponse(res, 502, arkErrorBody(`Proxy to upstream failed: ${msg}`));
683
+ return "handled";
684
+ };
685
+ let submitUrl;
686
+ try {
687
+ submitUrl = require_url.resolveUpstreamUrl(upstreamBase, BYTEPLUS_VIDEO_TASKS_PATH);
688
+ } catch (err) {
689
+ return proxyError(`Invalid upstream URL: ${upstreamBase} (${err instanceof Error ? err.message : String(err)})`);
690
+ }
691
+ defaults.logger.warn(`NO FIXTURE MATCH — proxying video submit to ${submitUrl.toString()}`);
692
+ const worldGeneration = jobs.generation;
693
+ let fetched;
694
+ try {
695
+ const headers = require_recorder.prepareEgressHeaders(req, submitUrl, "byteplus", record.providerKeys?.byteplus);
696
+ if (!headers) return proxyError("No configured provider credential");
697
+ const upstreamRes = await fetch(submitUrl, {
698
+ method: "POST",
699
+ headers,
700
+ body: raw,
701
+ signal: require_video_proxy_shared.upstreamTimeoutSignal(record)
702
+ });
703
+ fetched = {
704
+ status: upstreamRes.status,
705
+ contentType: upstreamRes.headers.get("content-type"),
706
+ text: await require_video_proxy_shared.readEnvelopeText(upstreamRes, record)
707
+ };
708
+ } catch (err) {
709
+ return proxyError(err instanceof Error ? err.message : "Unknown proxy error");
710
+ }
711
+ if (fetched.status === 401 || fetched.status === 403) {
712
+ defaults.logger.warn(`Upstream rejected the video submit (${fetched.status}) — relaying the upstream status`);
713
+ if (res.destroyed || res.writableEnded) return "handled";
714
+ journal.add({
715
+ method,
716
+ path,
717
+ headers: require_helpers.flattenHeaders(req.headers),
718
+ body: syntheticReq,
719
+ response: {
720
+ status: fetched.status,
721
+ fixture: null,
722
+ source: "proxy",
723
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
724
+ }
725
+ });
726
+ res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
727
+ res.end(fetched.text);
728
+ return "handled";
729
+ }
730
+ if (fetched.status < 200 || fetched.status >= 300) return proxyError(`Submit ${fetched.status}: ${fetched.text.slice(0, 200)}`);
731
+ let upstreamTaskId;
732
+ {
733
+ let parsed;
734
+ try {
735
+ parsed = JSON.parse(fetched.text);
736
+ } catch {
737
+ return proxyError(`Submit returned non-JSON: ${fetched.text.slice(0, 200)}`);
738
+ }
739
+ const envelope = asRecord(parsed);
740
+ if (!envelope) return proxyError("Submit response is not a JSON object");
741
+ upstreamTaskId = String(envelope.id ?? "").trim();
742
+ if (!upstreamTaskId) return proxyError("Submit response missing id");
743
+ }
744
+ const upstreamPollingUrl = require_url.resolveUpstreamUrl(upstreamBase, `${BYTEPLUS_VIDEO_TASKS_PATH}/${encodeURIComponent(upstreamTaskId)}`).toString();
745
+ const testId = require_helpers.getTestId(req);
746
+ const matchRequest = defaults.requestTransform ? defaults.requestTransform(syntheticReq) : syntheticReq;
747
+ const id = `cgt-${node_crypto.default.randomUUID()}`;
748
+ const job = {
749
+ kind: "record",
750
+ id,
751
+ upstreamTaskId,
752
+ upstreamPollingUrl,
753
+ match: require_recorder.buildFixtureMatch(matchRequest, record)
754
+ };
755
+ if (jobs.generation === worldGeneration) jobs.set(`${testId}:${id}`, job);
756
+ else defaults.logger.warn(`BytePlus video submit for upstream task ${upstreamTaskId} completed after a fixtures reset — not inserting the job into the new world (its polls will 404)`);
757
+ if (res.destroyed || res.writableEnded) return "handled";
758
+ journal.add({
759
+ method,
760
+ path,
761
+ headers: require_helpers.flattenHeaders(req.headers),
762
+ body: syntheticReq,
763
+ response: {
764
+ status: 200,
765
+ fixture: null,
766
+ source: "proxy",
767
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
768
+ }
769
+ });
770
+ res.writeHead(200, { "Content-Type": "application/json" });
771
+ res.end(JSON.stringify({ id }));
772
+ return "handled";
773
+ }
774
+ /**
775
+ * Proxy a status poll 1:1 and relay the result with the mock id substituted.
776
+ *
777
+ * An upstream 4xx is RELAYED VERBATIM rather than converted to 502 — a 404 in
778
+ * particular, because the client has a deliberate 404 branch that keeps Ark's
779
+ * own code/message on purpose, and converting it would make record mode behave
780
+ * differently from live Ark for exactly the case that branch exists to serve.
781
+ * 5xx and transport failures become 502.
782
+ *
783
+ * On ANY terminal status the rewritten body is relayed immediately and the
784
+ * envelope is captured. All four terminal states are captured identically:
785
+ * there is no warn-and-drop branch, so a task cancelled from the Ark console
786
+ * mid-record yields an artifact rather than a warning and an empty directory.
787
+ */
788
+ async function proxyBytePlusVideoRecordPoll(args) {
789
+ const { req, res, job, key, testId, fixtures, journal, defaults, jobs, method, path } = args;
790
+ const logger = defaults.logger;
791
+ const journalProxy = (status) => {
792
+ journal.add({
793
+ method,
794
+ path,
795
+ headers: require_helpers.flattenHeaders(req.headers),
796
+ body: null,
797
+ response: {
798
+ status,
799
+ fixture: null,
800
+ source: "proxy",
801
+ ...require_helpers.strictOverrideField(defaults.strict, req.headers)
802
+ }
803
+ });
804
+ };
805
+ const proxyError = (msg) => {
806
+ logger.error(`BytePlus video poll proxy failed: ${msg}`);
807
+ if (res.destroyed || res.writableEnded) return;
808
+ journalProxy(502);
809
+ require_sse_writer.writeErrorResponse(res, 502, arkErrorBody(`Proxy to upstream failed: ${msg}`));
810
+ };
811
+ const record = defaults.record;
812
+ if (!record) {
813
+ proxyError("record mode is no longer configured for an in-flight record job");
814
+ return;
815
+ }
816
+ let fetched;
817
+ try {
818
+ const headers = require_recorder.prepareEgressHeaders(req, new URL(job.upstreamPollingUrl), "byteplus", record.providerKeys?.byteplus);
819
+ if (!headers) {
820
+ proxyError("No configured provider credential");
821
+ return;
822
+ }
823
+ const upstreamRes = await fetch(job.upstreamPollingUrl, {
824
+ headers,
825
+ signal: require_video_proxy_shared.upstreamTimeoutSignal(record)
826
+ });
827
+ fetched = {
828
+ status: upstreamRes.status,
829
+ contentType: upstreamRes.headers.get("content-type"),
830
+ text: await require_video_proxy_shared.readEnvelopeText(upstreamRes, record)
831
+ };
832
+ } catch (err) {
833
+ proxyError(err instanceof Error ? err.message : "Unknown proxy error");
834
+ return;
835
+ }
836
+ if (fetched.status >= 400 && fetched.status < 500) {
837
+ logger.warn(`Upstream returned ${fetched.status} for video task ${job.upstreamTaskId} — relaying it verbatim`);
838
+ if (res.destroyed || res.writableEnded) return;
839
+ journalProxy(fetched.status);
840
+ res.writeHead(fetched.status, { "Content-Type": fetched.contentType ?? "application/json" });
841
+ res.end(fetched.text);
842
+ return;
843
+ }
844
+ if (fetched.status < 200 || fetched.status >= 300) {
845
+ proxyError(`Status ${fetched.status}: ${fetched.text.slice(0, 200)}`);
846
+ return;
847
+ }
848
+ let upstreamBody;
849
+ {
850
+ let parsed;
851
+ try {
852
+ parsed = JSON.parse(fetched.text);
853
+ } catch {
854
+ proxyError(`Status returned non-JSON: ${fetched.text.slice(0, 200)}`);
855
+ return;
856
+ }
857
+ const envelope = asRecord(parsed);
858
+ if (!envelope) {
859
+ proxyError("Status response is not a JSON object");
860
+ return;
861
+ }
862
+ upstreamBody = envelope;
863
+ }
864
+ const relayBody = {
865
+ ...upstreamBody,
866
+ id: job.id
867
+ };
868
+ const relayJson = (body) => {
869
+ if (res.destroyed || res.writableEnded) return;
870
+ journalProxy(200);
871
+ res.writeHead(200, { "Content-Type": "application/json" });
872
+ res.end(JSON.stringify(body));
873
+ };
874
+ const rawStatus = upstreamBody.status;
875
+ const upstreamStatus = typeof rawStatus === "string" ? rawStatus : "";
876
+ if (!upstreamStatus || !isBytePlusTerminalStatus(upstreamStatus)) {
877
+ if (jobs.get(key) === job) jobs.set(key, job);
878
+ relayJson(relayBody);
879
+ return;
880
+ }
881
+ if (record.proxyOnly) {
882
+ if (jobs.get(key) === job) jobs.set(key, job);
883
+ relayJson(relayBody);
884
+ return;
885
+ }
886
+ if (jobs.get(key) !== job) {
887
+ relayJson(relayBody);
888
+ return;
889
+ }
890
+ captureBytePlusVideoRecordFixture({
891
+ job,
892
+ key,
893
+ testId,
894
+ fixtures,
895
+ defaults,
896
+ jobs,
897
+ record,
898
+ upstreamBody,
899
+ res
900
+ });
901
+ relayJson(relayBody);
902
+ }
903
+ /**
904
+ * Capture a terminal record job into a fixture (PERSIST only — NO byte
905
+ * download) and mutate the map entry into a terminal replay job.
906
+ *
907
+ * Runs BEFORE the caller relays, and is fully synchronous, so a persist failure
908
+ * still reaches the client as an `X-AIMock-Record-Error` header rather than
909
+ * only as a log line behind a 200 the client cannot distinguish from a clean
910
+ * record.
911
+ *
912
+ * The upstream `id` is REMOVED at capture: replay stamps its own, and keeping a
913
+ * real Ark task id in a committed artifact leaks it for no benefit.
914
+ */
915
+ function captureBytePlusVideoRecordFixture(args) {
916
+ const { job, key, testId, fixtures, defaults, jobs, record, upstreamBody, res } = args;
917
+ const logger = defaults.logger;
918
+ try {
919
+ const { id: _discardedUpstreamId, ...envelope } = upstreamBody;
920
+ const persistResult = require_recorder.persistFixture({
921
+ record,
922
+ providerKey: "byteplus",
923
+ testId,
924
+ fixture: {
925
+ match: job.match,
926
+ response: { json: envelope }
927
+ },
928
+ fixtures,
929
+ logger
930
+ });
931
+ if (persistResult.kind === "failed" && !res.headersSent) res.setHeader("X-AIMock-Record-Error", require_recorder.sanitizeHeaderValue(persistResult.error));
932
+ jobs.set(key, {
933
+ kind: "replay",
934
+ id: job.id,
935
+ phase: "terminal",
936
+ pollCount: 0,
937
+ pollsBeforeRunning: 0,
938
+ pollsBeforeTerminal: 0,
939
+ envelope
940
+ });
941
+ } catch (err) {
942
+ const msg = err instanceof Error ? err.message : String(err);
943
+ logger.error(`BytePlus video capture for task ${job.upstreamTaskId} failed unexpectedly (${msg}) — fixture not persisted; the job keeps proxying live`);
944
+ }
945
+ }
946
+
947
+ //#endregion
948
+ exports.BYTEPLUS_VIDEO_MAX_ENTRIES = BYTEPLUS_VIDEO_MAX_ENTRIES;
949
+ exports.BYTEPLUS_VIDEO_TASKS_PATH = BYTEPLUS_VIDEO_TASKS_PATH;
950
+ exports.BytePlusVideoJobMap = BytePlusVideoJobMap;
951
+ exports.buildBytePlusMatchText = buildBytePlusMatchText;
952
+ exports.handleBytePlusVideoCreate = handleBytePlusVideoCreate;
953
+ exports.handleBytePlusVideoStatus = handleBytePlusVideoStatus;
954
+ //# sourceMappingURL=byteplus-video.cjs.map