@copilotkit/aimock 1.37.4 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +48 -0
  4. package/README.md +50 -11
  5. package/dist/agui-handler.cjs +20 -18
  6. package/dist/agui-handler.cjs.map +1 -1
  7. package/dist/agui-handler.d.cts +8 -1
  8. package/dist/agui-handler.d.cts.map +1 -1
  9. package/dist/agui-handler.d.ts +8 -1
  10. package/dist/agui-handler.d.ts.map +1 -1
  11. package/dist/agui-handler.js +20 -18
  12. package/dist/agui-handler.js.map +1 -1
  13. package/dist/agui-recorder.cjs +6 -0
  14. package/dist/agui-recorder.cjs.map +1 -1
  15. package/dist/agui-recorder.d.cts.map +1 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +6 -0
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-stub.d.cts +2 -2
  20. package/dist/agui-stub.d.ts +2 -2
  21. package/dist/agui-types.d.cts +19 -1
  22. package/dist/agui-types.d.cts.map +1 -1
  23. package/dist/agui-types.d.ts +19 -1
  24. package/dist/agui-types.d.ts.map +1 -1
  25. package/dist/aimock-cli.cjs +1 -1
  26. package/dist/aimock-cli.cjs.map +1 -1
  27. package/dist/aimock-cli.js +1 -1
  28. package/dist/aimock-cli.js.map +1 -1
  29. package/dist/api-key-auth.cjs +166 -0
  30. package/dist/api-key-auth.cjs.map +1 -0
  31. package/dist/api-key-auth.d.cts +17 -0
  32. package/dist/api-key-auth.d.cts.map +1 -0
  33. package/dist/api-key-auth.d.ts +17 -0
  34. package/dist/api-key-auth.d.ts.map +1 -0
  35. package/dist/api-key-auth.js +158 -0
  36. package/dist/api-key-auth.js.map +1 -0
  37. package/dist/cli.cjs +14 -11
  38. package/dist/cli.cjs.map +1 -1
  39. package/dist/cli.js +14 -11
  40. package/dist/cli.js.map +1 -1
  41. package/dist/config-loader.cjs +4 -2
  42. package/dist/config-loader.cjs.map +1 -1
  43. package/dist/config-loader.d.cts +2 -1
  44. package/dist/config-loader.d.cts.map +1 -1
  45. package/dist/config-loader.d.ts +2 -1
  46. package/dist/config-loader.d.ts.map +1 -1
  47. package/dist/config-loader.js +5 -3
  48. package/dist/config-loader.js.map +1 -1
  49. package/dist/fal-audio.cjs +2 -0
  50. package/dist/fal-audio.cjs.map +1 -1
  51. package/dist/fal-audio.d.cts.map +1 -1
  52. package/dist/fal-audio.d.ts.map +1 -1
  53. package/dist/fal-audio.js +2 -0
  54. package/dist/fal-audio.js.map +1 -1
  55. package/dist/fal.cjs +4 -1
  56. package/dist/fal.cjs.map +1 -1
  57. package/dist/fal.d.cts.map +1 -1
  58. package/dist/fal.d.ts.map +1 -1
  59. package/dist/fal.js +4 -1
  60. package/dist/fal.js.map +1 -1
  61. package/dist/fixture-loader.cjs +0 -0
  62. package/dist/fixture-loader.cjs.map +1 -1
  63. package/dist/fixture-loader.d.cts +3 -1
  64. package/dist/fixture-loader.d.cts.map +1 -1
  65. package/dist/fixture-loader.d.ts +3 -1
  66. package/dist/fixture-loader.d.ts.map +1 -1
  67. package/dist/fixture-loader.js +0 -0
  68. package/dist/fixture-loader.js.map +1 -1
  69. package/dist/grok-video.cjs +9 -2
  70. package/dist/grok-video.cjs.map +1 -1
  71. package/dist/grok-video.js +10 -3
  72. package/dist/grok-video.js.map +1 -1
  73. package/dist/helpers.cjs +16 -22
  74. package/dist/helpers.cjs.map +1 -1
  75. package/dist/helpers.d.cts.map +1 -1
  76. package/dist/helpers.d.ts.map +1 -1
  77. package/dist/helpers.js +16 -21
  78. package/dist/helpers.js.map +1 -1
  79. package/dist/index.d.cts +3 -3
  80. package/dist/index.d.ts +3 -3
  81. package/dist/llmock.cjs +37 -12
  82. package/dist/llmock.cjs.map +1 -1
  83. package/dist/llmock.d.cts +26 -2
  84. package/dist/llmock.d.cts.map +1 -1
  85. package/dist/llmock.d.ts +26 -2
  86. package/dist/llmock.d.ts.map +1 -1
  87. package/dist/llmock.js +39 -15
  88. package/dist/llmock.js.map +1 -1
  89. package/dist/openrouter-chat.cjs +333 -0
  90. package/dist/openrouter-chat.cjs.map +1 -0
  91. package/dist/openrouter-chat.js +325 -0
  92. package/dist/openrouter-chat.js.map +1 -0
  93. package/dist/openrouter-video.cjs +24 -7
  94. package/dist/openrouter-video.cjs.map +1 -1
  95. package/dist/openrouter-video.d.cts.map +1 -1
  96. package/dist/openrouter-video.d.ts.map +1 -1
  97. package/dist/openrouter-video.js +25 -8
  98. package/dist/openrouter-video.js.map +1 -1
  99. package/dist/provider-auth.cjs +20 -0
  100. package/dist/provider-auth.cjs.map +1 -1
  101. package/dist/provider-auth.js +20 -1
  102. package/dist/provider-auth.js.map +1 -1
  103. package/dist/recorder.cjs +50 -8
  104. package/dist/recorder.cjs.map +1 -1
  105. package/dist/recorder.d.cts.map +1 -1
  106. package/dist/recorder.d.ts.map +1 -1
  107. package/dist/recorder.js +50 -10
  108. package/dist/recorder.js.map +1 -1
  109. package/dist/responses.cjs +200 -89
  110. package/dist/responses.cjs.map +1 -1
  111. package/dist/responses.d.cts +18 -3
  112. package/dist/responses.d.cts.map +1 -1
  113. package/dist/responses.d.ts +18 -3
  114. package/dist/responses.d.ts.map +1 -1
  115. package/dist/responses.js +199 -90
  116. package/dist/responses.js.map +1 -1
  117. package/dist/server.cjs +190 -83
  118. package/dist/server.cjs.map +1 -1
  119. package/dist/server.d.cts +7 -2
  120. package/dist/server.d.cts.map +1 -1
  121. package/dist/server.d.ts +7 -2
  122. package/dist/server.d.ts.map +1 -1
  123. package/dist/server.js +190 -85
  124. package/dist/server.js.map +1 -1
  125. package/dist/sse-writer.cjs +3 -0
  126. package/dist/sse-writer.cjs.map +1 -1
  127. package/dist/sse-writer.d.cts +8 -0
  128. package/dist/sse-writer.d.cts.map +1 -1
  129. package/dist/sse-writer.d.ts +8 -0
  130. package/dist/sse-writer.d.ts.map +1 -1
  131. package/dist/sse-writer.js +3 -0
  132. package/dist/sse-writer.js.map +1 -1
  133. package/dist/stream-collapse.cjs +103 -0
  134. package/dist/stream-collapse.cjs.map +1 -1
  135. package/dist/stream-collapse.d.cts +7 -0
  136. package/dist/stream-collapse.d.cts.map +1 -1
  137. package/dist/stream-collapse.d.ts +7 -0
  138. package/dist/stream-collapse.d.ts.map +1 -1
  139. package/dist/stream-collapse.js +103 -0
  140. package/dist/stream-collapse.js.map +1 -1
  141. package/dist/transcription.cjs +49 -2
  142. package/dist/transcription.cjs.map +1 -1
  143. package/dist/transcription.d.cts.map +1 -1
  144. package/dist/transcription.d.ts.map +1 -1
  145. package/dist/transcription.js +50 -3
  146. package/dist/transcription.js.map +1 -1
  147. package/dist/types.d.cts +187 -5
  148. package/dist/types.d.cts.map +1 -1
  149. package/dist/types.d.ts +187 -5
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/vector-types.d.ts.map +1 -1
  152. package/dist/veo-video.cjs +9 -2
  153. package/dist/veo-video.cjs.map +1 -1
  154. package/dist/veo-video.js +10 -3
  155. package/dist/veo-video.js.map +1 -1
  156. package/dist/ws-gemini-live.cjs +41 -6
  157. package/dist/ws-gemini-live.cjs.map +1 -1
  158. package/dist/ws-gemini-live.js +41 -6
  159. package/dist/ws-gemini-live.js.map +1 -1
  160. package/dist/ws-realtime.cjs +306 -57
  161. package/dist/ws-realtime.cjs.map +1 -1
  162. package/dist/ws-realtime.d.cts +1 -0
  163. package/dist/ws-realtime.d.cts.map +1 -1
  164. package/dist/ws-realtime.d.ts +1 -0
  165. package/dist/ws-realtime.d.ts.map +1 -1
  166. package/dist/ws-realtime.js +307 -58
  167. package/dist/ws-realtime.js.map +1 -1
  168. package/dist/ws-responses.cjs +11 -6
  169. package/dist/ws-responses.cjs.map +1 -1
  170. package/dist/ws-responses.d.cts.map +1 -1
  171. package/dist/ws-responses.d.ts.map +1 -1
  172. package/dist/ws-responses.js +12 -7
  173. package/dist/ws-responses.js.map +1 -1
  174. package/package.json +3 -2
  175. package/skills/write-fixtures/SKILL.md +53 -19
