@copilotkit/aimock 1.21.0 → 1.22.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 +56 -0
- package/README.md +1 -0
- package/dist/a2a-mock.cjs +1 -1
- package/dist/a2a-mock.cjs.map +1 -1
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +1 -1
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +25 -12
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.js +25 -12
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +0 -0
- package/dist/aimock-cli.js +0 -0
- package/dist/bedrock-converse.cjs +72 -26
- 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 +73 -27
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +69 -24
- 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 +70 -25
- package/dist/bedrock.js.map +1 -1
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +34 -11
- 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 +35 -12
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +8 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +46 -20
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +47 -21
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +25 -21
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +26 -22
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +138 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +139 -44
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +27 -8
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -9
- package/dist/fal.js.map +1 -1
- package/dist/fixture-loader.cjs +9 -1
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.js +9 -1
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +34 -9
- 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 +34 -11
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +50 -21
- 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 +51 -22
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +82 -8
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +7 -0
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +80 -9
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +31 -10
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +32 -11
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/journal.cjs +17 -7
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +2 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +2 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +15 -4
- package/dist/journal.js.map +1 -1
- package/dist/mcp-mock.cjs +1 -1
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +1 -1
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +38 -14
- 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 +39 -15
- package/dist/messages.js.map +1 -1
- package/dist/moderation.cjs +3 -2
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +3 -2
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +69 -22
- 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 +70 -23
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +89 -41
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +3 -2
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +3 -2
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +89 -41
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +3 -2
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +3 -2
- package/dist/rerank.js.map +1 -1
- package/dist/responses.cjs +66 -54
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +67 -55
- package/dist/responses.js.map +1 -1
- package/dist/search.cjs +3 -2
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +3 -2
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +117 -171
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +95 -149
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +31 -10
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +32 -11
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +51 -21
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +1 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +1 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +51 -21
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +59 -19
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +60 -20
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +4 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.cjs +10 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +10 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/video.cjs +55 -16
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +8 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +8 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +56 -17
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +40 -31
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +2 -0
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +2 -0
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +40 -31
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +257 -16
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +2 -0
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +2 -0
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +257 -16
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +54 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +2 -0
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +2 -0
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +55 -17
- package/dist/ws-responses.js.map +1 -1
- package/package.json +2 -2
package/dist/fal-audio.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { FORMAT_TO_CONTENT_TYPE, getTestId, isAudioResponse, isErrorResponse, resolveResponse } from "./helpers.js";
|
|
1
|
+
import { FORMAT_TO_CONTENT_TYPE, flattenHeaders, getTestId, isAudioResponse, isErrorResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
|
|
2
2
|
import { matchFixture } from "./router.js";
|
|
3
|
+
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
|
+
import { applyChaos } from "./chaos.js";
|
|
3
5
|
import { proxyAndRecord } from "./recorder.js";
|
|
4
6
|
import crypto from "node:crypto";
|
|
5
7
|
|
|
@@ -11,9 +13,30 @@ const FAL_JOB_TTL_MS = 36e5;
|
|
|
11
13
|
* Entries older than FAL_JOB_TTL_MS are lazily evicted on `get`.
|
|
12
14
|
* When the map exceeds FAL_JOB_MAX_ENTRIES on `set`, the oldest entries
|
|
13
15
|
* are removed to stay within bounds.
|
|
16
|
+
* A background sweep runs every 60 s to proactively evict expired entries.
|
|
14
17
|
*/
|
|
15
18
|
var FalJobMap = class {
|
|
16
19
|
entries = /* @__PURE__ */ new Map();
|
|
20
|
+
sweepTimer = null;
|
|
21
|
+
constructor() {
|
|
22
|
+
this.startSweep();
|
|
23
|
+
}
|
|
24
|
+
/** Start the proactive TTL sweep (every 60 s). */
|
|
25
|
+
startSweep() {
|
|
26
|
+
if (this.sweepTimer) return;
|
|
27
|
+
this.sweepTimer = setInterval(() => {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const [key, entry] of this.entries) if (now - entry.createdAt > FAL_JOB_TTL_MS) this.entries.delete(key);
|
|
30
|
+
}, 6e4);
|
|
31
|
+
if (this.sweepTimer && typeof this.sweepTimer === "object" && "unref" in this.sweepTimer) this.sweepTimer.unref();
|
|
32
|
+
}
|
|
33
|
+
/** Stop the proactive TTL sweep. */
|
|
34
|
+
stopSweep() {
|
|
35
|
+
if (this.sweepTimer) {
|
|
36
|
+
clearInterval(this.sweepTimer);
|
|
37
|
+
this.sweepTimer = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
17
40
|
get(key) {
|
|
18
41
|
const entry = this.entries.get(key);
|
|
19
42
|
if (!entry) return void 0;
|
|
@@ -43,6 +66,10 @@ var FalJobMap = class {
|
|
|
43
66
|
clear() {
|
|
44
67
|
this.entries.clear();
|
|
45
68
|
}
|
|
69
|
+
destroy() {
|
|
70
|
+
this.stopSweep();
|
|
71
|
+
this.entries.clear();
|
|
72
|
+
}
|
|
46
73
|
get size() {
|
|
47
74
|
return this.entries.size;
|
|
48
75
|
}
|
|
@@ -107,7 +134,7 @@ async function handleFalQueue(req, res, body, pathname, fixtures, defaults, jour
|
|
|
107
134
|
journal.add({
|
|
108
135
|
method: req.method ?? "GET",
|
|
109
136
|
path: pathname,
|
|
110
|
-
headers:
|
|
137
|
+
headers: flattenHeaders(req.headers),
|
|
111
138
|
body: null,
|
|
112
139
|
response: {
|
|
113
140
|
status: 404,
|
|
@@ -121,11 +148,12 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
121
148
|
let parsed = {};
|
|
122
149
|
if (body.trim()) try {
|
|
123
150
|
parsed = JSON.parse(body);
|
|
124
|
-
} catch {
|
|
151
|
+
} catch (parseErr) {
|
|
152
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
125
153
|
journal.add({
|
|
126
154
|
method: req.method ?? "POST",
|
|
127
155
|
path: pathname,
|
|
128
|
-
headers:
|
|
156
|
+
headers: flattenHeaders(req.headers),
|
|
129
157
|
body: null,
|
|
130
158
|
response: {
|
|
131
159
|
status: 400,
|
|
@@ -134,7 +162,7 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
134
162
|
});
|
|
135
163
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
136
164
|
res.end(JSON.stringify({ error: {
|
|
137
|
-
message:
|
|
165
|
+
message: `Malformed JSON: ${detail}`,
|
|
138
166
|
type: "invalid_request_error"
|
|
139
167
|
} }));
|
|
140
168
|
return;
|
|
@@ -148,15 +176,42 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
148
176
|
_endpointType: "fal-audio"
|
|
149
177
|
};
|
|
150
178
|
const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);
|
|
179
|
+
if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
180
|
+
if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
181
|
+
method: req.method ?? "POST",
|
|
182
|
+
path: pathname,
|
|
183
|
+
headers: flattenHeaders(req.headers),
|
|
184
|
+
body: syntheticReq
|
|
185
|
+
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
151
186
|
if (!fixture) {
|
|
187
|
+
if (resolveStrictMode(defaults.strict, req.headers)) {
|
|
188
|
+
journal.add({
|
|
189
|
+
method: req.method ?? "POST",
|
|
190
|
+
path: pathname,
|
|
191
|
+
headers: {},
|
|
192
|
+
body: syntheticReq,
|
|
193
|
+
response: {
|
|
194
|
+
status: 503,
|
|
195
|
+
fixture: null,
|
|
196
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
200
|
+
res.end(JSON.stringify({ error: {
|
|
201
|
+
message: "Strict mode: no fixture matched",
|
|
202
|
+
type: "invalid_request_error",
|
|
203
|
+
code: "no_fixture_match"
|
|
204
|
+
} }));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
152
207
|
if (defaults.record) {
|
|
153
208
|
const outcome = await proxyAndRecord(req, res, syntheticReq, "fal", pathname, fixtures, defaults, body);
|
|
154
209
|
if (outcome === "handled_by_hook") return;
|
|
155
|
-
if (outcome
|
|
210
|
+
if (outcome !== "not_configured") {
|
|
156
211
|
journal.add({
|
|
157
212
|
method: req.method ?? "POST",
|
|
158
213
|
path: pathname,
|
|
159
|
-
headers:
|
|
214
|
+
headers: flattenHeaders(req.headers),
|
|
160
215
|
body: syntheticReq,
|
|
161
216
|
response: {
|
|
162
217
|
status: res.statusCode ?? 200,
|
|
@@ -167,34 +222,32 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
167
222
|
return;
|
|
168
223
|
}
|
|
169
224
|
}
|
|
170
|
-
const strictStatus = defaults.strict ? 503 : 404;
|
|
171
|
-
const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
172
225
|
journal.add({
|
|
173
226
|
method: req.method ?? "POST",
|
|
174
227
|
path: pathname,
|
|
175
|
-
headers:
|
|
228
|
+
headers: flattenHeaders(req.headers),
|
|
176
229
|
body: syntheticReq,
|
|
177
230
|
response: {
|
|
178
|
-
status:
|
|
179
|
-
fixture: null
|
|
231
|
+
status: 404,
|
|
232
|
+
fixture: null,
|
|
233
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
180
234
|
}
|
|
181
235
|
});
|
|
182
|
-
res.writeHead(
|
|
236
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
183
237
|
res.end(JSON.stringify({ error: {
|
|
184
|
-
message:
|
|
238
|
+
message: "No fixture matched",
|
|
185
239
|
type: "invalid_request_error",
|
|
186
240
|
code: "no_fixture_match"
|
|
187
241
|
} }));
|
|
188
242
|
return;
|
|
189
243
|
}
|
|
190
|
-
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
191
244
|
const response = await resolveResponse(fixture, syntheticReq);
|
|
192
245
|
if (isErrorResponse(response)) {
|
|
193
246
|
const status = response.status ?? 500;
|
|
194
247
|
journal.add({
|
|
195
248
|
method: req.method ?? "POST",
|
|
196
249
|
path: pathname,
|
|
197
|
-
headers:
|
|
250
|
+
headers: flattenHeaders(req.headers),
|
|
198
251
|
body: syntheticReq,
|
|
199
252
|
response: {
|
|
200
253
|
status,
|
|
@@ -202,14 +255,14 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
202
255
|
}
|
|
203
256
|
});
|
|
204
257
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
205
|
-
res.end(
|
|
258
|
+
res.end(serializeErrorResponse(response));
|
|
206
259
|
return;
|
|
207
260
|
}
|
|
208
261
|
if (!isAudioResponse(response)) {
|
|
209
262
|
journal.add({
|
|
210
263
|
method: req.method ?? "POST",
|
|
211
264
|
path: pathname,
|
|
212
|
-
headers:
|
|
265
|
+
headers: flattenHeaders(req.headers),
|
|
213
266
|
body: syntheticReq,
|
|
214
267
|
response: {
|
|
215
268
|
status: 500,
|
|
@@ -228,15 +281,14 @@ async function handleQueueSubmit(req, res, body, pathname, modelId, testId, fixt
|
|
|
228
281
|
requestId,
|
|
229
282
|
modelId,
|
|
230
283
|
status: "COMPLETED",
|
|
231
|
-
result: audioToFalFile(response)
|
|
232
|
-
createdAt: Date.now()
|
|
284
|
+
result: audioToFalFile(response)
|
|
233
285
|
};
|
|
234
286
|
const stateKey = `${testId}:${requestId}`;
|
|
235
287
|
falJobs.set(stateKey, job);
|
|
236
288
|
journal.add({
|
|
237
289
|
method: req.method ?? "POST",
|
|
238
290
|
path: pathname,
|
|
239
|
-
headers:
|
|
291
|
+
headers: flattenHeaders(req.headers),
|
|
240
292
|
body: syntheticReq,
|
|
241
293
|
response: {
|
|
242
294
|
status: 200,
|
|
@@ -259,7 +311,7 @@ function handleQueueStatus(req, res, pathname, requestId, testId, journal) {
|
|
|
259
311
|
journal.add({
|
|
260
312
|
method: req.method ?? "GET",
|
|
261
313
|
path: pathname,
|
|
262
|
-
headers:
|
|
314
|
+
headers: flattenHeaders(req.headers),
|
|
263
315
|
body: null,
|
|
264
316
|
response: {
|
|
265
317
|
status: 404,
|
|
@@ -276,7 +328,7 @@ function handleQueueStatus(req, res, pathname, requestId, testId, journal) {
|
|
|
276
328
|
journal.add({
|
|
277
329
|
method: req.method ?? "GET",
|
|
278
330
|
path: pathname,
|
|
279
|
-
headers:
|
|
331
|
+
headers: flattenHeaders(req.headers),
|
|
280
332
|
body: null,
|
|
281
333
|
response: {
|
|
282
334
|
status: 200,
|
|
@@ -297,7 +349,7 @@ function handleQueueResult(req, res, pathname, requestId, testId, journal) {
|
|
|
297
349
|
journal.add({
|
|
298
350
|
method: req.method ?? "GET",
|
|
299
351
|
path: pathname,
|
|
300
|
-
headers:
|
|
352
|
+
headers: flattenHeaders(req.headers),
|
|
301
353
|
body: null,
|
|
302
354
|
response: {
|
|
303
355
|
status: 404,
|
|
@@ -311,10 +363,27 @@ function handleQueueResult(req, res, pathname, requestId, testId, journal) {
|
|
|
311
363
|
} }));
|
|
312
364
|
return;
|
|
313
365
|
}
|
|
366
|
+
if (job.result === null) {
|
|
367
|
+
journal.add({
|
|
368
|
+
method: req.method ?? "GET",
|
|
369
|
+
path: pathname,
|
|
370
|
+
headers: flattenHeaders(req.headers),
|
|
371
|
+
body: null,
|
|
372
|
+
response: {
|
|
373
|
+
status: 409,
|
|
374
|
+
fixture: null
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
writeErrorResponse(res, 409, JSON.stringify({ error: {
|
|
378
|
+
message: "Job result not yet available",
|
|
379
|
+
type: "not_ready"
|
|
380
|
+
} }));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
314
383
|
journal.add({
|
|
315
384
|
method: req.method ?? "GET",
|
|
316
385
|
path: pathname,
|
|
317
|
-
headers:
|
|
386
|
+
headers: flattenHeaders(req.headers),
|
|
318
387
|
body: null,
|
|
319
388
|
response: {
|
|
320
389
|
status: 200,
|
|
@@ -330,7 +399,7 @@ function handleQueueCancel(req, res, pathname, requestId, testId, journal) {
|
|
|
330
399
|
journal.add({
|
|
331
400
|
method: req.method ?? "DELETE",
|
|
332
401
|
path: pathname,
|
|
333
|
-
headers:
|
|
402
|
+
headers: flattenHeaders(req.headers),
|
|
334
403
|
body: null,
|
|
335
404
|
response: {
|
|
336
405
|
status: 404,
|
|
@@ -344,7 +413,7 @@ function handleQueueCancel(req, res, pathname, requestId, testId, journal) {
|
|
|
344
413
|
journal.add({
|
|
345
414
|
method: req.method ?? "DELETE",
|
|
346
415
|
path: pathname,
|
|
347
|
-
headers:
|
|
416
|
+
headers: flattenHeaders(req.headers),
|
|
348
417
|
body: null,
|
|
349
418
|
response: {
|
|
350
419
|
status: 400,
|
|
@@ -358,11 +427,12 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
358
427
|
let parsed = {};
|
|
359
428
|
if (body.trim()) try {
|
|
360
429
|
parsed = JSON.parse(body);
|
|
361
|
-
} catch {
|
|
430
|
+
} catch (parseErr) {
|
|
431
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
362
432
|
journal.add({
|
|
363
433
|
method: req.method ?? "POST",
|
|
364
434
|
path: pathname,
|
|
365
|
-
headers:
|
|
435
|
+
headers: flattenHeaders(req.headers),
|
|
366
436
|
body: null,
|
|
367
437
|
response: {
|
|
368
438
|
status: 400,
|
|
@@ -371,7 +441,7 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
371
441
|
});
|
|
372
442
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
373
443
|
res.end(JSON.stringify({ error: {
|
|
374
|
-
message:
|
|
444
|
+
message: `Malformed JSON: ${detail}`,
|
|
375
445
|
type: "invalid_request_error"
|
|
376
446
|
} }));
|
|
377
447
|
return;
|
|
@@ -385,15 +455,42 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
385
455
|
_endpointType: "fal-audio"
|
|
386
456
|
};
|
|
387
457
|
const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);
|
|
458
|
+
if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, getTestId(req));
|
|
459
|
+
if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
460
|
+
method: req.method ?? "POST",
|
|
461
|
+
path: pathname,
|
|
462
|
+
headers: flattenHeaders(req.headers),
|
|
463
|
+
body: syntheticReq
|
|
464
|
+
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
388
465
|
if (!fixture) {
|
|
466
|
+
if (resolveStrictMode(defaults.strict, req.headers)) {
|
|
467
|
+
journal.add({
|
|
468
|
+
method: req.method ?? "POST",
|
|
469
|
+
path: pathname,
|
|
470
|
+
headers: {},
|
|
471
|
+
body: syntheticReq,
|
|
472
|
+
response: {
|
|
473
|
+
status: 503,
|
|
474
|
+
fixture: null,
|
|
475
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
479
|
+
res.end(JSON.stringify({ error: {
|
|
480
|
+
message: "Strict mode: no fixture matched",
|
|
481
|
+
type: "invalid_request_error",
|
|
482
|
+
code: "no_fixture_match"
|
|
483
|
+
} }));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
389
486
|
if (defaults.record) {
|
|
390
487
|
const outcome = await proxyAndRecord(req, res, syntheticReq, "fal", pathname, fixtures, defaults, body);
|
|
391
488
|
if (outcome === "handled_by_hook") return;
|
|
392
|
-
if (outcome
|
|
489
|
+
if (outcome !== "not_configured") {
|
|
393
490
|
journal.add({
|
|
394
491
|
method: req.method ?? "POST",
|
|
395
492
|
path: pathname,
|
|
396
|
-
headers:
|
|
493
|
+
headers: flattenHeaders(req.headers),
|
|
397
494
|
body: syntheticReq,
|
|
398
495
|
response: {
|
|
399
496
|
status: res.statusCode ?? 200,
|
|
@@ -404,34 +501,32 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
404
501
|
return;
|
|
405
502
|
}
|
|
406
503
|
}
|
|
407
|
-
const strictStatus = defaults.strict ? 503 : 404;
|
|
408
|
-
const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
409
504
|
journal.add({
|
|
410
505
|
method: req.method ?? "POST",
|
|
411
506
|
path: pathname,
|
|
412
|
-
headers:
|
|
507
|
+
headers: flattenHeaders(req.headers),
|
|
413
508
|
body: syntheticReq,
|
|
414
509
|
response: {
|
|
415
|
-
status:
|
|
416
|
-
fixture: null
|
|
510
|
+
status: 404,
|
|
511
|
+
fixture: null,
|
|
512
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
417
513
|
}
|
|
418
514
|
});
|
|
419
|
-
res.writeHead(
|
|
515
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
420
516
|
res.end(JSON.stringify({ error: {
|
|
421
|
-
message:
|
|
517
|
+
message: "No fixture matched",
|
|
422
518
|
type: "invalid_request_error",
|
|
423
519
|
code: "no_fixture_match"
|
|
424
520
|
} }));
|
|
425
521
|
return;
|
|
426
522
|
}
|
|
427
|
-
journal.incrementFixtureMatchCount(fixture, fixtures, getTestId(req));
|
|
428
523
|
const response = await resolveResponse(fixture, syntheticReq);
|
|
429
524
|
if (isErrorResponse(response)) {
|
|
430
525
|
const status = response.status ?? 500;
|
|
431
526
|
journal.add({
|
|
432
527
|
method: req.method ?? "POST",
|
|
433
528
|
path: pathname,
|
|
434
|
-
headers:
|
|
529
|
+
headers: flattenHeaders(req.headers),
|
|
435
530
|
body: syntheticReq,
|
|
436
531
|
response: {
|
|
437
532
|
status,
|
|
@@ -439,14 +534,14 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
439
534
|
}
|
|
440
535
|
});
|
|
441
536
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
442
|
-
res.end(
|
|
537
|
+
res.end(serializeErrorResponse(response));
|
|
443
538
|
return;
|
|
444
539
|
}
|
|
445
540
|
if (!isAudioResponse(response)) {
|
|
446
541
|
journal.add({
|
|
447
542
|
method: req.method ?? "POST",
|
|
448
543
|
path: pathname,
|
|
449
|
-
headers:
|
|
544
|
+
headers: flattenHeaders(req.headers),
|
|
450
545
|
body: syntheticReq,
|
|
451
546
|
response: {
|
|
452
547
|
status: 500,
|
|
@@ -464,7 +559,7 @@ async function handleSyncRun(req, res, body, pathname, modelId, fixtures, defaul
|
|
|
464
559
|
journal.add({
|
|
465
560
|
method: req.method ?? "POST",
|
|
466
561
|
path: pathname,
|
|
467
|
-
headers:
|
|
562
|
+
headers: flattenHeaders(req.headers),
|
|
468
563
|
body: syntheticReq,
|
|
469
564
|
response: {
|
|
470
565
|
status: 200,
|
package/dist/fal-audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fal-audio.js","names":[],"sources":["../src/fal-audio.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type { AudioResponse, ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n FORMAT_TO_CONTENT_TYPE,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport type { Journal } from \"./journal.js\";\n\n// ─── FalJobMap with TTL and size bound ───────────────────────────────────\n\nconst FAL_JOB_MAX_ENTRIES = 10_000;\nconst FAL_JOB_TTL_MS = 3_600_000; // 1 hour\n\ninterface FalJob {\n requestId: string;\n modelId: string;\n status: \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\";\n result: Record<string, unknown> | null;\n createdAt: number;\n}\n\ninterface FalJobEntry {\n job: FalJob;\n createdAt: number;\n}\n\n/**\n * A Map wrapper for fal.ai queue jobs that enforces a maximum size and per-entry TTL.\n * Entries older than FAL_JOB_TTL_MS are lazily evicted on `get`.\n * When the map exceeds FAL_JOB_MAX_ENTRIES on `set`, the oldest entries\n * are removed to stay within bounds.\n */\nexport class FalJobMap {\n private readonly entries = new Map<string, FalJobEntry>();\n\n get(key: string): FalJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_JOB_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n // Evict oldest entries if over capacity\n if (this.entries.size > FAL_JOB_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_JOB_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n// Module-level singleton — exported so server.ts can clear it during reset\nexport const falJobs = new FalJobMap();\n\n// ─── Audio response translation ──────────────────────────────────────────\n\nexport function audioToFalFile(response: AudioResponse): Record<string, unknown> {\n let contentType: string;\n let data: string;\n\n if (typeof response.audio === \"string\") {\n data = response.audio;\n contentType = FORMAT_TO_CONTENT_TYPE[response.format ?? \"mp3\"] ?? \"audio/mpeg\";\n } else {\n data = response.audio.b64Json;\n contentType = response.audio.contentType ?? \"audio/mpeg\";\n }\n\n const ext =\n response.format ??\n (contentType !== \"audio/mpeg\"\n ? (Object.entries(FORMAT_TO_CONTENT_TYPE).find(([, v]) => v === contentType)?.[0] ?? \"mp3\")\n : \"mp3\");\n\n const fileSize =\n Math.ceil((data.length * 3) / 4) - (data.endsWith(\"==\") ? 2 : data.endsWith(\"=\") ? 1 : 0);\n\n return {\n audio: {\n url: `https://mock.fal.media/files/generated_audio.${ext}`,\n content_type: contentType,\n file_name: `generated_audio.${ext}`,\n file_size: fileSize,\n },\n };\n}\n\n// ─── Route patterns ──────────────────────────────────────────────────────\n\nconst QUEUE_SUBMIT_RE = /^\\/fal\\/queue\\/submit\\/(.+)$/;\nconst QUEUE_STATUS_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)\\/status$/;\nconst QUEUE_RESULT_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)$/;\nconst QUEUE_CANCEL_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)\\/cancel$/;\nconst SYNC_RUN_RE = /^\\/fal\\/run\\/(.+)$/;\n\n// ─── Handler ─────────────────────────────────────────────────────────────\n\nexport async function handleFalQueue(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<void> {\n const testId = getTestId(req);\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n\n // ── Queue Submit ───────────────────────────────────────────────────\n const submitMatch = QUEUE_SUBMIT_RE.exec(pathname);\n if (submitMatch && req.method === \"POST\") {\n const modelId = submitMatch[1];\n return handleQueueSubmit(\n req,\n res,\n body,\n pathname,\n modelId,\n testId,\n fixtures,\n defaults,\n matchCounts,\n journal,\n );\n }\n\n // ── Queue Status ───────────────────────────────────────────────────\n const statusMatch = QUEUE_STATUS_RE.exec(pathname);\n if (statusMatch) {\n const requestId = statusMatch[1];\n return handleQueueStatus(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Queue Cancel ───────────────────────────────────────────────────\n const cancelMatch = QUEUE_CANCEL_RE.exec(pathname);\n if (cancelMatch) {\n const requestId = cancelMatch[1];\n return handleQueueCancel(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Queue Result ───────────────────────────────────────────────────\n const resultMatch = QUEUE_RESULT_RE.exec(pathname);\n if (resultMatch) {\n const requestId = resultMatch[1];\n return handleQueueResult(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Synchronous Run ────────────────────────────────────────────────\n const runMatch = SYNC_RUN_RE.exec(pathname);\n if (runMatch && req.method === \"POST\") {\n const modelId = runMatch[1];\n return handleSyncRun(\n req,\n res,\n body,\n pathname,\n modelId,\n fixtures,\n defaults,\n matchCounts,\n journal,\n );\n }\n\n // Unknown fal path\n const errorBody = { error: { message: \"Unknown fal.ai endpoint\", type: \"not_found\" } };\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(errorBody));\n}\n\n// ─── Sub-handlers ────────────────────────────────────────────────────────\n\nasync function handleQueueSubmit(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n modelId: string,\n testId: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n matchCounts: Map<Fixture, number>,\n journal: Journal,\n): Promise<void> {\n let parsed: Record<string, unknown> = {};\n if (body.trim()) {\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Malformed JSON\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n }\n\n const prompt =\n (typeof parsed.prompt === \"string\" ? parsed.prompt : null) ??\n (typeof parsed.text === \"string\" ? parsed.text : null) ??\n \"\";\n\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"fal-audio\",\n };\n\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n pathname,\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome === \"relayed\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n res.writeHead(strictStatus, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status, fixture },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(response));\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n const requestId = crypto.randomUUID();\n const result = audioToFalFile(response);\n\n const job: FalJob = {\n requestId,\n modelId,\n status: \"COMPLETED\",\n result,\n createdAt: Date.now(),\n };\n\n const stateKey = `${testId}:${requestId}`;\n falJobs.set(stateKey, job);\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n request_id: requestId,\n response_url: `https://queue.fal.run/${modelId}/requests/${requestId}/response`,\n status_url: `https://queue.fal.run/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://queue.fal.run/${modelId}/requests/${requestId}/cancel`,\n queue_position: 0,\n }),\n );\n}\n\nfunction handleQueueStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n status: job.status,\n request_id: job.requestId,\n response_url: `https://queue.fal.run/${job.modelId}/requests/${requestId}/response`,\n }),\n );\n}\n\nfunction handleQueueResult(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(job.result));\n}\n\nfunction handleQueueCancel(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"DELETE\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return;\n }\n\n // Since we complete immediately, cancellation always returns ALREADY_COMPLETED\n journal.add({\n method: req.method ?? \"DELETE\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n}\n\nasync function handleSyncRun(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n modelId: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n matchCounts: Map<Fixture, number>,\n journal: Journal,\n): Promise<void> {\n let parsed: Record<string, unknown> = {};\n if (body.trim()) {\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Malformed JSON\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n }\n\n const prompt =\n (typeof parsed.prompt === \"string\" ? parsed.prompt : null) ??\n (typeof parsed.text === \"string\" ? parsed.text : null) ??\n \"\";\n\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"fal-audio\",\n };\n\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n pathname,\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome === \"relayed\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n res.writeHead(strictStatus, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.incrementFixtureMatchCount(fixture, fixtures, getTestId(req));\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status, fixture },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(response));\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n const result = audioToFalFile(response);\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fal-audio.js","names":[],"sources":["../src/fal-audio.ts"],"sourcesContent":["import type http from \"node:http\";\nimport crypto from \"node:crypto\";\nimport type { AudioResponse, ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n FORMAT_TO_CONTENT_TYPE,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { matchFixture } from \"./router.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\n\n// ─── FalJobMap with TTL and size bound ───────────────────────────────────\n\nconst FAL_JOB_MAX_ENTRIES = 10_000;\nconst FAL_JOB_TTL_MS = 3_600_000; // 1 hour\n\ninterface FalJob {\n requestId: string;\n modelId: string;\n status: \"IN_QUEUE\" | \"IN_PROGRESS\" | \"COMPLETED\";\n result: Record<string, unknown> | null;\n}\n\ninterface FalJobEntry {\n job: FalJob;\n createdAt: number;\n}\n\n/**\n * A Map wrapper for fal.ai queue jobs that enforces a maximum size and per-entry TTL.\n * Entries older than FAL_JOB_TTL_MS are lazily evicted on `get`.\n * When the map exceeds FAL_JOB_MAX_ENTRIES on `set`, the oldest entries\n * are removed to stay within bounds.\n * A background sweep runs every 60 s to proactively evict expired entries.\n */\nexport class FalJobMap {\n private readonly entries = new Map<string, FalJobEntry>();\n private sweepTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor() {\n this.startSweep();\n }\n\n /** Start the proactive TTL sweep (every 60 s). */\n startSweep(): void {\n if (this.sweepTimer) return;\n this.sweepTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (now - entry.createdAt > FAL_JOB_TTL_MS) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n // Allow the process to exit even if the timer is still active\n if (this.sweepTimer && typeof this.sweepTimer === \"object\" && \"unref\" in this.sweepTimer) {\n this.sweepTimer.unref();\n }\n }\n\n /** Stop the proactive TTL sweep. */\n stopSweep(): void {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = null;\n }\n }\n\n get(key: string): FalJob | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > FAL_JOB_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.job;\n }\n\n set(key: string, job: FalJob): void {\n this.entries.set(key, { job, createdAt: Date.now() });\n // Evict oldest entries if over capacity\n if (this.entries.size > FAL_JOB_MAX_ENTRIES) {\n const excess = this.entries.size - FAL_JOB_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n destroy(): void {\n this.stopSweep();\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n// Module-level singleton — exported so server.ts can clear it during reset\nexport const falJobs = new FalJobMap();\n\n// ─── Audio response translation ──────────────────────────────────────────\n\nexport function audioToFalFile(response: AudioResponse): Record<string, unknown> {\n let contentType: string;\n let data: string;\n\n if (typeof response.audio === \"string\") {\n data = response.audio;\n contentType = FORMAT_TO_CONTENT_TYPE[response.format ?? \"mp3\"] ?? \"audio/mpeg\";\n } else {\n data = response.audio.b64Json;\n contentType = response.audio.contentType ?? \"audio/mpeg\";\n }\n\n const ext =\n response.format ??\n (contentType !== \"audio/mpeg\"\n ? (Object.entries(FORMAT_TO_CONTENT_TYPE).find(([, v]) => v === contentType)?.[0] ?? \"mp3\")\n : \"mp3\");\n\n const fileSize =\n Math.ceil((data.length * 3) / 4) - (data.endsWith(\"==\") ? 2 : data.endsWith(\"=\") ? 1 : 0);\n\n return {\n audio: {\n url: `https://mock.fal.media/files/generated_audio.${ext}`,\n content_type: contentType,\n file_name: `generated_audio.${ext}`,\n file_size: fileSize,\n },\n };\n}\n\n// ─── Route patterns ──────────────────────────────────────────────────────\n\nconst QUEUE_SUBMIT_RE = /^\\/fal\\/queue\\/submit\\/(.+)$/;\nconst QUEUE_STATUS_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)\\/status$/;\nconst QUEUE_RESULT_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)$/;\nconst QUEUE_CANCEL_RE = /^\\/fal\\/queue\\/requests\\/([^/]+)\\/cancel$/;\nconst SYNC_RUN_RE = /^\\/fal\\/run\\/(.+)$/;\n\n// ─── Handler ─────────────────────────────────────────────────────────────\n\nexport async function handleFalQueue(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n): Promise<void> {\n const testId = getTestId(req);\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n\n // ── Queue Submit ───────────────────────────────────────────────────\n const submitMatch = QUEUE_SUBMIT_RE.exec(pathname);\n if (submitMatch && req.method === \"POST\") {\n const modelId = submitMatch[1];\n return handleQueueSubmit(\n req,\n res,\n body,\n pathname,\n modelId,\n testId,\n fixtures,\n defaults,\n matchCounts,\n journal,\n );\n }\n\n // ── Queue Status ───────────────────────────────────────────────────\n const statusMatch = QUEUE_STATUS_RE.exec(pathname);\n if (statusMatch) {\n const requestId = statusMatch[1];\n return handleQueueStatus(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Queue Cancel ───────────────────────────────────────────────────\n const cancelMatch = QUEUE_CANCEL_RE.exec(pathname);\n if (cancelMatch) {\n const requestId = cancelMatch[1];\n return handleQueueCancel(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Queue Result ───────────────────────────────────────────────────\n const resultMatch = QUEUE_RESULT_RE.exec(pathname);\n if (resultMatch) {\n const requestId = resultMatch[1];\n return handleQueueResult(req, res, pathname, requestId, testId, journal);\n }\n\n // ── Synchronous Run ────────────────────────────────────────────────\n const runMatch = SYNC_RUN_RE.exec(pathname);\n if (runMatch && req.method === \"POST\") {\n const modelId = runMatch[1];\n return handleSyncRun(\n req,\n res,\n body,\n pathname,\n modelId,\n fixtures,\n defaults,\n matchCounts,\n journal,\n );\n }\n\n // Unknown fal path\n const errorBody = { error: { message: \"Unknown fal.ai endpoint\", type: \"not_found\" } };\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(errorBody));\n}\n\n// ─── Sub-handlers ────────────────────────────────────────────────────────\n\nasync function handleQueueSubmit(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n modelId: string,\n testId: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n matchCounts: Map<Fixture, number>,\n journal: Journal,\n): Promise<void> {\n let parsed: Record<string, unknown> = {};\n if (body.trim()) {\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Malformed JSON: ${detail}`, type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n }\n\n const prompt =\n (typeof parsed.prompt === \"string\" ? parsed.prompt : null) ??\n (typeof parsed.text === \"string\" ? parsed.text : null) ??\n \"\";\n\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"fal-audio\",\n };\n\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n pathname,\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(serializeErrorResponse(response));\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n const requestId = crypto.randomUUID();\n const result = audioToFalFile(response);\n\n const job: FalJob = {\n requestId,\n modelId,\n status: \"COMPLETED\",\n result,\n };\n\n const stateKey = `${testId}:${requestId}`;\n falJobs.set(stateKey, job);\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n request_id: requestId,\n response_url: `https://queue.fal.run/${modelId}/requests/${requestId}/response`,\n status_url: `https://queue.fal.run/${modelId}/requests/${requestId}/status`,\n cancel_url: `https://queue.fal.run/${modelId}/requests/${requestId}/cancel`,\n queue_position: 0,\n }),\n );\n}\n\nfunction handleQueueStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n status: job.status,\n request_id: job.requestId,\n response_url: `https://queue.fal.run/${job.modelId}/requests/${requestId}/response`,\n }),\n );\n}\n\nfunction handleQueueResult(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Request ${requestId} not found`, type: \"not_found\" },\n }),\n );\n return;\n }\n\n if (job.result === null) {\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 409, fixture: null },\n });\n writeErrorResponse(\n res,\n 409,\n JSON.stringify({\n error: { message: \"Job result not yet available\", type: \"not_ready\" },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"GET\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(job.result));\n}\n\nfunction handleQueueCancel(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n requestId: string,\n testId: string,\n journal: Journal,\n): void {\n const stateKey = `${testId}:${requestId}`;\n const job = falJobs.get(stateKey);\n\n if (!job) {\n journal.add({\n method: req.method ?? \"DELETE\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"NOT_FOUND\" }));\n return;\n }\n\n // Since we complete immediately, cancellation always returns ALREADY_COMPLETED\n journal.add({\n method: req.method ?? \"DELETE\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ status: \"ALREADY_COMPLETED\" }));\n}\n\nasync function handleSyncRun(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n pathname: string,\n modelId: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n matchCounts: Map<Fixture, number>,\n journal: Journal,\n): Promise<void> {\n let parsed: Record<string, unknown> = {};\n if (body.trim()) {\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: `Malformed JSON: ${detail}`, type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n }\n\n const prompt =\n (typeof parsed.prompt === \"string\" ? parsed.prompt : null) ??\n (typeof parsed.text === \"string\" ? parsed.text : null) ??\n \"\";\n\n const syntheticReq: ChatCompletionRequest = {\n model: modelId,\n messages: [{ role: \"user\", content: prompt }],\n _endpointType: \"fal-audio\",\n };\n\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, getTestId(req));\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: {},\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"fal\",\n pathname,\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(serializeErrorResponse(response));\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n const result = audioToFalFile(response);\n\n journal.add({\n method: req.method ?? \"POST\",\n path: pathname,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(result));\n}\n"],"mappings":"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"}
|
package/dist/fal.cjs
CHANGED
|
@@ -239,11 +239,31 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
239
239
|
};
|
|
240
240
|
const fixture = require_router.matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
|
|
241
241
|
if (!fixture) {
|
|
242
|
+
if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
|
|
243
|
+
journal.add({
|
|
244
|
+
method: req.method ?? "POST",
|
|
245
|
+
path: pathname,
|
|
246
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
247
|
+
body: syntheticReq,
|
|
248
|
+
response: {
|
|
249
|
+
status: 503,
|
|
250
|
+
fixture: null,
|
|
251
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
255
|
+
res.end(JSON.stringify({ error: {
|
|
256
|
+
message: "Strict mode: no fixture matched",
|
|
257
|
+
type: "invalid_request_error",
|
|
258
|
+
code: "no_fixture_match"
|
|
259
|
+
} }));
|
|
260
|
+
return "handled";
|
|
261
|
+
}
|
|
242
262
|
if (defaults.record) {
|
|
243
263
|
const effectiveDefaults = withFalUpstream(defaults, route.targetHost);
|
|
244
264
|
const outcome = await require_recorder.proxyAndRecord(req, res, syntheticReq, "fal", stripFalPrefix(pathname), fixtures, effectiveDefaults, body);
|
|
245
265
|
if (outcome === "handled_by_hook") return "handled";
|
|
246
|
-
if (outcome
|
|
266
|
+
if (outcome !== "not_configured") {
|
|
247
267
|
journal.add({
|
|
248
268
|
method: req.method ?? "POST",
|
|
249
269
|
path: pathname,
|
|
@@ -258,21 +278,20 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
258
278
|
return "handled";
|
|
259
279
|
}
|
|
260
280
|
}
|
|
261
|
-
const strictStatus = defaults.strict ? 503 : 404;
|
|
262
|
-
const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
263
281
|
journal.add({
|
|
264
282
|
method: req.method ?? "POST",
|
|
265
283
|
path: pathname,
|
|
266
284
|
headers: require_helpers.flattenHeaders(req.headers),
|
|
267
285
|
body: syntheticReq,
|
|
268
286
|
response: {
|
|
269
|
-
status:
|
|
270
|
-
fixture: null
|
|
287
|
+
status: 404,
|
|
288
|
+
fixture: null,
|
|
289
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
271
290
|
}
|
|
272
291
|
});
|
|
273
|
-
res.writeHead(
|
|
292
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
274
293
|
res.end(JSON.stringify({ error: {
|
|
275
|
-
message:
|
|
294
|
+
message: "No fixture matched",
|
|
276
295
|
type: "invalid_request_error",
|
|
277
296
|
code: "no_fixture_match"
|
|
278
297
|
} }));
|
|
@@ -293,7 +312,7 @@ async function handleFal(req, res, body, pathname, fixtures, defaults, journal)
|
|
|
293
312
|
}
|
|
294
313
|
});
|
|
295
314
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
296
|
-
res.end(
|
|
315
|
+
res.end(require_helpers.serializeErrorResponse(response));
|
|
297
316
|
return "handled";
|
|
298
317
|
}
|
|
299
318
|
let payload;
|