@copilotkit/aimock 1.42.0 → 1.43.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 (341) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +102 -16
  5. package/dist/agui-mock.cjs +1 -1
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +1 -1
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-recorder.cjs +33 -11
  12. package/dist/agui-recorder.cjs.map +1 -1
  13. package/dist/agui-recorder.d.cts +10 -1
  14. package/dist/agui-recorder.d.cts.map +1 -1
  15. package/dist/agui-recorder.d.ts +10 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +33 -11
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-types.d.cts +1 -0
  20. package/dist/agui-types.d.cts.map +1 -1
  21. package/dist/agui-types.d.ts +1 -0
  22. package/dist/agui-types.d.ts.map +1 -1
  23. package/dist/aimock-cli.cjs +38 -10
  24. package/dist/aimock-cli.cjs.map +1 -1
  25. package/dist/aimock-cli.d.cts +5 -0
  26. package/dist/aimock-cli.d.cts.map +1 -1
  27. package/dist/aimock-cli.d.ts +5 -0
  28. package/dist/aimock-cli.d.ts.map +1 -1
  29. package/dist/aimock-cli.js +38 -10
  30. package/dist/aimock-cli.js.map +1 -1
  31. package/dist/batches.cjs +458 -0
  32. package/dist/batches.cjs.map +1 -0
  33. package/dist/batches.d.cts +57 -0
  34. package/dist/batches.d.cts.map +1 -0
  35. package/dist/batches.d.ts +57 -0
  36. package/dist/batches.d.ts.map +1 -0
  37. package/dist/batches.js +453 -0
  38. package/dist/batches.js.map +1 -0
  39. package/dist/bedrock-converse.cjs +12 -8
  40. package/dist/bedrock-converse.cjs.map +1 -1
  41. package/dist/bedrock-converse.d.cts.map +1 -1
  42. package/dist/bedrock-converse.d.ts.map +1 -1
  43. package/dist/bedrock-converse.js +14 -10
  44. package/dist/bedrock-converse.js.map +1 -1
  45. package/dist/bedrock.cjs +10 -8
  46. package/dist/bedrock.cjs.map +1 -1
  47. package/dist/bedrock.d.cts.map +1 -1
  48. package/dist/bedrock.d.ts.map +1 -1
  49. package/dist/bedrock.js +12 -10
  50. package/dist/bedrock.js.map +1 -1
  51. package/dist/byteplus-video.cjs +3 -3
  52. package/dist/byteplus-video.cjs.map +1 -1
  53. package/dist/byteplus-video.js +4 -4
  54. package/dist/byteplus-video.js.map +1 -1
  55. package/dist/chaos.cjs +622 -72
  56. package/dist/chaos.cjs.map +1 -1
  57. package/dist/chaos.d.cts +164 -7
  58. package/dist/chaos.d.cts.map +1 -1
  59. package/dist/chaos.d.ts +164 -7
  60. package/dist/chaos.d.ts.map +1 -1
  61. package/dist/chaos.js +613 -74
  62. package/dist/chaos.js.map +1 -1
  63. package/dist/cli.cjs +42 -23
  64. package/dist/cli.cjs.map +1 -1
  65. package/dist/cli.js +42 -23
  66. package/dist/cli.js.map +1 -1
  67. package/dist/cohere.cjs +10 -8
  68. package/dist/cohere.cjs.map +1 -1
  69. package/dist/cohere.d.cts.map +1 -1
  70. package/dist/cohere.d.ts.map +1 -1
  71. package/dist/cohere.js +12 -10
  72. package/dist/cohere.js.map +1 -1
  73. package/dist/config-loader.d.cts.map +1 -1
  74. package/dist/config-loader.d.ts.map +1 -1
  75. package/dist/convert.cjs +1 -1
  76. package/dist/convert.cjs.map +1 -1
  77. package/dist/convert.js +1 -1
  78. package/dist/convert.js.map +1 -1
  79. package/dist/elevenlabs-audio.cjs +3 -3
  80. package/dist/elevenlabs-audio.cjs.map +1 -1
  81. package/dist/elevenlabs-audio.js +4 -4
  82. package/dist/elevenlabs-audio.js.map +1 -1
  83. package/dist/elevenlabs-voice.cjs +1053 -0
  84. package/dist/elevenlabs-voice.cjs.map +1 -0
  85. package/dist/elevenlabs-voice.d.cts +72 -0
  86. package/dist/elevenlabs-voice.d.cts.map +1 -0
  87. package/dist/elevenlabs-voice.d.ts +72 -0
  88. package/dist/elevenlabs-voice.d.ts.map +1 -0
  89. package/dist/elevenlabs-voice.js +1048 -0
  90. package/dist/elevenlabs-voice.js.map +1 -0
  91. package/dist/embeddings.cjs +8 -4
  92. package/dist/embeddings.cjs.map +1 -1
  93. package/dist/embeddings.d.cts.map +1 -1
  94. package/dist/embeddings.d.ts.map +1 -1
  95. package/dist/embeddings.js +9 -5
  96. package/dist/embeddings.js.map +1 -1
  97. package/dist/fal-audio.cjs +3 -3
  98. package/dist/fal-audio.cjs.map +1 -1
  99. package/dist/fal-audio.js +4 -4
  100. package/dist/fal-audio.js.map +1 -1
  101. package/dist/fal.cjs +28 -4
  102. package/dist/fal.cjs.map +1 -1
  103. package/dist/fal.d.cts +16 -1
  104. package/dist/fal.d.cts.map +1 -1
  105. package/dist/fal.d.ts +16 -1
  106. package/dist/fal.d.ts.map +1 -1
  107. package/dist/fal.js +28 -5
  108. package/dist/fal.js.map +1 -1
  109. package/dist/files.cjs +1071 -0
  110. package/dist/files.cjs.map +1 -0
  111. package/dist/files.d.cts +68 -0
  112. package/dist/files.d.cts.map +1 -0
  113. package/dist/files.d.ts +68 -0
  114. package/dist/files.d.ts.map +1 -0
  115. package/dist/files.js +1059 -0
  116. package/dist/files.js.map +1 -0
  117. package/dist/fine-tuning.cjs +1051 -0
  118. package/dist/fine-tuning.cjs.map +1 -0
  119. package/dist/fine-tuning.d.cts +96 -0
  120. package/dist/fine-tuning.d.cts.map +1 -0
  121. package/dist/fine-tuning.d.ts +96 -0
  122. package/dist/fine-tuning.d.ts.map +1 -0
  123. package/dist/fine-tuning.js +1044 -0
  124. package/dist/fine-tuning.js.map +1 -0
  125. package/dist/fixture-loader.cjs +0 -0
  126. package/dist/fixture-loader.cjs.map +1 -1
  127. package/dist/fixture-loader.d.cts +37 -5
  128. package/dist/fixture-loader.d.cts.map +1 -1
  129. package/dist/fixture-loader.d.ts +37 -5
  130. package/dist/fixture-loader.d.ts.map +1 -1
  131. package/dist/fixture-loader.js +0 -0
  132. package/dist/fixture-loader.js.map +1 -1
  133. package/dist/gemini-embeddings.cjs +2 -2
  134. package/dist/gemini-embeddings.cjs.map +1 -1
  135. package/dist/gemini-embeddings.js +3 -3
  136. package/dist/gemini-embeddings.js.map +1 -1
  137. package/dist/gemini-interactions.cjs +13 -7
  138. package/dist/gemini-interactions.cjs.map +1 -1
  139. package/dist/gemini-interactions.d.cts.map +1 -1
  140. package/dist/gemini-interactions.d.ts.map +1 -1
  141. package/dist/gemini-interactions.js +15 -9
  142. package/dist/gemini-interactions.js.map +1 -1
  143. package/dist/gemini.cjs +11 -7
  144. package/dist/gemini.cjs.map +1 -1
  145. package/dist/gemini.d.cts.map +1 -1
  146. package/dist/gemini.d.ts.map +1 -1
  147. package/dist/gemini.js +13 -9
  148. package/dist/gemini.js.map +1 -1
  149. package/dist/grok-video.cjs +14 -14
  150. package/dist/grok-video.cjs.map +1 -1
  151. package/dist/grok-video.js +15 -15
  152. package/dist/grok-video.js.map +1 -1
  153. package/dist/helpers.cjs +229 -7
  154. package/dist/helpers.cjs.map +1 -1
  155. package/dist/helpers.d.cts +38 -2
  156. package/dist/helpers.d.cts.map +1 -1
  157. package/dist/helpers.d.ts +38 -2
  158. package/dist/helpers.d.ts.map +1 -1
  159. package/dist/helpers.js +221 -8
  160. package/dist/helpers.js.map +1 -1
  161. package/dist/images.cjs +54 -123
  162. package/dist/images.cjs.map +1 -1
  163. package/dist/images.d.cts.map +1 -1
  164. package/dist/images.d.ts.map +1 -1
  165. package/dist/images.js +55 -124
  166. package/dist/images.js.map +1 -1
  167. package/dist/index.cjs +41 -8
  168. package/dist/index.d.cts +12 -7
  169. package/dist/index.d.ts +12 -7
  170. package/dist/index.js +14 -10
  171. package/dist/jest.cjs +14 -7
  172. package/dist/jest.cjs.map +1 -1
  173. package/dist/jest.d.cts +3 -2
  174. package/dist/jest.d.cts.map +1 -1
  175. package/dist/jest.d.ts +3 -2
  176. package/dist/jest.d.ts.map +1 -1
  177. package/dist/jest.js +14 -7
  178. package/dist/jest.js.map +1 -1
  179. package/dist/journal.cjs +41 -4
  180. package/dist/journal.cjs.map +1 -1
  181. package/dist/journal.d.cts +44 -3
  182. package/dist/journal.d.cts.map +1 -1
  183. package/dist/journal.d.ts +44 -3
  184. package/dist/journal.d.ts.map +1 -1
  185. package/dist/journal.js +41 -5
  186. package/dist/journal.js.map +1 -1
  187. package/dist/live-fixture.cjs +1192 -0
  188. package/dist/live-fixture.cjs.map +1 -0
  189. package/dist/live-fixture.js +1187 -0
  190. package/dist/live-fixture.js.map +1 -0
  191. package/dist/live-recorder.cjs +256 -0
  192. package/dist/live-recorder.cjs.map +1 -0
  193. package/dist/live-recorder.js +254 -0
  194. package/dist/live-recorder.js.map +1 -0
  195. package/dist/live-replay.cjs +286 -0
  196. package/dist/live-replay.cjs.map +1 -0
  197. package/dist/live-replay.js +285 -0
  198. package/dist/live-replay.js.map +1 -0
  199. package/dist/live-sanitize.cjs +196 -0
  200. package/dist/live-sanitize.cjs.map +1 -0
  201. package/dist/live-sanitize.js +196 -0
  202. package/dist/live-sanitize.js.map +1 -0
  203. package/dist/live-types.d.cts +71 -0
  204. package/dist/live-types.d.cts.map +1 -0
  205. package/dist/live-types.d.ts +71 -0
  206. package/dist/live-types.d.ts.map +1 -0
  207. package/dist/llmock.cjs +52 -41
  208. package/dist/llmock.cjs.map +1 -1
  209. package/dist/llmock.d.cts +16 -5
  210. package/dist/llmock.d.cts.map +1 -1
  211. package/dist/llmock.d.ts +16 -5
  212. package/dist/llmock.d.ts.map +1 -1
  213. package/dist/llmock.js +53 -42
  214. package/dist/llmock.js.map +1 -1
  215. package/dist/messages.cjs +11 -7
  216. package/dist/messages.cjs.map +1 -1
  217. package/dist/messages.d.cts.map +1 -1
  218. package/dist/messages.d.ts.map +1 -1
  219. package/dist/messages.js +13 -9
  220. package/dist/messages.js.map +1 -1
  221. package/dist/metrics.cjs +258 -5
  222. package/dist/metrics.cjs.map +1 -1
  223. package/dist/metrics.d.cts +3 -1
  224. package/dist/metrics.d.cts.map +1 -1
  225. package/dist/metrics.d.ts +3 -1
  226. package/dist/metrics.d.ts.map +1 -1
  227. package/dist/metrics.js +255 -6
  228. package/dist/metrics.js.map +1 -1
  229. package/dist/moderation.cjs +10 -1
  230. package/dist/moderation.cjs.map +1 -1
  231. package/dist/moderation.d.cts.map +1 -1
  232. package/dist/moderation.d.ts.map +1 -1
  233. package/dist/moderation.js +10 -1
  234. package/dist/moderation.js.map +1 -1
  235. package/dist/ollama.cjs +5 -5
  236. package/dist/ollama.cjs.map +1 -1
  237. package/dist/ollama.js +6 -6
  238. package/dist/ollama.js.map +1 -1
  239. package/dist/openrouter-video.cjs +5 -5
  240. package/dist/openrouter-video.cjs.map +1 -1
  241. package/dist/openrouter-video.js +6 -6
  242. package/dist/openrouter-video.js.map +1 -1
  243. package/dist/recorder.cjs +45 -9
  244. package/dist/recorder.cjs.map +1 -1
  245. package/dist/recorder.d.cts.map +1 -1
  246. package/dist/recorder.d.ts.map +1 -1
  247. package/dist/recorder.js +43 -11
  248. package/dist/recorder.js.map +1 -1
  249. package/dist/responses.cjs +4 -3
  250. package/dist/responses.cjs.map +1 -1
  251. package/dist/responses.js +5 -5
  252. package/dist/responses.js.map +1 -1
  253. package/dist/router.cjs +20 -33
  254. package/dist/router.cjs.map +1 -1
  255. package/dist/router.d.cts +4 -1
  256. package/dist/router.d.cts.map +1 -1
  257. package/dist/router.d.ts +4 -1
  258. package/dist/router.d.ts.map +1 -1
  259. package/dist/router.js +20 -34
  260. package/dist/router.js.map +1 -1
  261. package/dist/server.cjs +701 -625
  262. package/dist/server.cjs.map +1 -1
  263. package/dist/server.d.cts +1 -0
  264. package/dist/server.d.cts.map +1 -1
  265. package/dist/server.d.ts +1 -0
  266. package/dist/server.d.ts.map +1 -1
  267. package/dist/server.js +704 -628
  268. package/dist/server.js.map +1 -1
  269. package/dist/speech.cjs +2 -2
  270. package/dist/speech.cjs.map +1 -1
  271. package/dist/speech.js +3 -3
  272. package/dist/speech.js.map +1 -1
  273. package/dist/transcription.cjs +4 -4
  274. package/dist/transcription.cjs.map +1 -1
  275. package/dist/transcription.js +5 -5
  276. package/dist/transcription.js.map +1 -1
  277. package/dist/types.d.cts +119 -16
  278. package/dist/types.d.cts.map +1 -1
  279. package/dist/types.d.ts +119 -16
  280. package/dist/types.d.ts.map +1 -1
  281. package/dist/validate-cli.cjs +925 -0
  282. package/dist/validate-cli.cjs.map +1 -0
  283. package/dist/validate-cli.js +924 -0
  284. package/dist/validate-cli.js.map +1 -0
  285. package/dist/vector-types.d.cts.map +1 -1
  286. package/dist/vector-types.d.ts.map +1 -1
  287. package/dist/veo-video.cjs +13 -13
  288. package/dist/veo-video.cjs.map +1 -1
  289. package/dist/veo-video.js +14 -14
  290. package/dist/veo-video.js.map +1 -1
  291. package/dist/video.cjs +4 -4
  292. package/dist/video.cjs.map +1 -1
  293. package/dist/video.d.cts +1 -1
  294. package/dist/video.d.cts.map +1 -1
  295. package/dist/video.d.ts +1 -1
  296. package/dist/video.d.ts.map +1 -1
  297. package/dist/video.js +5 -5
  298. package/dist/video.js.map +1 -1
  299. package/dist/vitest.cjs +13 -6
  300. package/dist/vitest.cjs.map +1 -1
  301. package/dist/vitest.d.cts +2 -1
  302. package/dist/vitest.d.cts.map +1 -1
  303. package/dist/vitest.d.ts +2 -1
  304. package/dist/vitest.d.ts.map +1 -1
  305. package/dist/vitest.js +14 -7
  306. package/dist/vitest.js.map +1 -1
  307. package/dist/watcher.cjs +2 -1
  308. package/dist/watcher.cjs.map +1 -1
  309. package/dist/watcher.js +2 -1
  310. package/dist/watcher.js.map +1 -1
  311. package/dist/ws-framing.cjs +150 -35
  312. package/dist/ws-framing.cjs.map +1 -1
  313. package/dist/ws-framing.d.cts +18 -3
  314. package/dist/ws-framing.d.cts.map +1 -1
  315. package/dist/ws-framing.d.ts +18 -3
  316. package/dist/ws-framing.d.ts.map +1 -1
  317. package/dist/ws-framing.js +151 -36
  318. package/dist/ws-framing.js.map +1 -1
  319. package/dist/ws-gemini-live.cjs +2 -2
  320. package/dist/ws-gemini-live.d.ts +2 -2
  321. package/dist/ws-gemini-live.js +2 -2
  322. package/dist/ws-live.cjs +401 -0
  323. package/dist/ws-live.cjs.map +1 -0
  324. package/dist/ws-live.js +400 -0
  325. package/dist/ws-live.js.map +1 -0
  326. package/dist/ws-realtime.cjs +46 -2
  327. package/dist/ws-realtime.cjs.map +1 -1
  328. package/dist/ws-realtime.d.cts.map +1 -1
  329. package/dist/ws-realtime.d.ts +2 -2
  330. package/dist/ws-realtime.d.ts.map +1 -1
  331. package/dist/ws-realtime.js +46 -2
  332. package/dist/ws-realtime.js.map +1 -1
  333. package/dist/ws-responses.cjs +2 -2
  334. package/dist/ws-responses.js +2 -2
  335. package/dist/ws-upstream.cjs +118 -0
  336. package/dist/ws-upstream.cjs.map +1 -0
  337. package/dist/ws-upstream.js +115 -0
  338. package/dist/ws-upstream.js.map +1 -0
  339. package/fixtures/openai-live-client.json +390 -0
  340. package/fixtures/openai-live-managed.json +709 -0
  341. package/package.json +1 -1