package/dist/server.d.cts CHANGED
@@ -20,6 +20,12 @@ interface ServerInstance {
20
20
  veoVideoJobs: VeoVideoJobMap;
21
21
  grokVideoJobs: GrokVideoJobMap;
22
22
  }
23
+ /**
24
+ * The per-server state a full reset clears. `ServerInstance` structurally
25
+ * satisfies this, so `LLMock.reset()` and the control-API full-reset route
26
+ * share a single definition of "everything" instead of two lists that drift.
27
+ */
28
+
23
29
  interface ServiceFixtures {
24
30
  search: SearchFixture[];
25
31
  rerank: RerankFixture[];
@@ -29,8 +35,7 @@ declare function createServer(fixtures: Fixture[], options?: MockServerOptions,
29
35
  path: string;
30
36
  handler: Mountable;
31
37
  }>, serviceFixtures?: ServiceFixtures): Promise<ServerInstance>;
32
- //# sourceMappingURL=server.d.ts.map
33
-
38
+ /** @internal Config startup uses this to avoid parsing a selected auth source twice. */
34
39
  //#endregion
35
40
  export { ServerInstance, ServiceFixtures, createServer };
36
41
  //# sourceMappingURL=server.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UA+FiB,cAAA;UACP,MAAA,CAAK;EADE,OAAA,EAEN,OAFM;EAAc,GAAA,EAAA,MAAA;UACrB,EAGE,eAHG;aACJ,EAGI,aAHJ;qBAEC,EAEW,qBAFX;cACG,EAEC,cAFD;eACQ,EAEN,eAFM;;AAEN,UAq+BA,eAAA,CAr+BA;EAAe,MAAA,EAs+BtB,aAt+BsB,EAAA;EAq+Bf,MAAA,EAEP,aAFsB,EAAA;EAAA,UAAA,EAGlB,iBAHkB,EAAA;;AAEtB,iBAOY,YAAA,CAPZ,QAAA,EAQE,OARF,EAAA,EAAA,OAAA,CAAA,EASE,iBATF,EAAA,MAOV,CAPU,EAUC,KAVD,CAAA;MACI,EAAA,MAAA;EAAiB,OAAA,EASW,SATX;AAM/B,CAAA,CAAA,EAAsB,eAAY,CAAA,EAId,eAJc,CAAA,EAK/B,OAL+B,CAKvB,cALuB,CAAA"}
