@copilotkit/aimock 1.41.0 → 1.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +12 -1
- package/README.md +2 -1
- package/dist/a2a-mock.cjs +18 -11
- package/dist/a2a-mock.cjs.map +1 -1
- package/dist/a2a-mock.d.cts +1 -0
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts +1 -0
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +18 -11
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +2 -0
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +2 -0
- package/dist/agui-mock.js.map +1 -1
- package/dist/bedrock-converse.cjs +36 -2
- 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 +37 -3
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +50 -13
- 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 +51 -14
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +2 -2
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +2 -2
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +36 -7
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +8 -3
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +8 -3
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +36 -8
- package/dist/chaos.js.map +1 -1
- package/dist/cohere.cjs +58 -5
- 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 +59 -6
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/elevenlabs-audio.cjs +36 -2
- 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 +37 -3
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +58 -3
- 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 +59 -4
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +38 -2
- 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 +39 -3
- package/dist/fal-audio.js.map +1 -1
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +9 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +9 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +19 -1
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +20 -2
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +15 -1
- 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 +16 -2
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +70 -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 +71 -7
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +2 -2
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +2 -2
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +160 -5
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +152 -6
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +22 -4
- 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 +23 -5
- package/dist/images.js.map +1 -1
- package/dist/llmock.cjs +15 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +12 -0
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +12 -0
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +16 -1
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-mock.cjs +11 -0
- 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 +11 -0
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +18 -1
- 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 +19 -2
- package/dist/messages.js.map +1 -1
- package/dist/moderation.cjs +67 -3
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts +1 -0
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts +1 -0
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +68 -4
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +75 -5
- 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 +76 -6
- package/dist/ollama.js.map +1 -1
- package/dist/openrouter-video.cjs +4 -4
- 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 +4 -4
- package/dist/openrouter-video.js.map +1 -1
- package/dist/rerank.cjs +65 -2
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.d.cts +1 -1
- package/dist/rerank.d.cts.map +1 -1
- package/dist/rerank.d.ts +1 -1
- package/dist/rerank.d.ts.map +1 -1
- package/dist/rerank.js +66 -3
- package/dist/rerank.js.map +1 -1
- package/dist/responses.cjs +18 -1
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +19 -2
- package/dist/responses.js.map +1 -1
- package/dist/search.cjs +67 -3
- package/dist/search.cjs.map +1 -1
- package/dist/search.d.cts +1 -0
- package/dist/search.d.cts.map +1 -1
- package/dist/search.d.ts +1 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +68 -4
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +317 -13
- 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 +320 -16
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +18 -1
- 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 +19 -2
- package/dist/speech.js.map +1 -1
- package/dist/transcription.cjs +21 -2
- 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 +21 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +39 -2
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +39 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.cjs +88 -0
- 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 +89 -1
- package/dist/vector-mock.js.map +1 -1
- package/dist/veo-video.cjs +2 -2
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.d.cts.map +1 -1
- package/dist/veo-video.d.ts.map +1 -1
- package/dist/veo-video.js +2 -2
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +19 -2
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -3
- package/dist/video.js.map +1 -1
- package/package.json +1 -1
package/dist/server.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgJiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AAqkDjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;UAgJiB,cAAA;EAAA,MAAA,EACP,MAAA,CAAK,MADgB;EAAA,OAAA,EAEpB,OAFoB;KACrB,EAAA,MAAK;UACJ,EAEC,eAFD;aAEC,EACG,aADH;qBACG,EACQ,qBADR;cACQ,EACP,cADO;eACP,EACC,eADD;mBACC,EACI,mBADJ;;;AAqkDjB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
|
package/dist/server.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { DEFAULT_TEST_ID } from "./constants.js";
|
|
1
2
|
import { markAuthenticatedRequest, resolveInboundAuth, validateRequestApiKey, writeApiKeyHttpRejection, writeApiKeyUpgradeRejection } from "./api-key-auth.js";
|
|
2
|
-
import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, readBody, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
3
|
+
import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, readBody, resolveFixtureBlocks, resolveReasoningForModel, resolveResponse, resolveStrictMode, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
3
4
|
import { Logger } from "./logger.js";
|
|
4
5
|
import { Journal } from "./journal.js";
|
|
5
6
|
import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
|
|
6
|
-
import { entryToFixture, validateFixtures } from "./fixture-loader.js";
|
|
7
|
+
import { INJECTED_STATUS_RANGE, entryToFixture, isInjectableStatus, validateFixtures } from "./fixture-loader.js";
|
|
7
8
|
import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
|
|
8
9
|
import { createInterruptionSignal } from "./interruption.js";
|
|
9
10
|
import { buildOpenRouterCandidates, handleOpenRouterCredits, handleOpenRouterKey, handleOpenRouterModels, isOpenRouterPath, resolveOpenRouterShaping, serializeOpenRouterError, shapeOpenRouterChunks, shapeOpenRouterCompletion } from "./openrouter-chat.js";
|
|
10
|
-
import { applyChaosAction, evaluateChaos } from "./chaos.js";
|
|
11
|
+
import { applyChaosAction, evaluateChaos, isChaosScope } from "./chaos.js";
|
|
11
12
|
import { proxyAndRecord } from "./recorder.js";
|
|
12
13
|
import { handleResponses } from "./responses.js";
|
|
13
14
|
import { handleMessages } from "./messages.js";
|
|
@@ -125,7 +126,8 @@ const DEFAULT_MODELS = [
|
|
|
125
126
|
const CORS_HEADERS = {
|
|
126
127
|
"Access-Control-Allow-Origin": "*",
|
|
127
128
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
128
|
-
"Access-Control-Allow-Headers": "*"
|
|
129
|
+
"Access-Control-Allow-Headers": "*",
|
|
130
|
+
"Access-Control-Expose-Headers": "X-Total-Count"
|
|
129
131
|
};
|
|
130
132
|
function setCorsHeaders(res) {
|
|
131
133
|
for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
|
|
@@ -143,11 +145,24 @@ function handleNotFound(res, message) {
|
|
|
143
145
|
} }));
|
|
144
146
|
}
|
|
145
147
|
const CONTROL_PREFIX = "/__aimock";
|
|
148
|
+
/** The complete `GET /__aimock/fixtures` query-param vocabulary; anything else 400s. */
|
|
149
|
+
const FIXTURES_PARAMS = new Set(["include"]);
|
|
150
|
+
/** The complete `GET /__aimock/journal` query-param vocabulary; anything else 400s. */
|
|
151
|
+
const JOURNAL_PARAMS = new Set([
|
|
152
|
+
"limit",
|
|
153
|
+
"offset",
|
|
154
|
+
"path",
|
|
155
|
+
"method",
|
|
156
|
+
"status",
|
|
157
|
+
"service",
|
|
158
|
+
"testId"
|
|
159
|
+
]);
|
|
146
160
|
/**
|
|
147
161
|
* Perform a full reset: clear the fixtures array, the journal (entries *and*
|
|
148
162
|
* per-test fixture match-counts, i.e. sequence position), the video and fal.ai
|
|
149
|
-
* job/queue state,
|
|
150
|
-
* the
|
|
163
|
+
* job/queue state, the Gemini interaction/event-id counters, and any runtime
|
|
164
|
+
* chaos override (reverting to the construction-time chaos config), then
|
|
165
|
+
* re-zero the `aimock_fixtures_loaded` gauge.
|
|
151
166
|
*
|
|
152
167
|
* `targets` is `null` when no server is running (an in-process `reset()` before
|
|
153
168
|
* `start()`). The process-global generation state is reset either way, since it
|
|
@@ -164,6 +179,7 @@ function performFullReset(fixtures, targets) {
|
|
|
164
179
|
resetEventIdCounter();
|
|
165
180
|
if (!targets) return;
|
|
166
181
|
targets.journal.clear();
|
|
182
|
+
targets.defaults.chaos = void 0;
|
|
167
183
|
targets.videoStates.clear();
|
|
168
184
|
targets.openRouterVideoJobs.clear();
|
|
169
185
|
targets.veoVideoJobs.clear();
|
|
@@ -172,10 +188,103 @@ function performFullReset(fixtures, targets) {
|
|
|
172
188
|
if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
|
|
173
189
|
}
|
|
174
190
|
/**
|
|
191
|
+
* JSON-safe redaction for control-API inspection output. Functions (fixture
|
|
192
|
+
* `predicate`s, response factories) and RegExps do not survive
|
|
193
|
+
* JSON.stringify as anything useful, so they become marker strings.
|
|
194
|
+
*/
|
|
195
|
+
function redactForInspection(value) {
|
|
196
|
+
if (typeof value === "function") return "[function]";
|
|
197
|
+
if (value instanceof RegExp) return String(value);
|
|
198
|
+
if (Array.isArray(value)) return value.map(redactForInspection);
|
|
199
|
+
if (value !== null && typeof value === "object") {
|
|
200
|
+
const out = {};
|
|
201
|
+
for (const [key, entry] of Object.entries(value)) out[key] = redactForInspection(entry);
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* One-line kind label for a fixture response used by
|
|
208
|
+
* `GET /__aimock/fixtures?include=fixtures` (e.g. "text", "error").
|
|
209
|
+
* Response factories are reported as "factory".
|
|
210
|
+
*
|
|
211
|
+
* Discriminates with the same `is*Response` guards the request handlers use,
|
|
212
|
+
* in the same order — NOT by inspecting keys. Every response shape extends
|
|
213
|
+
* `ResponseOverrides` (`id`/`created`/`model`/`usage`), so a recorded fixture's
|
|
214
|
+
* first key is usually an envelope override rather than the discriminant. The
|
|
215
|
+
* audio-before-content/toolCalls order is the ORDERING CONTRACT documented on
|
|
216
|
+
* `AudioResponse` in types.ts: those shapes structurally overlap.
|
|
217
|
+
*/
|
|
218
|
+
function fixtureResponseKind(response) {
|
|
219
|
+
if (isResponseFactory(response)) return "factory";
|
|
220
|
+
if (isErrorResponse(response)) return "error";
|
|
221
|
+
if (isAudioResponse(response)) return "audio";
|
|
222
|
+
if (isVideoResponse(response)) return "video";
|
|
223
|
+
if (isTranscriptionResponse(response)) return "transcription";
|
|
224
|
+
if (isImageResponse(response)) return "image";
|
|
225
|
+
if (isEmbeddingResponse(response)) return "embedding";
|
|
226
|
+
if (isJSONResponse(response)) return "json";
|
|
227
|
+
if (isContentWithToolCallsResponse(response)) return "contentWithToolCalls";
|
|
228
|
+
if (isToolCallResponse(response)) return "toolCalls";
|
|
229
|
+
if (isTextResponse(response)) return "text";
|
|
230
|
+
return "unknown";
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve a journal entry's testId exactly the way `getTestId` resolves an
|
|
234
|
+
* incoming request's: the `X-Test-Id` header wins, then `?testId=` parsed out
|
|
235
|
+
* of the query string, then `DEFAULT_TEST_ID`. The
|
|
236
|
+
* filter and the server's own per-test isolation must never disagree — and a
|
|
237
|
+
* raw `path.includes("testId=t1")` both prefix-collides with `t10` and matches
|
|
238
|
+
* unrelated params like `notTestId`.
|
|
239
|
+
*/
|
|
240
|
+
function journalEntryTestId(entry) {
|
|
241
|
+
const header = entry.headers["x-test-id"];
|
|
242
|
+
if (header) return header;
|
|
243
|
+
const qIdx = entry.path.indexOf("?");
|
|
244
|
+
if (qIdx !== -1) {
|
|
245
|
+
const queryValue = new URLSearchParams(entry.path.slice(qIdx + 1)).get("testId");
|
|
246
|
+
if (queryValue) return queryValue;
|
|
247
|
+
}
|
|
248
|
+
return DEFAULT_TEST_ID;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The testId a chaos override is stored under, and read back with — resolved by
|
|
252
|
+
* `getTestId`, i.e. `X-Test-Id` header then `?testId=`, exactly as
|
|
253
|
+
* `resolveScopedDefaults` resolves the traffic being evaluated. A control call
|
|
254
|
+
* and the requests it is meant to affect can then never land in different
|
|
255
|
+
* scopes. `DEFAULT_TEST_ID` (neither header nor param) means the server-wide
|
|
256
|
+
* baseline.
|
|
257
|
+
*
|
|
258
|
+
* Returns `null` when `X-Test-Id` is PRESENT but blank. `String(testId ?? "")`
|
|
259
|
+
* in a harness produces that trivially, and silently treating it as "untagged"
|
|
260
|
+
* would install a server-wide baseline that fails every other test — the exact
|
|
261
|
+
* cross-test leak per-testId scoping exists to prevent. Callers must 400.
|
|
262
|
+
*/
|
|
263
|
+
function chaosScopeId(req) {
|
|
264
|
+
const headerValue = req.headers["x-test-id"];
|
|
265
|
+
const raw = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
266
|
+
if (typeof raw === "string" && raw.trim() === "") return null;
|
|
267
|
+
return getTestId(req);
|
|
268
|
+
}
|
|
269
|
+
/** 400 for a control call whose `X-Test-Id` is present but blank. */
|
|
270
|
+
function writeBlankTestId(res) {
|
|
271
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
272
|
+
res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
/** The chaos config in effect for one testId: its override, else the baseline. */
|
|
276
|
+
function effectiveChaos(defaults, scopeId) {
|
|
277
|
+
const scoped = defaults.chaosByTestId?.get(scopeId);
|
|
278
|
+
if (scoped) return scoped;
|
|
279
|
+
const current = defaults.chaos;
|
|
280
|
+
if (!current) return {};
|
|
281
|
+
return isChaosScope(current) ? current.base ?? {} : current;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
175
284
|
* Handle requests under `/__aimock/`. Returns `true` if the request was
|
|
176
285
|
* handled, `false` if the path doesn't match the control prefix.
|
|
177
286
|
*/
|
|
178
|
-
async function handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
|
|
287
|
+
async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
|
|
179
288
|
if (!pathname.startsWith(CONTROL_PREFIX)) return false;
|
|
180
289
|
const subPath = pathname.slice(9);
|
|
181
290
|
setCorsHeaders(res);
|
|
@@ -185,13 +294,148 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
|
|
|
185
294
|
return true;
|
|
186
295
|
}
|
|
187
296
|
if (subPath === "/journal" && req.method === "GET") {
|
|
188
|
-
|
|
189
|
-
|
|
297
|
+
const bad = (message) => {
|
|
298
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
299
|
+
res.end(JSON.stringify({ error: message }));
|
|
300
|
+
return true;
|
|
301
|
+
};
|
|
302
|
+
for (const key of searchParams.keys()) if (!JOURNAL_PARAMS.has(key)) return bad(`Unknown query parameter: '${key}'. Supported: ${[...JOURNAL_PARAMS].join(", ")}`);
|
|
303
|
+
const parseNonNegativeInt = (name, fallback) => {
|
|
304
|
+
const raw = searchParams.get(name);
|
|
305
|
+
if (raw === null) return fallback;
|
|
306
|
+
if (!/^\d+$/.test(raw.trim())) return null;
|
|
307
|
+
return Number(raw);
|
|
308
|
+
};
|
|
309
|
+
const limit = parseNonNegativeInt("limit", -1);
|
|
310
|
+
const offset = parseNonNegativeInt("offset", 0);
|
|
311
|
+
if (limit === null) return bad("Invalid 'limit': must be an integer >= 0");
|
|
312
|
+
if (offset === null) return bad("Invalid 'offset': must be an integer >= 0");
|
|
313
|
+
let statusFilter;
|
|
314
|
+
const statusRaw = searchParams.get("status");
|
|
315
|
+
if (statusRaw !== null) {
|
|
316
|
+
if (!/^\d+$/.test(statusRaw.trim())) return bad("Invalid 'status': must be an integer");
|
|
317
|
+
statusFilter = Number(statusRaw);
|
|
318
|
+
}
|
|
319
|
+
const pathFilter = searchParams.get("path");
|
|
320
|
+
const methodFilter = searchParams.get("method");
|
|
321
|
+
const serviceFilter = searchParams.get("service");
|
|
322
|
+
const testIdFilter = searchParams.get("testId");
|
|
323
|
+
let entries = journal.getAll();
|
|
324
|
+
if (methodFilter !== null) {
|
|
325
|
+
const want = methodFilter.toUpperCase();
|
|
326
|
+
entries = entries.filter((e) => e.method.toUpperCase() === want);
|
|
327
|
+
}
|
|
328
|
+
if (pathFilter !== null) entries = entries.filter((e) => e.path.includes(pathFilter));
|
|
329
|
+
if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
|
|
330
|
+
if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
|
|
331
|
+
if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
|
|
332
|
+
const total = entries.length;
|
|
333
|
+
if (offset > 0) entries = entries.slice(offset);
|
|
334
|
+
if (limit >= 0) entries = entries.slice(0, limit);
|
|
335
|
+
res.writeHead(200, {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
"X-Total-Count": String(total)
|
|
338
|
+
});
|
|
339
|
+
res.end(JSON.stringify(entries));
|
|
190
340
|
return true;
|
|
191
341
|
}
|
|
192
342
|
if (subPath === "/fixtures" && req.method === "GET") {
|
|
343
|
+
for (const key of searchParams.keys()) if (!FIXTURES_PARAMS.has(key)) {
|
|
344
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
345
|
+
res.end(JSON.stringify({ error: `Unknown query parameter: '${key}'. Supported: ${[...FIXTURES_PARAMS].join(", ")}` }));
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
const include = searchParams.get("include");
|
|
349
|
+
if (include === null) {
|
|
350
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
351
|
+
res.end(JSON.stringify({ count: fixtures.length }));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (include !== "fixtures") {
|
|
355
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
356
|
+
res.end(JSON.stringify({ error: "Invalid 'include': expected 'fixtures'" }));
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
360
|
+
res.end(JSON.stringify({
|
|
361
|
+
count: fixtures.length,
|
|
362
|
+
fixtures: fixtures.map((fixture, index) => ({
|
|
363
|
+
index,
|
|
364
|
+
match: redactForInspection(fixture.match),
|
|
365
|
+
responseKind: fixtureResponseKind(fixture.response),
|
|
366
|
+
...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
|
|
367
|
+
...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
|
|
368
|
+
}))
|
|
369
|
+
}));
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (subPath === "/chaos" && req.method === "GET") {
|
|
373
|
+
const scopeId = chaosScopeId(req);
|
|
374
|
+
if (scopeId === null) return writeBlankTestId(res);
|
|
375
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
376
|
+
res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (subPath === "/chaos" && req.method === "DELETE") {
|
|
380
|
+
const scopeId = chaosScopeId(req);
|
|
381
|
+
if (scopeId === null) return writeBlankTestId(res);
|
|
382
|
+
defaults.chaosByTestId?.delete(scopeId);
|
|
383
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
384
|
+
res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
if (subPath === "/chaos" && req.method === "POST") {
|
|
388
|
+
const scopeId = chaosScopeId(req);
|
|
389
|
+
if (scopeId === null) return writeBlankTestId(res);
|
|
390
|
+
let raw;
|
|
391
|
+
try {
|
|
392
|
+
raw = await readBody(req);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
395
|
+
defaults.logger.error(`POST /__aimock/chaos: failed to read body: ${msg}`);
|
|
396
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
397
|
+
res.end(JSON.stringify({ error: `Failed to read request body: ${msg}` }));
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = JSON.parse(raw);
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
405
|
+
defaults.logger.error(`POST /__aimock/chaos: invalid JSON: ${msg}`);
|
|
406
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
407
|
+
res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
411
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
412
|
+
res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
const allowed = [
|
|
416
|
+
"dropRate",
|
|
417
|
+
"malformedRate",
|
|
418
|
+
"disconnectRate"
|
|
419
|
+
];
|
|
420
|
+
for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
|
|
421
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
422
|
+
res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
const next = {};
|
|
426
|
+
for (const key of allowed) {
|
|
427
|
+
const value = parsed[key];
|
|
428
|
+
if (value === void 0) continue;
|
|
429
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) {
|
|
430
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
431
|
+
res.end(JSON.stringify({ error: `Invalid '${key}': must be a number between 0 and 1` }));
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
next[key] = value;
|
|
435
|
+
}
|
|
436
|
+
defaults.chaosByTestId?.set(scopeId, next);
|
|
193
437
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
194
|
-
res.end(JSON.stringify({
|
|
438
|
+
res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
|
|
195
439
|
return true;
|
|
196
440
|
}
|
|
197
441
|
if (subPath === "/fixtures" && req.method === "POST") {
|
|
@@ -300,8 +544,37 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
|
|
|
300
544
|
res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
|
|
301
545
|
return true;
|
|
302
546
|
}
|
|
547
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
548
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
549
|
+
res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
303
552
|
const status = parsed.status ?? 500;
|
|
553
|
+
if (!isInjectableStatus(status)) {
|
|
554
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
555
|
+
res.end(JSON.stringify({ error: `Invalid 'status': must be ${INJECTED_STATUS_RANGE}` }));
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
304
558
|
const errorBody = parsed.body;
|
|
559
|
+
if (errorBody !== void 0 && errorBody !== null) {
|
|
560
|
+
if (typeof errorBody !== "object" || Array.isArray(errorBody)) {
|
|
561
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
562
|
+
res.end(JSON.stringify({ error: "Invalid 'body': must be an object" }));
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
for (const field of [
|
|
566
|
+
"message",
|
|
567
|
+
"type",
|
|
568
|
+
"code"
|
|
569
|
+
]) {
|
|
570
|
+
const value = errorBody[field];
|
|
571
|
+
if (value !== void 0 && value !== null && typeof value !== "string") {
|
|
572
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
573
|
+
res.end(JSON.stringify({ error: `Invalid 'body.${field}': must be a string` }));
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
305
578
|
const errorFixture = {
|
|
306
579
|
match: { predicate: () => true },
|
|
307
580
|
response: {
|
|
@@ -361,7 +634,6 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
361
634
|
let body;
|
|
362
635
|
try {
|
|
363
636
|
body = JSON.parse(raw);
|
|
364
|
-
if (modelFallback && !body.model) body.model = modelFallback;
|
|
365
637
|
} catch (parseErr) {
|
|
366
638
|
const detail = parseErr instanceof Error ? parseErr.message : "unknown parse error";
|
|
367
639
|
journal.add({
|
|
@@ -382,6 +654,24 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
382
654
|
} }));
|
|
383
655
|
return;
|
|
384
656
|
}
|
|
657
|
+
if (!isJsonObject(body)) {
|
|
658
|
+
journal.add({
|
|
659
|
+
method: req.method ?? "POST",
|
|
660
|
+
path: req.url ?? COMPLETIONS_PATH,
|
|
661
|
+
headers: flattenHeaders(req.headers),
|
|
662
|
+
body: null,
|
|
663
|
+
response: {
|
|
664
|
+
status: 400,
|
|
665
|
+
fixture: null
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, "Request body must be a JSON object") : JSON.stringify({ error: {
|
|
669
|
+
message: "Request body must be a JSON object",
|
|
670
|
+
type: "invalid_request_error"
|
|
671
|
+
} }));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (modelFallback && !body.model) body.model = modelFallback;
|
|
385
675
|
if (!Array.isArray(body.messages)) {
|
|
386
676
|
journal.add({
|
|
387
677
|
method: req.method ?? "POST",
|
|
@@ -473,7 +763,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
473
763
|
const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
|
|
474
764
|
defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
|
|
475
765
|
}
|
|
476
|
-
const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger);
|
|
766
|
+
const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
477
767
|
const chaosContext = {
|
|
478
768
|
method,
|
|
479
769
|
path,
|
|
@@ -770,13 +1060,27 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
770
1060
|
const logger = new Logger(options?.logLevel ?? "silent");
|
|
771
1061
|
const registry = options?.metrics ? createMetricsRegistry() : void 0;
|
|
772
1062
|
const serverOptions = options ?? {};
|
|
1063
|
+
const chaosByTestId = /* @__PURE__ */ new Map();
|
|
773
1064
|
const defaults = {
|
|
774
1065
|
latency: serverOptions.latency ?? 0,
|
|
775
1066
|
chunkSize: Math.max(1, serverOptions.chunkSize ?? DEFAULT_CHUNK_SIZE),
|
|
776
1067
|
replaySpeed: serverOptions.replaySpeed ?? 1,
|
|
777
1068
|
logger,
|
|
1069
|
+
chaosByTestId,
|
|
778
1070
|
get chaos() {
|
|
779
|
-
return
|
|
1071
|
+
return {
|
|
1072
|
+
base: chaosByTestId.get(DEFAULT_TEST_ID) ?? serverOptions.chaos,
|
|
1073
|
+
byTestId: chaosByTestId
|
|
1074
|
+
};
|
|
1075
|
+
},
|
|
1076
|
+
set chaos(value) {
|
|
1077
|
+
if (value === void 0) {
|
|
1078
|
+
chaosByTestId.clear();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const base = isChaosScope(value) ? value.base : value;
|
|
1082
|
+
if (base === void 0) chaosByTestId.delete(DEFAULT_TEST_ID);
|
|
1083
|
+
else chaosByTestId.set(DEFAULT_TEST_ID, base);
|
|
780
1084
|
},
|
|
781
1085
|
registry,
|
|
782
1086
|
get record() {
|
|
@@ -933,7 +1237,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
933
1237
|
markAuthenticatedRequest(req, resolvedAuth.policy);
|
|
934
1238
|
}
|
|
935
1239
|
if (pathname.startsWith(CONTROL_PREFIX)) {
|
|
936
|
-
await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
|
|
1240
|
+
await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
|
|
937
1241
|
return;
|
|
938
1242
|
}
|
|
939
1243
|
if (mounts) {
|
|
@@ -1825,7 +2129,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1825
2129
|
try {
|
|
1826
2130
|
falBody = req.method === "POST" || req.method === "PUT" ? await readBody(req) : "";
|
|
1827
2131
|
const raw = falBody;
|
|
1828
|
-
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
|
|
2132
|
+
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
1829
2133
|
if (chaosAction) {
|
|
1830
2134
|
applyChaosAction(chaosAction, res, null, journal, {
|
|
1831
2135
|
method: req.method ?? "GET",
|
|
@@ -1867,7 +2171,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1867
2171
|
setCorsHeaders(res);
|
|
1868
2172
|
try {
|
|
1869
2173
|
const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await readBody(req) : "{}";
|
|
1870
|
-
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger);
|
|
2174
|
+
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
1871
2175
|
if (chaosAction) {
|
|
1872
2176
|
applyChaosAction(chaosAction, res, null, journal, {
|
|
1873
2177
|
method: req.method ?? "GET",
|