package/dist/speech.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  const require_helpers = require('./helpers.cjs');
2
- const require_router = require('./router.cjs');
3
2
  const require_sse_writer = require('./sse-writer.cjs');
4
3
  const require_chaos = require('./chaos.cjs');
4
+ const require_router = require('./router.cjs');
5
5
  const require_recorder = require('./recorder.cjs');
6
6
 
7
7
  //#region src/speech.ts
@@ -80,7 +80,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
80
80
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
81
81
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
82
82
  } else defaults.logger.debug(`No fixture matched for request`);
83
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
83
+ if (await require_chaos.applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
84
84
  method,
85
85
  path,
86
86
  headers: require_helpers.flattenHeaders(req.headers),
@@ -1 +1 @@
1
- {"version":3,"file":"speech.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaos","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isAudioResponse","FORMAT_TO_CONTENT_TYPE"],"sources":["../src/speech.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!speechReq.input) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'input'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"speech.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isAudioResponse","FORMAT_TO_CONTENT_TYPE"],"sources":["../src/speech.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!speechReq.input) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'input'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
package/dist/speech.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { FORMAT_TO_CONTENT_TYPE, flattenHeaders, getContext, getTestId, isAudioResponse, isErrorResponse, isJsonObject, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
- import { matchFixtureDiagnostic } from "./router.js";
3
2
  import { writeErrorResponse } from "./sse-writer.js";
4
- import { applyChaos } from "./chaos.js";
3
+ import { applyChaosAsync } from "./chaos.js";
4
+ import { matchFixtureDiagnostic } from "./router.js";
5
5
  import { proxyAndRecord } from "./recorder.js";
6
6
 
7
7
  //#region src/speech.ts
@@ -80,7 +80,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
80
80
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
81
81
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
82
82
  } else defaults.logger.debug(`No fixture matched for request`);
83
- if (applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
83
+ if (await applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
84
84
  method,
85
85
  path,
86
86
  headers: flattenHeaders(req.headers),
@@ -1 +1 @@
1
- {"version":3,"file":"speech.js","names":[],"sources":["../src/speech.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!speechReq.input) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'input'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"speech.js","names":[],"sources":["../src/speech.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n if (!speechReq.input) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: \"Missing required parameter: 'input'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  const require_helpers = require('./helpers.cjs');
2
- const require_router = require('./router.cjs');
3
2
  const require_sse_writer = require('./sse-writer.cjs');
4
- const require_interruption = require('./interruption.cjs');
5
3
  const require_chaos = require('./chaos.cjs');
4
+ const require_router = require('./router.cjs');
6
5
  const require_recorder = require('./recorder.cjs');
6
+ const require_interruption = require('./interruption.cjs');
7
7
 
8
8
  //#region src/transcription.ts
9
9
  /**
@@ -16,7 +16,7 @@ const require_recorder = require('./recorder.cjs');
16
16
  * read. So the quotes are consumed BY the match rather than stripped after it:
17
17
  * a post-strip runs on an already-truncated value and leaves `boundary="a b c"`
18
18
  * as the useless `"a`, and the delimiter then never matches, so every form
19
- * field silently falls back to its default (`whisper-1`, `dall-e-2`, …).
19
+ * field silently falls back to its default (`whisper-1`, `gpt-image-1`, …).
20
20
  *
21
21
  * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a
22
22
  * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value
@@ -79,7 +79,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
79
79
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
80
80
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
81
81
  } else defaults.logger.debug(`No fixture matched for request`);
82
- if (require_chaos.applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
82
+ if (await require_chaos.applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
83
83
  method,
84
84
  path,
85
85
  headers: require_helpers.flattenHeaders(req.headers),
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.cjs","names":["getContext","getTestId","matchFixtureDiagnostic","applyChaos","flattenHeaders","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse","createInterruptionSignal","calculateDelay","delay"],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { calculateDelay, delay, writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n *\n * RFC 2046 §5.1.1 defines the boundary parameter as either a bare token\n * (`boundary=abc123`) or a quoted-string (`boundary=\"abc123\"`), and its\n * `bchars` production INCLUDES a space — which is exactly the case the quoted\n * form exists for, and the one a capture that stops at whitespace can never\n * read. So the quotes are consumed BY the match rather than stripped after it:\n * a post-strip runs on an already-truncated value and leaves `boundary=\"a b c\"`\n * as the useless `\"a`, and the delimiter then never matches, so every form\n * field silently falls back to its default (`whisper-1`, `dall-e-2`, …).\n *\n * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a\n * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value\n * literally includes the quotes, and stripping them would break a working\n * request.\n *\n * The `(?:^|[;\\s])` prefix keeps a decoy parameter such as `myboundary=` from\n * matching, and `\\s*=\\s*` tolerates the LWSP some clients put around the `=`.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/(?:^|[;\\s])boundary\\s*=\\s*(?:\"([^\"]*)\"|([^\\s;]+))/i);\n if (!match) return undefined;\n // Group 1 is the quoted form (possibly the empty string), group 2 the bare\n // token; an empty boundary is meaningless, so it reads as absent.\n const boundary = match[1] !== undefined ? match[1] : match[2];\n return boundary || undefined;\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n const stream = extractFormField(raw, \"stream\", boundary) === \"true\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n const journalEntry = journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n\n // Only the transcription endpoint streams modern models. Whisper-1 ignores\n // `stream=true`, and translations continue to return their JSON response.\n if (endpointType === \"transcription\" && stream && model !== \"whisper-1\") {\n const done = {\n type: \"transcript.text.done\",\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n };\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n let chunkIndex = 0;\n const interruption = createInterruptionSignal(fixture);\n for (let index = 0; index < t.text.length; index += chunkSize) {\n const chunkDelay = calculateDelay(\n chunkIndex,\n fixture.streamingProfile,\n latency,\n fixture.recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) break;\n res.write(\n `data: ${JSON.stringify({ type: \"transcript.text.delta\", delta: t.text.slice(index, index + chunkSize) })}\\n\\n`,\n );\n interruption?.tick();\n chunkIndex++;\n }\n if (interruption?.signal.aborted) {\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption.reason();\n interruption.cleanup();\n res.destroy();\n return;\n }\n res.write(`data: ${JSON.stringify(done)}\\n\\n`);\n // The live `gpt-transcribe&stream=true` stream ends with the `[DONE]`\n // sentinel after `transcript.text.done`. Omitting it leaves clients that\n // loop until the sentinel waiting on a stream that never terminates.\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n interruption?.cleanup();\n return;\n }\n\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n }),\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transcription.cjs","names":["getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","flattenHeaders","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse","createInterruptionSignal","calculateDelay","delay"],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { calculateDelay, delay, writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n *\n * RFC 2046 §5.1.1 defines the boundary parameter as either a bare token\n * (`boundary=abc123`) or a quoted-string (`boundary=\"abc123\"`), and its\n * `bchars` production INCLUDES a space — which is exactly the case the quoted\n * form exists for, and the one a capture that stops at whitespace can never\n * read. So the quotes are consumed BY the match rather than stripped after it:\n * a post-strip runs on an already-truncated value and leaves `boundary=\"a b c\"`\n * as the useless `\"a`, and the delimiter then never matches, so every form\n * field silently falls back to its default (`whisper-1`, `gpt-image-1`, …).\n *\n * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a\n * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value\n * literally includes the quotes, and stripping them would break a working\n * request.\n *\n * The `(?:^|[;\\s])` prefix keeps a decoy parameter such as `myboundary=` from\n * matching, and `\\s*=\\s*` tolerates the LWSP some clients put around the `=`.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/(?:^|[;\\s])boundary\\s*=\\s*(?:\"([^\"]*)\"|([^\\s;]+))/i);\n if (!match) return undefined;\n // Group 1 is the quoted form (possibly the empty string), group 2 the bare\n // token; an empty boundary is meaningless, so it reads as absent.\n const boundary = match[1] !== undefined ? match[1] : match[2];\n return boundary || undefined;\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n const stream = extractFormField(raw, \"stream\", boundary) === \"true\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n const journalEntry = journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n\n // Only the transcription endpoint streams modern models. Whisper-1 ignores\n // `stream=true`, and translations continue to return their JSON response.\n if (endpointType === \"transcription\" && stream && model !== \"whisper-1\") {\n const done = {\n type: \"transcript.text.done\",\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n };\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n let chunkIndex = 0;\n const interruption = createInterruptionSignal(fixture);\n for (let index = 0; index < t.text.length; index += chunkSize) {\n const chunkDelay = calculateDelay(\n chunkIndex,\n fixture.streamingProfile,\n latency,\n fixture.recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) break;\n res.write(\n `data: ${JSON.stringify({ type: \"transcript.text.delta\", delta: t.text.slice(index, index + chunkSize) })}\\n\\n`,\n );\n interruption?.tick();\n chunkIndex++;\n }\n if (interruption?.signal.aborted) {\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption.reason();\n interruption.cleanup();\n res.destroy();\n return;\n }\n res.write(`data: ${JSON.stringify(done)}\\n\\n`);\n // The live `gpt-transcribe&stream=true` stream ends with the `[DONE]`\n // sentinel after `transcript.text.done`. Omitting it leaves clients that\n // loop until the sentinel waiting on a stream that never terminates.\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n interruption?.cleanup();\n return;\n }\n\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n }),\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAgB,gBAAgB,aAAqD;AACnF,KAAI,CAAC,YAAa,QAAO;CACzB,MAAM,QAAQ,YAAY,MAAM,qDAAqD;AACrF,KAAI,CAAC,MAAO,QAAO;AAInB,SADiB,MAAM,OAAO,SAAY,MAAM,KAAK,MAAM,OACxC;;;;;;;;;AAUrB,SAAgB,iBACd,KACA,WACA,UACoB;AACpB,KAAI,CAAC,UAAU;AAEb,UAAQ,KAAK,kFAAkF;EAC/F,MAAM,UAAU,UAAU,QAAQ,uBAAuB,OAAO;EAChE,MAAM,UAAU,IAAI,OAClB,qDAAqD,QAAQ,sCAC7D,IACD;AAED,SADc,IAAI,MAAM,QAAQ,GACjB;;CAIjB,MAAM,YAAY,KAAK;CACvB,MAAM,QAAQ,IAAI,MAAM,UAAU;AAElC,MAAK,MAAM,QAAQ,OAAO;AAExB,MAAI,CAAC,QAAQ,KAAK,WAAW,CAAC,WAAW,KAAK,CAAE;EAGhD,MAAM,YAAY,KAAK,QAAQ,WAAW;AAC1C,MAAI,cAAc,GAAI;EAEtB,MAAM,UAAU,KAAK,MAAM,GAAG,UAAU;EACxC,MAAM,OAAO,KAAK,MAAM,YAAY,EAAE;EAGtC,MAAM,UAAU,QAAQ,MAAM,2DAA2D;AACzF,MAAI,WAAW,QAAQ,OAAO,UAE5B,QAAO,KAAK,QAAQ,SAAS,GAAG;;;AAMtC,eAAsB,oBACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACA,eAAgD,iBACjC;AACf,gBAAe,IAAI;CACnB,MAAM,cACJ,iBAAiB,gBAAgB,2BAA2B;CAC9D,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,SAAS,IAAI,UAAU;CAK7B,MAAM,WAAW,gBAHG,MAAM,QAAQ,IAAI,QAAQ,gBAAgB,GAC1D,IAAI,QAAQ,gBAAgB,KAC5B,IAAI,QAAQ,gBAC6B;CAE7C,MAAM,QAAQ,iBAAiB,KAAK,SAAS,SAAS,IAAI;CAC1D,MAAM,iBAAiB,iBAAiB,KAAK,mBAAmB,SAAS,IAAI;CAC7E,MAAM,SAAS,iBAAiB,KAAK,UAAU,SAAS,KAAK;CAE7D,MAAM,eAAsC;EAC1C;EACA,UAAU,EAAE;EACZ,eAAe;EACf,UAAUA,2BAAW,IAAI;EAC1B;CAED,MAAM,SAASC,0BAAU,IAAI;CAC7B,MAAM,EAAE,SAAS,4BAA4BC,sCAC3C,UACA,cACA,QAAQ,6BAA6B,OAAO,EAC5C,SAAS,iBACV;AAED,KAAI,SAAS;AACX,UAAQ,2BAA2B,SAAS,UAAU,OAAO;AAC7D,WAAS,OAAO,MAAM,oBAAoB,KAAK,UAAU,QAAQ,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG;OAExF,UAAS,OAAO,MAAM,iCAAiC;AAGzD,KACE,MAAMC,8BACJ,KACA,SACA,SAAS,OACT,IAAI,SACJ,IAAI,KACJ,SACA;EAAE;EAAQ;EAAM,SAASC,+BAAe,IAAI,QAAQ;EAAE,MAAM;EAAc,EAC1E,UAAU,YAAY,SACtB,SAAS,UACT,SAAS,OACV,CAED;AAEF,KAAI,CAAC,SAAS;AAEZ,MADwBC,kCAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;GACnB,MAAM,gBAAgBC,qCAAqB,wBAAwB;AACnE,YAAS,OAAO,MAAMC,qCAAqB,QAAQ,MAAM,wBAAwB,CAAC;AAClF,WAAQ,IAAI;IACV;IACA;IACA,SAASH,+BAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KACR,QAAQ;KACR,SAAS;KACT,GAAGI,oCAAoB,SAAS,QAAQ,IAAI,QAAQ;KACrD;IACF,CAAC;AACF,yCACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;IACL,SAAS;IACT,MAAM;IACN,MAAM;IACP,EACF,CAAC,CACH;AACD;;AAEF,MAAI,SAAS,QAAQ;GACnB,MAAM,UAAU,MAAMC,gCACpB,KACA,KACA,cACA,UACA,IAAI,OAAO,aACX,UACA,UACA,IACD;AACD,OAAI,YAAY,kBAAmB;AACnC,OAAI,YAAY,kBAAkB;AAChC,YAAQ,IAAI;KACV;KACA;KACA,SAASL,+BAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ,IAAI,cAAc;MAAK,SAAS;MAAM,QAAQ;MAAS;KAC5E,CAAC;AACF;;;AAIJ,UAAQ,IAAI;GACV;GACA;GACA,SAASA,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IACR,QAAQ;IACR,SAAS;IACT,GAAGI,oCAAoB,SAAS,QAAQ,IAAI,QAAQ;IACrD;GACF,CAAC;AACF,wCACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,WAAW,MAAME,gCAAgB,SAAS,aAAa;AAE7D,KAAIC,gCAAgB,SAAS,EAAE;EAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,UAAQ,IAAI;GACV;GACA;GACA,SAASP,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE;IAAQ;IAAS;GAC9B,CAAC;AACF,wCAAmB,KAAK,QAAQQ,uCAAuB,SAAS,EAAE,EAChE,YAAY,SAAS,YACtB,CAAC;AACF;;AAGF,KAAI,CAACC,wCAAwB,SAAS,EAAE;AACtC,UAAQ,IAAI;GACV;GACA;GACA,SAAST,+BAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,wCACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,eAAe,QAAQ,IAAI;EAC/B;EACA;EACA,SAASA,+BAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK;GAAS;EACnC,CAAC;CAEF,MAAM,IAAI,SAAS;AAInB,KAAI,iBAAiB,mBAAmB,UAAU,UAAU,aAAa;EACvE,MAAM,OAAO;GACX,MAAM;GACN,MAAM,EAAE;GACR,GAAI,EAAE,cAAc,SAAY,EAAE,WAAW,EAAE,WAAW,GAAG,EAAE;GAC/D,GAAI,EAAE,UAAU,SAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE;GACpD;EACD,MAAM,UAAU,QAAQ,WAAW,SAAS;EAC5C,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,aAAa,SAAS,UAAU;EACtE,MAAM,cAAc,QAAQ,eAAe,SAAS;AACpD,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACb,CAAC;EACF,IAAI,aAAa;EACjB,MAAM,eAAeU,8CAAyB,QAAQ;AACtD,OAAK,IAAI,QAAQ,GAAG,QAAQ,EAAE,KAAK,QAAQ,SAAS,WAAW;GAC7D,MAAM,aAAaC,kCACjB,YACA,QAAQ,kBACR,SACA,QAAQ,iBACR,YACD;AACD,OAAI,aAAa,EAAG,OAAMC,yBAAM,YAAY,cAAc,OAAO;AACjE,OAAI,cAAc,OAAO,QAAS;AAClC,OAAI,MACF,SAAS,KAAK,UAAU;IAAE,MAAM;IAAyB,OAAO,EAAE,KAAK,MAAM,OAAO,QAAQ,UAAU;IAAE,CAAC,CAAC,MAC3G;AACD,iBAAc,MAAM;AACpB;;AAEF,MAAI,cAAc,OAAO,SAAS;AAChC,gBAAa,SAAS,cAAc;AACpC,gBAAa,SAAS,kBAAkB,aAAa,QAAQ;AAC7D,gBAAa,SAAS;AACtB,OAAI,SAAS;AACb;;AAEF,MAAI,MAAM,SAAS,KAAK,UAAU,KAAK,CAAC,MAAM;AAI9C,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;AACT,gBAAc,SAAS;AACvB;;AAKF,KAFmB,mBAAmB,kBAAkB,EAAE,SAAS,QAAQ,EAAE,YAAY,MAEzE;EACd,MAAM,cAAuC;GAC3C,MAAM,iBAAiB,gBAAgB,cAAc;GACrD,UAAU,EAAE,YAAY;GACxB,UAAU,EAAE,YAAY;GACxB,MAAM,EAAE;GACT;AACD,MAAI,EAAE,SAAS,EAAE,MAAM,SAAS,EAC9B,aAAY,QAAQ,EAAE;AAExB,MAAI,EAAE,YAAY,EAAE,SAAS,SAAS,EACpC,aAAY,WAAW,EAAE;AAE3B,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IAAI,KAAK,UAAU,YAAY,CAAC;QAC/B;AACL,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IACF,KAAK,UAAU;GACb,MAAM,EAAE;GACR,GAAI,EAAE,cAAc,SAAY,EAAE,WAAW,EAAE,WAAW,GAAG,EAAE;GAC/D,GAAI,EAAE,UAAU,SAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE;GACpD,CAAC,CACH"}
@@ -1,9 +1,9 @@
1
1
  import { flattenHeaders, getContext, getTestId, isErrorResponse, isTranscriptionResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
2
- import { matchFixtureDiagnostic } from "./router.js";
3
2
  import { calculateDelay, delay, writeErrorResponse } from "./sse-writer.js";
4
- import { createInterruptionSignal } from "./interruption.js";
5
- import { applyChaos } from "./chaos.js";
3
+ import { applyChaosAsync } from "./chaos.js";
4
+ import { matchFixtureDiagnostic } from "./router.js";
6
5
  import { proxyAndRecord } from "./recorder.js";
6
+ import { createInterruptionSignal } from "./interruption.js";
7
7
 
8
8
  //#region src/transcription.ts
9
9
  /**
@@ -16,7 +16,7 @@ import { proxyAndRecord } from "./recorder.js";
16
16
  * read. So the quotes are consumed BY the match rather than stripped after it:
17
17
  * a post-strip runs on an already-truncated value and leaves `boundary="a b c"`
18
18
  * as the useless `"a`, and the delimiter then never matches, so every form
19
- * field silently falls back to its default (`whisper-1`, `dall-e-2`, …).
19
+ * field silently falls back to its default (`whisper-1`, `gpt-image-1`, …).
20
20
  *
21
21
  * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a
22
22
  * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value
@@ -79,7 +79,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
79
79
  journal.incrementFixtureMatchCount(fixture, fixtures, testId);
80
80
  defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);
81
81
  } else defaults.logger.debug(`No fixture matched for request`);
82
- if (applyChaos(res, fixture, defaults.chaos, req.headers, req.url, journal, {
82
+ if (await applyChaosAsync(res, fixture, defaults.chaos, req.headers, req.url, journal, {
83
83
  method,
84
84
  path,
85
85
  headers: flattenHeaders(req.headers),
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.js","names":[],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { calculateDelay, delay, writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n *\n * RFC 2046 §5.1.1 defines the boundary parameter as either a bare token\n * (`boundary=abc123`) or a quoted-string (`boundary=\"abc123\"`), and its\n * `bchars` production INCLUDES a space — which is exactly the case the quoted\n * form exists for, and the one a capture that stops at whitespace can never\n * read. So the quotes are consumed BY the match rather than stripped after it:\n * a post-strip runs on an already-truncated value and leaves `boundary=\"a b c\"`\n * as the useless `\"a`, and the delimiter then never matches, so every form\n * field silently falls back to its default (`whisper-1`, `dall-e-2`, …).\n *\n * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a\n * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value\n * literally includes the quotes, and stripping them would break a working\n * request.\n *\n * The `(?:^|[;\\s])` prefix keeps a decoy parameter such as `myboundary=` from\n * matching, and `\\s*=\\s*` tolerates the LWSP some clients put around the `=`.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/(?:^|[;\\s])boundary\\s*=\\s*(?:\"([^\"]*)\"|([^\\s;]+))/i);\n if (!match) return undefined;\n // Group 1 is the quoted form (possibly the empty string), group 2 the bare\n // token; an empty boundary is meaningless, so it reads as absent.\n const boundary = match[1] !== undefined ? match[1] : match[2];\n return boundary || undefined;\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n const stream = extractFormField(raw, \"stream\", boundary) === \"true\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n const journalEntry = journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n\n // Only the transcription endpoint streams modern models. Whisper-1 ignores\n // `stream=true`, and translations continue to return their JSON response.\n if (endpointType === \"transcription\" && stream && model !== \"whisper-1\") {\n const done = {\n type: \"transcript.text.done\",\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n };\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n let chunkIndex = 0;\n const interruption = createInterruptionSignal(fixture);\n for (let index = 0; index < t.text.length; index += chunkSize) {\n const chunkDelay = calculateDelay(\n chunkIndex,\n fixture.streamingProfile,\n latency,\n fixture.recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) break;\n res.write(\n `data: ${JSON.stringify({ type: \"transcript.text.delta\", delta: t.text.slice(index, index + chunkSize) })}\\n\\n`,\n );\n interruption?.tick();\n chunkIndex++;\n }\n if (interruption?.signal.aborted) {\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption.reason();\n interruption.cleanup();\n res.destroy();\n return;\n }\n res.write(`data: ${JSON.stringify(done)}\\n\\n`);\n // The live `gpt-transcribe&stream=true` stream ends with the `[DONE]`\n // sentinel after `transcript.text.done`. Omitting it leaves clients that\n // loop until the sentinel waiting on a stream that never terminates.\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n interruption?.cleanup();\n return;\n }\n\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n }),\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transcription.js","names":[],"sources":["../src/transcription.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isTranscriptionResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { calculateDelay, delay, writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\n\n/**\n * Extract the multipart boundary string from a Content-Type header.\n *\n * RFC 2046 §5.1.1 defines the boundary parameter as either a bare token\n * (`boundary=abc123`) or a quoted-string (`boundary=\"abc123\"`), and its\n * `bchars` production INCLUDES a space — which is exactly the case the quoted\n * form exists for, and the one a capture that stops at whitespace can never\n * read. So the quotes are consumed BY the match rather than stripped after it:\n * a post-strip runs on an already-truncated value and leaves `boundary=\"a b c\"`\n * as the useless `\"a`, and the delimiter then never matches, so every form\n * field silently falls back to its default (`whisper-1`, `gpt-image-1`, …).\n *\n * Only DQUOTE quotes. An apostrophe is a legal RFC 2045 token character and a\n * legal RFC 2046 `bchar`, so `boundary='abc'` is a BARE token whose value\n * literally includes the quotes, and stripping them would break a working\n * request.\n *\n * The `(?:^|[;\\s])` prefix keeps a decoy parameter such as `myboundary=` from\n * matching, and `\\s*=\\s*` tolerates the LWSP some clients put around the `=`.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/(?:^|[;\\s])boundary\\s*=\\s*(?:\"([^\"]*)\"|([^\\s;]+))/i);\n if (!match) return undefined;\n // Group 1 is the quoted form (possibly the empty string), group 2 the bare\n // token; an empty boundary is meaningless, so it reads as absent.\n const boundary = match[1] !== undefined ? match[1] : match[2];\n return boundary || undefined;\n}\n\n/**\n * Extract a text field from multipart form data using boundary-based parsing.\n * Splits the body by the multipart boundary so each part is isolated, then\n * checks each part's Content-Disposition header for the target field name.\n * This avoids false matches from binary audio data that might contain\n * header-like byte sequences.\n */\nexport function extractFormField(\n raw: string,\n fieldName: string,\n boundary: string | undefined,\n): string | undefined {\n if (!boundary) {\n // Fallback: no boundary available, use simple regex (best-effort)\n console.warn(\"extractFormField: no multipart boundary found, using best-effort regex fallback\");\n const escaped = fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${escaped}\"[^\\\\r\\\\n]*\\\\r\\\\n\\\\r\\\\n([^\\\\r\\\\n]*)`,\n \"i\",\n );\n const match = raw.match(pattern);\n return match?.[1];\n }\n\n // Split by boundary delimiter — each chunk is one part\n const delimiter = `--${boundary}`;\n const parts = raw.split(delimiter);\n\n for (const part of parts) {\n // Skip the preamble (before first boundary) and epilogue (after closing boundary)\n if (!part || part.trimStart().startsWith(\"--\")) continue;\n\n // Split part into headers and body at the first blank line (\\r\\n\\r\\n)\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd === -1) continue;\n\n const headers = part.slice(0, headerEnd);\n const body = part.slice(headerEnd + 4);\n\n // Check if this part's Content-Disposition names the target field\n const cdMatch = headers.match(/Content-Disposition:\\s*form-data;[^\\r\\n]*name=\"([^\"]+)\"/i);\n if (cdMatch && cdMatch[1] === fieldName) {\n // Return the body value, trimming trailing \\r\\n from the part boundary\n return body.replace(/\\r\\n$/, \"\");\n }\n }\n return undefined;\n}\n\nexport async function handleTranscription(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n endpointType: \"transcription\" | \"translation\" = \"transcription\",\n): Promise<void> {\n setCorsHeaders(res);\n const defaultPath =\n endpointType === \"translation\" ? \"/v1/audio/translations\" : \"/v1/audio/transcriptions\";\n const path = req.url ?? defaultPath;\n const method = req.method ?? \"POST\";\n\n const contentType = Array.isArray(req.headers[\"content-type\"])\n ? req.headers[\"content-type\"][0]\n : req.headers[\"content-type\"];\n const boundary = extractBoundary(contentType);\n\n const model = extractFormField(raw, \"model\", boundary) ?? \"whisper-1\";\n const responseFormat = extractFormField(raw, \"response_format\", boundary) ?? \"json\";\n const stream = extractFormField(raw, \"stream\", boundary) === \"true\";\n\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\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 const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\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 if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? defaultPath,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\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,\n path,\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 writeErrorResponse(\n res,\n 404,\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,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isTranscriptionResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a transcription type\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n const journalEntry = journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const t = response.transcription;\n\n // Only the transcription endpoint streams modern models. Whisper-1 ignores\n // `stream=true`, and translations continue to return their JSON response.\n if (endpointType === \"transcription\" && stream && model !== \"whisper-1\") {\n const done = {\n type: \"transcript.text.done\",\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n };\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n let chunkIndex = 0;\n const interruption = createInterruptionSignal(fixture);\n for (let index = 0; index < t.text.length; index += chunkSize) {\n const chunkDelay = calculateDelay(\n chunkIndex,\n fixture.streamingProfile,\n latency,\n fixture.recordedTimings,\n replaySpeed,\n );\n if (chunkDelay > 0) await delay(chunkDelay, interruption?.signal);\n if (interruption?.signal.aborted) break;\n res.write(\n `data: ${JSON.stringify({ type: \"transcript.text.delta\", delta: t.text.slice(index, index + chunkSize) })}\\n\\n`,\n );\n interruption?.tick();\n chunkIndex++;\n }\n if (interruption?.signal.aborted) {\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption.reason();\n interruption.cleanup();\n res.destroy();\n return;\n }\n res.write(`data: ${JSON.stringify(done)}\\n\\n`);\n // The live `gpt-transcribe&stream=true` stream ends with the `[DONE]`\n // sentinel after `transcript.text.done`. Omitting it leaves clients that\n // loop until the sentinel waiting on a stream that never terminates.\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n interruption?.cleanup();\n return;\n }\n\n const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: endpointType === \"translation\" ? \"translate\" : \"transcribe\",\n language: t.language ?? \"english\",\n duration: t.duration ?? 0,\n text: t.text,\n };\n if (t.words && t.words.length > 0) {\n verboseBody.words = t.words;\n }\n if (t.segments && t.segments.length > 0) {\n verboseBody.segments = t.segments;\n }\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(verboseBody));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n text: t.text,\n ...(t.languages !== undefined ? { languages: t.languages } : {}),\n ...(t.usage !== undefined ? { usage: t.usage } : {}),\n }),\n );\n }\n}\n"],"mappings":"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"}