1
+ {"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UAyHiB,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;;;AAorChB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
package/dist/server.d.ts CHANGED
@@ -20,6 +20,12 @@ interface ServerInstance {
20
20
  veoVideoJobs: VeoVideoJobMap;
21
21
  grokVideoJobs: GrokVideoJobMap;
22
22
  }
23
+ /**
24
+ * The per-server state a full reset clears. `ServerInstance` structurally
25
+ * satisfies this, so `LLMock.reset()` and the control-API full-reset route
26
+ * share a single definition of "everything" instead of two lists that drift.
27
+ */
28
+
23
29
  interface ServiceFixtures {
24
30
  search: SearchFixture[];
25
31
  rerank: RerankFixture[];
@@ -29,8 +35,7 @@ declare function createServer(fixtures: Fixture[], options?: MockServerOptions,
29
35
  path: string;
30
36
  handler: Mountable;
31
37
  }>, serviceFixtures?: ServiceFixtures): Promise<ServerInstance>;
32
- //# sourceMappingURL=server.d.ts.map
33
-
38
+ /** @internal Config startup uses this to avoid parsing a selected auth source twice. */
34
39
  //#endregion
35
40
  export { ServerInstance, ServiceFixtures, createServer };
36
41
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UA+FiB,cAAA;UACP,MAAA,CAAK;EADE,OAAA,EAEN,OAFM;EAAc,GAAA,EAAA,MAAA;UACrB,EAGE,eAHG;aACJ,EAGI,aAHJ;qBAEC,EAEW,qBAFX;cACG,EAEC,cAFD;eACQ,EAEN,eAFM;;AAEN,UAq+BA,eAAA,CAr+BA;EAAe,MAAA,EAs+BtB,aAt+BsB,EAAA;EAq+Bf,MAAA,EAEP,aAFsB,EAAA;EAAA,UAAA,EAGlB,iBAHkB,EAAA;;AAEtB,iBAOY,YAAA,CAPZ,QAAA,EAQE,OARF,EAAA,EAAA,OAAA,CAAA,EASE,iBATF,EAAA,MAOV,CAPU,EAUC,KAVD,CAAA;MACI,EAAA,MAAA;EAAiB,OAAA,EASW,SATX;AAM/B,CAAA,CAAA,EAAsB,eAAY,CAAA,EAId,eAJc,CAAA,EAK/B,OAL+B,CAKvB,cALuB,CAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UAyHiB,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;;;AAorChB;;;;;UAAiB,eAAA;UACP;UACA;cACI;;iBAMQ,YAAA,WACV,qBACA,4BACD;;WAA+B;sBACtB,kBACjB,QAAQ"}
package/dist/server.js CHANGED
@@ -1,10 +1,12 @@
1
- import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, estimatePromptTokens, estimateTokens, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, readBody, resolveReasoningForModel, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
1
+ 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";
2
3
  import { Logger } from "./logger.js";
3
4
  import { Journal } from "./journal.js";
4
5
  import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
5
6
  import { entryToFixture, validateFixtures } from "./fixture-loader.js";
6
7
  import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
7
8
  import { createInterruptionSignal } from "./interruption.js";
9
+ import { buildOpenRouterCandidates, handleOpenRouterCredits, handleOpenRouterKey, handleOpenRouterModels, isOpenRouterPath, resolveOpenRouterShaping, serializeOpenRouterError, shapeOpenRouterChunks, shapeOpenRouterCompletion } from "./openrouter-chat.js";
8
10
  import { applyChaosAction, evaluateChaos } from "./chaos.js";
9
11
  import { proxyAndRecord } from "./recorder.js";
10
12
  import { handleResponses } from "./responses.js";
@@ -141,23 +143,31 @@ function handleNotFound(res, message) {
141
143
  }
142
144
  const CONTROL_PREFIX = "/__aimock";
143
145
  /**
144
- * Perform a full fixtures reset: clear the fixtures array, journal, video/fal
145
- * generation state, and the interaction/event-id counters, then zero the
146
- * `aimock_fixtures_loaded` gauge. Shared by `/reset/fixtures` and the
147
- * deprecated `/reset` alias.
146
+ * Perform a full reset: clear the fixtures array, the journal (entries *and*
147
+ * per-test fixture match-counts, i.e. sequence position), the video and fal.ai
148
+ * job/queue state, and the Gemini interaction/event-id counters, then re-zero
149
+ * the `aimock_fixtures_loaded` gauge.
150
+ *
151
+ * `targets` is `null` when no server is running (an in-process `reset()` before
152
+ * `start()`). The process-global generation state is reset either way, since it
153
+ * is not owned by any one server instance.
154
+ *
155
+ * Shared by `POST /__aimock/reset` (canonical), its deprecated
156
+ * `POST /__aimock/reset/fixtures` alias, and `LLMock.reset()`.
148
157
  */
149
- function performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults) {
158
+ function performFullReset(fixtures, targets) {
150
159
  fixtures.length = 0;
151
- journal.clear();
152
- videoStates.clear();
153
- openRouterVideoJobs.clear();
154
- veoVideoJobs.clear();
155
- grokVideoJobs.clear();
156
160
  falJobs.clear();
157
161
  falQueueStates.clear();
158
162
  resetInteractionCounter();
159
163
  resetEventIdCounter();
160
- if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
164
+ if (!targets) return;
165
+ targets.journal.clear();
166
+ targets.videoStates.clear();
167
+ targets.openRouterVideoJobs.clear();
168
+ targets.veoVideoJobs.clear();
169
+ targets.grokVideoJobs.clear();
170
+ if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
161
171
  }
162
172
  /**
163
173
  * Handle requests under `/__aimock/`. Returns `true` if the request was
@@ -226,8 +236,16 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
226
236
  res.end(JSON.stringify({ cleared: true }));
227
237
  return true;
228
238
  }
229
- if (subPath === "/reset/fixtures" && req.method === "POST") {
230
- performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults);
239
+ const resetTargets = () => ({
240
+ journal,
241
+ videoStates,
242
+ openRouterVideoJobs,
243
+ veoVideoJobs,
244
+ grokVideoJobs,
245
+ defaults
246
+ });
247
+ if (subPath === "/reset" && req.method === "POST") {
248
+ performFullReset(fixtures, resetTargets());
231
249
  res.writeHead(200, { "Content-Type": "application/json" });
232
250
  res.end(JSON.stringify({ reset: true }));
233
251
  return true;
@@ -238,10 +256,10 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
238
256
  res.end(JSON.stringify({ reset: true }));
239
257
  return true;
240
258
  }
241
- if (subPath === "/reset" && req.method === "POST") {
242
- performFixturesReset(fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults);
243
- const deprecation = "POST /__aimock/reset is deprecated; use POST /__aimock/reset/fixtures (full reset) or POST /__aimock/reset/journal (journal only)";
244
- defaults.logger.warn("POST /__aimock/reset is deprecated; use /__aimock/reset/fixtures or /__aimock/reset/journal");
259
+ if (subPath === "/reset/fixtures" && req.method === "POST") {
260
+ performFullReset(fixtures, resetTargets());
261
+ const deprecation = "POST /__aimock/reset/fixtures is deprecated; use POST /__aimock/reset (full reset) or POST /__aimock/reset/journal (journal only)";
262
+ defaults.logger.warn("POST /__aimock/reset/fixtures is deprecated; use /__aimock/reset or /__aimock/reset/journal");
245
263
  res.writeHead(200, {
246
264
  "Content-Type": "application/json",
247
265
  Deprecation: "true"
@@ -309,7 +327,7 @@ async function handleControlAPI(req, res, pathname, fixtures, journal, videoStat
309
327
  handleNotFound(res, `Unknown control endpoint: ${pathname}`);
310
328
  return true;
311
329
  }
312
- async function handleCompletions(req, res, fixtures, journal, defaults, modelFallback, providerKey) {
330
+ async function handleCompletions(req, res, fixtures, journal, defaults, modelFallback, providerKey, openRouter = false) {
313
331
  setCorsHeaders(res);
314
332
  let raw;
315
333
  try {
@@ -326,7 +344,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
326
344
  fixture: null
327
345
  }
328
346
  });
329
- writeErrorResponse(res, 500, JSON.stringify({ error: {
347
+ writeErrorResponse(res, 500, openRouter ? serializeOpenRouterError(500, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
330
348
  message: `Request body read failed: ${msg}`,
331
349
  type: "server_error"
332
350
  } }));
@@ -348,7 +366,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
348
366
  fixture: null
349
367
  }
350
368
  });
351
- writeErrorResponse(res, 400, JSON.stringify({ error: {
369
+ writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, `Malformed JSON: ${detail}`) : JSON.stringify({ error: {
352
370
  message: `Malformed JSON: ${detail}`,
353
371
  type: "invalid_request_error",
354
372
  param: null,
@@ -367,7 +385,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
367
385
  fixture: null
368
386
  }
369
387
  });
370
- writeErrorResponse(res, 400, JSON.stringify({ error: {
388
+ writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, "Missing required parameter: 'messages'") : JSON.stringify({ error: {
371
389
  message: "Missing required parameter: 'messages'",
372
390
  type: "invalid_request_error",
373
391
  param: null,
@@ -381,9 +399,66 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
381
399
  body._endpointType = "chat";
382
400
  body._context = getContext(req);
383
401
  const testId = getTestId(req);
384
- const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, recordMatchOptions(!!(defaults.record && providerKey), defaults.logger));
402
+ const matchOptions = recordMatchOptions(!!(defaults.record && providerKey), defaults.logger);
403
+ let fixture = null;
404
+ let skippedBySequenceOrTurn = 0;
405
+ let preResolvedResponse = null;
406
+ let responseModel = body.model;
407
+ let fixtureCountIncremented = false;
408
+ if (openRouter && Array.isArray(body.models) && body.models.length > 0) {
409
+ const candidates = buildOpenRouterCandidates(body);
410
+ let matchCounts = journal.getFixtureMatchCountsForTest(testId);
411
+ const allowFallbacks = body.provider?.allow_fallbacks !== false;
412
+ let lastErrorFixture = null;
413
+ let lastErrorResponse = null;
414
+ const resolveCandidate = async (candidate) => {
415
+ const probe = {
416
+ ...body,
417
+ model: candidate
418
+ };
419
+ const attempt = matchFixtureDiagnostic(fixtures, probe, matchCounts, defaults.requestTransform, matchOptions);
420
+ skippedBySequenceOrTurn = Math.max(skippedBySequenceOrTurn, attempt.skippedBySequenceOrTurn);
421
+ if (!attempt.fixture) return null;
422
+ const response = await resolveResponse(attempt.fixture, probe);
423
+ return {
424
+ fixture: attempt.fixture,
425
+ response,
426
+ isError: isErrorResponse(response)
427
+ };
428
+ };
429
+ for (const candidate of candidates) {
430
+ const outcome = await resolveCandidate(candidate);
431
+ if (!outcome) {
432
+ if (!allowFallbacks) break;
433
+ continue;
434
+ }
435
+ journal.incrementFixtureMatchCount(outcome.fixture, fixtures, testId);
436
+ fixtureCountIncremented = true;
437
+ matchCounts = journal.getFixtureMatchCountsForTest(testId);
438
+ responseModel = candidate;
439
+ if (outcome.isError) {
440
+ const errorFallsThrough = outcome.response.fallthrough !== false;
441
+ if (allowFallbacks && errorFallsThrough) {
442
+ lastErrorFixture = outcome.fixture;
443
+ lastErrorResponse = outcome.response;
444
+ continue;
445
+ }
446
+ }
447
+ fixture = outcome.fixture;
448
+ preResolvedResponse = outcome.response;
449
+ break;
450
+ }
451
+ if (!fixture && lastErrorFixture) {
452
+ fixture = lastErrorFixture;
453
+ preResolvedResponse = lastErrorResponse;
454
+ }
455
+ } else {
456
+ const single = matchFixtureDiagnostic(fixtures, body, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform, matchOptions);
457
+ fixture = single.fixture;
458
+ skippedBySequenceOrTurn = single.skippedBySequenceOrTurn;
459
+ }
385
460
  if (fixture) {
386
- journal.incrementFixtureMatchCount(fixture, fixtures, testId);
461
+ if (!fixtureCountIncremented) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
387
462
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
388
463
  } else {
389
464
  const lastUserMsg = body.messages.filter((m) => m.role === "user").pop();
@@ -421,7 +496,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
421
496
  ...strictOverrideField(defaults.strict, req.headers)
422
497
  }
423
498
  });
424
- writeErrorResponse(res, strictStatus, JSON.stringify({ error: {
499
+ writeErrorResponse(res, strictStatus, openRouter ? serializeOpenRouterError(strictStatus, strictMessage) : JSON.stringify({ error: {
425
500
  message: strictMessage,
426
501
  type: "invalid_request_error",
427
502
  param: null,
@@ -472,7 +547,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
472
547
  ...strictOverrideField(defaults.strict, req.headers)
473
548
  }
474
549
  });
475
- writeErrorResponse(res, 404, JSON.stringify({ error: {
550
+ writeErrorResponse(res, 404, openRouter ? serializeOpenRouterError(404, "No fixture matched") : JSON.stringify({ error: {
476
551
  message: "No fixture matched",
477
552
  type: "invalid_request_error",
478
553
  param: null,
@@ -480,10 +555,23 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
480
555
  } }));
481
556
  return;
482
557
  }
483
- const response = await resolveResponse(fixture, body);
558
+ const response = preResolvedResponse ?? await resolveResponse(fixture, body);
484
559
  const latency = fixture.latency ?? defaults.latency;
485
560
  const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
486
- const includeUsage = body.stream === true && body.stream_options?.include_usage === true;
561
+ const emitStreamingUsage = body.stream === true && body.stream_options?.include_usage === true || openRouter;
562
+ const streamingPromptText = body.messages.map((m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "").join("");
563
+ const resolveStreamingUsageTokens = (overrides, completionText) => resolveUsage(overrides, streamingPromptText, completionText);
564
+ const shapeORCompletion = (completion, overrides) => {
565
+ if (!openRouter) return completion;
566
+ return shapeOpenRouterCompletion(completion, resolveOpenRouterShaping(overrides, overrides?.model ?? responseModel), overrides?.id !== void 0);
567
+ };
568
+ const shapeORChunks = (chunks, usageChunk, overrides) => {
569
+ if (!openRouter) return;
570
+ const shaping = resolveOpenRouterShaping(overrides, overrides?.model ?? responseModel);
571
+ shapeOpenRouterChunks(chunks, shaping, overrides?.id !== void 0);
572
+ if (usageChunk) shapeOpenRouterChunks([usageChunk], shaping, overrides?.id !== void 0);
573
+ };
574
+ const openRouterProcessing = !!(openRouter && fixture.openRouterProcessing);
487
575
  if (isErrorResponse(response)) {
488
576
  const status = response.status ?? 500;
489
577
  journal.add({
@@ -496,7 +584,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
496
584
  fixture
497
585
  }
498
586
  });
499
- writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
587
+ writeErrorResponse(res, status, openRouter ? serializeOpenRouterError(status, response.error.message, response.error.metadata) : serializeErrorResponse(response), { retryAfter: response.retryAfter });
500
588
  return;
501
589
  }
502
590
  if (isAudioResponse(response)) {
@@ -510,7 +598,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
510
598
  fixture
511
599
  }
512
600
  });
513
- writeErrorResponse(res, 422, JSON.stringify({ error: {
601
+ writeErrorResponse(res, 422, openRouter ? serializeOpenRouterError(422, "Audio responses are not supported on the chat completions endpoint. Use Gemini generateContent or a dedicated audio endpoint.") : JSON.stringify({ error: {
514
602
  message: "Audio responses are not supported on the chat completions endpoint. Use Gemini generateContent or a dedicated audio endpoint.",
515
603
  type: "invalid_request_error"
516
604
  } }));
@@ -520,7 +608,9 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
520
608
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
521
609
  const overrides = extractOverrides(response);
522
610
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
523
- const effReasoning = resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
611
+ const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
612
+ const streaming = body.stream === true;
613
+ const streamingBlocks = streaming && response.blocks && response.blocks.length > 0 ? resolveFixtureBlocks(response.blocks) : void 0;
524
614
  const journalEntry = journal.add({
525
615
  method: req.method ?? "POST",
526
616
  path: req.url ?? COMPLETIONS_PATH,
@@ -531,22 +621,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
531
621
  fixture
532
622
  }
533
623
  });
534
- if (body.stream !== true) {
535
- const completion = buildContentWithToolCallsCompletion(response.content ?? "", response.toolCalls ?? [], body.model, effReasoning, overrides, body.messages);
624
+ if (!streaming) {
625
+ const completion = buildContentWithToolCallsCompletion(response.content ?? "", response.toolCalls ?? [], responseModel, effReasoning, overrides, body.messages);
536
626
  res.writeHead(200, { "Content-Type": "application/json" });
537
- res.end(JSON.stringify(completion));
627
+ res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
538
628
  } else {
539
- const chunks = buildContentWithToolCallsChunks(response.content ?? "", response.toolCalls ?? [], body.model, chunkSize, effReasoning, overrides, response.blocks);
540
- const completionText = (response.content ?? "") + (response.toolCalls ?? []).map((tc) => tc.name + tc.arguments).join("");
541
- const usageChunk = includeUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
542
- prompt_tokens: overrides.usage.prompt_tokens ?? 0,
543
- completion_tokens: overrides.usage.completion_tokens ?? 0,
544
- total_tokens: overrides.usage.total_tokens ?? (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0)
545
- } : {
546
- prompt_tokens: estimatePromptTokens(body.messages),
547
- completion_tokens: estimateTokens(completionText),
548
- total_tokens: estimatePromptTokens(body.messages) + estimateTokens(completionText)
549
- }, overrides?.systemFingerprint) : void 0;
629
+ const chunks = buildContentWithToolCallsChunks(response.content ?? "", response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides, streamingBlocks);
630
+ const completionText = streamingBlocks ? streamingBlocks.map((b) => b.type === "text" ? b.text : b.name + b.arguments).join("") : (response.content ?? "") + (response.toolCalls ?? []).map((tc) => tc.name + tc.arguments).join("");
631
+ const usageChunk = emitStreamingUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? responseModel, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), resolveStreamingUsageTokens(overrides, completionText), overrides?.systemFingerprint) : void 0;
632
+ shapeORChunks(chunks, usageChunk, overrides);
550
633
  const interruption = createInterruptionSignal(fixture);
551
634
  if (!await writeSSEStream(res, chunks, {
552
635
  latency,
@@ -555,7 +638,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
555
638
  onChunkSent: interruption?.tick,
556
639
  usageChunk,
557
640
  recordedTimings: fixture.recordedTimings,
558
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed
641
+ replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
642
+ openRouterProcessing
559
643
  })) {
560
644
  if (!res.writableEnded) res.destroy();
561
645
  journalEntry.response.interrupted = true;
@@ -569,7 +653,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
569
653
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
570
654
  const overrides = extractOverrides(response);
571
655
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
572
- const effReasoning = resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
656
+ const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
573
657
  const journalEntry = journal.add({
574
658
  method: req.method ?? "POST",
575
659
  path: req.url ?? COMPLETIONS_PATH,
@@ -581,20 +665,13 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
581
665
  }
582
666
  });
583
667
  if (body.stream !== true) {
584
- const completion = buildTextCompletion(response.content, body.model, effReasoning, overrides, body.messages);
668
+ const completion = buildTextCompletion(response.content, responseModel, effReasoning, overrides, body.messages);
585
669
  res.writeHead(200, { "Content-Type": "application/json" });
586
- res.end(JSON.stringify(completion));
670
+ res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
587
671
  } else {
588
- const chunks = buildTextChunks(response.content, body.model, chunkSize, effReasoning, overrides);
589
- const usageChunk = includeUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
590
- prompt_tokens: overrides.usage.prompt_tokens ?? 0,
591
- completion_tokens: overrides.usage.completion_tokens ?? 0,
592
- total_tokens: overrides.usage.total_tokens ?? (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0)
593
- } : {
594
- prompt_tokens: estimatePromptTokens(body.messages),
595
- completion_tokens: estimateTokens(response.content),
596
- total_tokens: estimatePromptTokens(body.messages) + estimateTokens(response.content)
597
- }, overrides?.systemFingerprint) : void 0;
672
+ const chunks = buildTextChunks(response.content, responseModel, chunkSize, effReasoning, overrides);
673
+ const usageChunk = emitStreamingUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? responseModel, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), resolveStreamingUsageTokens(overrides, response.content), overrides?.systemFingerprint) : void 0;
674
+ shapeORChunks(chunks, usageChunk, overrides);
598
675
  const interruption = createInterruptionSignal(fixture);
599
676
  if (!await writeSSEStream(res, chunks, {
600
677
  latency,
@@ -603,7 +680,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
603
680
  onChunkSent: interruption?.tick,
604
681
  usageChunk,
605
682
  recordedTimings: fixture.recordedTimings,
606
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed
683
+ replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
684
+ openRouterProcessing
607
685
  })) {
608
686
  if (!res.writableEnded) res.destroy();
609
687
  journalEntry.response.interrupted = true;
@@ -617,7 +695,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
617
695
  if (response.webSearches?.length) defaults.logger.warn("webSearches in fixture response are not supported for Chat Completions API — ignoring");
618
696
  const overrides = extractOverrides(response);
619
697
  const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
620
- const effReasoning = resolveReasoningForModel(response.reasoning, body.model, effectiveStrict, defaults.logger);
698
+ const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
621
699
  const journalEntry = journal.add({
622
700
  method: req.method ?? "POST",
623
701
  path: req.url ?? COMPLETIONS_PATH,
@@ -629,21 +707,14 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
629
707
  }
630
708
  });
631
709
  if (body.stream !== true) {
632
- const completion = buildToolCallCompletion(response.toolCalls, body.model, effReasoning, overrides, body.messages);
710
+ const completion = buildToolCallCompletion(response.toolCalls, responseModel, effReasoning, overrides, body.messages);
633
711
  res.writeHead(200, { "Content-Type": "application/json" });
634
- res.end(JSON.stringify(completion));
712
+ res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
635
713
  } else {
636
- const chunks = buildToolCallChunks(response.toolCalls, body.model, chunkSize, effReasoning, overrides);
714
+ const chunks = buildToolCallChunks(response.toolCalls, responseModel, chunkSize, effReasoning, overrides);
637
715
  const completionText = response.toolCalls.map((tc) => tc.name + tc.arguments).join("");
638
- const usageChunk = includeUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? body.model, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), overrides?.usage ? {
639
- prompt_tokens: overrides.usage.prompt_tokens ?? 0,
640
- completion_tokens: overrides.usage.completion_tokens ?? 0,
641
- total_tokens: overrides.usage.total_tokens ?? (overrides.usage.prompt_tokens ?? 0) + (overrides.usage.completion_tokens ?? 0)
642
- } : {
643
- prompt_tokens: estimatePromptTokens(body.messages),
644
- completion_tokens: estimateTokens(completionText),
645
- total_tokens: estimatePromptTokens(body.messages) + estimateTokens(completionText)
646
- }, overrides?.systemFingerprint) : void 0;
716
+ const usageChunk = emitStreamingUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? responseModel, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), resolveStreamingUsageTokens(overrides, completionText), overrides?.systemFingerprint) : void 0;
717
+ shapeORChunks(chunks, usageChunk, overrides);
647
718
  const interruption = createInterruptionSignal(fixture);
648
719
  if (!await writeSSEStream(res, chunks, {
649
720
  latency,
@@ -652,7 +723,8 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
652
723
  onChunkSent: interruption?.tick,
653
724
  usageChunk,
654
725
  recordedTimings: fixture.recordedTimings,
655
- replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed
726
+ replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
727
+ openRouterProcessing
656
728
  })) {
657
729
  if (!res.writableEnded) res.destroy();
658
730
  journalEntry.response.interrupted = true;
@@ -672,12 +744,19 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
672
744
  fixture
673
745
  }
674
746
  });
675
- writeErrorResponse(res, 500, JSON.stringify({ error: {
747
+ writeErrorResponse(res, 500, openRouter ? serializeOpenRouterError(500, "Fixture response did not match any known type") : JSON.stringify({ error: {
676
748
  message: "Fixture response did not match any known type",
677
749
  type: "server_error"
678
750
  } }));
679
751
  }
680
752
  async function createServer(fixtures, options, mounts, serviceFixtures) {
753
+ return createServerWithResolvedAuth(fixtures, options, resolveInboundAuth({
754
+ value: options?.auth,
755
+ label: "options.auth"
756
+ }), mounts, serviceFixtures);
757
+ }
758
+ /** @internal Config startup uses this to avoid parsing a selected auth source twice. */
759
+ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mounts, serviceFixtures) {
681
760
  const host = options?.host ?? "127.0.0.1";
682
761
  const port = options?.port ?? 0;
683
762
  const logger = new Logger(options?.logLevel ?? "silent");
@@ -791,13 +870,10 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
791
870
  });
