@copilotkit/aimock 1.34.0 → 1.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +22 -0
- package/README.md +2 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/bedrock-converse.cjs +110 -8
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +111 -9
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +141 -4
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +142 -5
- package/dist/bedrock.js.map +1 -1
- package/dist/cohere.cjs +89 -7
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +90 -8
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/fixture-loader.cjs +121 -20
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +121 -20
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +97 -60
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +98 -61
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +71 -6
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +72 -7
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +834 -0
- package/dist/grok-video.cjs.map +1 -0
- package/dist/grok-video.d.cts +74 -0
- package/dist/grok-video.d.cts.map +1 -0
- package/dist/grok-video.d.ts +74 -0
- package/dist/grok-video.d.ts.map +1 -0
- package/dist/grok-video.js +829 -0
- package/dist/grok-video.js.map +1 -0
- package/dist/helpers.cjs +181 -2
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +33 -3
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +33 -3
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +181 -3
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +11 -0
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/messages.cjs +99 -10
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +100 -11
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +11 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +8 -1
- package/dist/metrics.js.map +1 -1
- package/dist/ollama.cjs +74 -4
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +75 -5
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-video.cjs +19 -138
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +1 -120
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +36 -20
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.js +36 -20
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +136 -58
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +2 -2
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +2 -2
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +137 -59
- package/dist/responses.js.map +1 -1
- package/dist/server.cjs +162 -20
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +4 -0
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +164 -22
- package/dist/server.js.map +1 -1
- package/dist/stream-collapse.cjs +300 -74
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +17 -1
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +17 -1
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +300 -74
- package/dist/stream-collapse.js.map +1 -1
- package/dist/types.d.cts +104 -7
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +104 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +794 -0
- package/dist/veo-video.cjs.map +1 -0
- package/dist/veo-video.d.cts +50 -0
- package/dist/veo-video.d.cts.map +1 -0
- package/dist/veo-video.d.ts +50 -0
- package/dist/veo-video.d.ts.map +1 -0
- package/dist/veo-video.js +789 -0
- package/dist/veo-video.js.map +1 -0
- package/dist/video-proxy-shared.cjs +145 -0
- package/dist/video-proxy-shared.cjs.map +1 -0
- package/dist/video-proxy-shared.js +140 -0
- package/dist/video-proxy-shared.js.map +1 -0
- package/dist/ws-gemini-live.cjs +96 -3
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +97 -4
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +281 -4
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +282 -5
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +1 -1
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +1 -1
- package/dist/ws-responses.js.map +1 -1
- package/fixtures/examples/llm/blocks-tool-first.json +13 -0
- package/package.json +1 -1
- 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
|