792
871
  });
793
872
  async function handleHttpRequest(req, res) {
794
- if (req.method === "OPTIONS") {
795
- handleOptions(res);
796
- return;
797
- }
798
873
  const startTime = registry ? process.hrtime.bigint() : 0n;
799
874
  const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
800
875
  let pathname = parsedUrl.pathname;
876
+ const isOpenRouter = isOpenRouterPath(pathname);
801
877
  if (registry) res.on("finish", () => {
802
878
  try {
803
879
  const normalizedPath = normalizePathLabel(pathname);
@@ -817,6 +893,17 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
817
893
  defaults.logger.warn("metrics instrumentation error", err);
818
894
  }
819
895
  });
896
+ if (req.method === "OPTIONS" && (!resolvedAuth.policy.enabled || req.headers.origin !== void 0 && req.headers["access-control-request-method"] !== void 0)) {
897
+ handleOptions(res);
898
+ return;
899
+ }
900
+ if (!(req.method === "GET" && (pathname === HEALTH_PATH || pathname === READY_PATH || pathname === "/metrics"))) {
901
+ if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
902
+ writeApiKeyHttpRejection(res, setCorsHeaders);
903
+ return;
904
+ }
905
+ markAuthenticatedRequest(req, resolvedAuth.policy);
906
+ }
820
907
  if (pathname.startsWith(CONTROL_PREFIX)) {
821
908
  await handleControlAPI(req, res, pathname, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, defaults);
822
909
  return;
@@ -1001,6 +1088,18 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1001
1088
  }
1002
1089
  return;
1003
1090
  }
1091
+ if (pathname === "/api/v1/models" && req.method === "GET") {
1092
+ handleOpenRouterModels(req, res, fixtures, journal, defaults, setCorsHeaders);
1093
+ return;
1094
+ }
1095
+ if (pathname === "/api/v1/key" && req.method === "GET") {
1096
+ handleOpenRouterKey(req, res, journal, setCorsHeaders);
1097
+ return;
1098
+ }
1099
+ if (pathname === "/api/v1/credits" && req.method === "GET") {
1100
+ handleOpenRouterCredits(req, res, journal, setCorsHeaders);
1101
+ return;
1102
+ }
1004
1103
  let azureDeploymentId;
1005
1104
  const azureMatch = pathname.match(AZURE_DEPLOYMENT_RE);
1006
1105
  if (azureMatch && req.method === "POST") {
@@ -1726,17 +1825,17 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1726
1825
  handleNotFound(res, "Not found");
1727
1826
  return;
1728
1827
  }
1729
- const completionsProvider = azureDeploymentId ? "azure" : "openai";
1828
+ const completionsProvider = azureDeploymentId ? "azure" : isOpenRouter ? "openrouter" : "openai";
1730
1829
  try {
1731
- await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider);
1830
+ await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider, isOpenRouter);
1732
1831
  } catch (err) {
1733
1832
  const msg = err instanceof Error ? err.message : "Internal error";
1734
- if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
1833
+ if (!res.headersSent) writeErrorResponse(res, 500, isOpenRouter ? serializeOpenRouterError(500, msg) : JSON.stringify({ error: {
1735
1834
  message: msg,
1736
1835
  type: "server_error"
1737
1836
  } }));
1738
1837
  else if (!res.writableEnded) try {
1739
- res.write(`data: ${JSON.stringify({ error: {
1838
+ res.write(`data: ${isOpenRouter ? serializeOpenRouterError(500, msg) : JSON.stringify({ error: {
1740
1839
  message: msg,
1741
1840
  type: "server_error"
1742
1841
  } })}\n\n`);
@@ -1757,6 +1856,10 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1757
1856
  async function handleUpgradeRequest(req, socket, head) {
1758
1857
  const parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1759
1858
  let pathname = parsedUrl.pathname;
1859
+ if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
1860
+ writeApiKeyUpgradeRejection(socket);
1861
+ return;
1862
+ }
1760
1863
  if (mounts) {
1761
1864
  for (const { path: mountPath, handler } of mounts) if ((pathname === mountPath || pathname.startsWith(mountPath + "/")) && handler.handleUpgrade) {
1762
1865
  const subPath = pathname.slice(mountPath.length) || "/";
@@ -1795,10 +1898,12 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1795
1898
  upgradeHeaders: req.headers
1796
1899
  });
1797
1900
  else if (pathname === REALTIME_PATH) {
1798
- const model = parsedUrl.searchParams.get("model") ?? "gpt-realtime-2";
1901
+ const transcriptionIntent = parsedUrl.searchParams.get("intent") === "transcription";
1902
+ const model = transcriptionIntent ? "gpt-transcribe" : parsedUrl.searchParams.get("model") ?? "gpt-realtime-2";
1799
1903
  handleWebSocketRealtime(ws, fixtures, journal, {
1800
1904
  ...defaults,
1801
1905
  model,
1906
+ transcriptionIntent,
1802
1907
  testId: wsTestId,
1803
1908
  upgradeHeaders: req.headers
1804
1909
  });
@@ -1847,5 +1952,5 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
1847
1952
  }
1848
1953
 
1849
1954
  //#endregion
1850
- export { createServer };
1955
+ export { createServer, createServerWithResolvedAuth, performFullReset };
1851
1956
  //# sourceMappingURL=server.js.map