@copilotkit/aimock 1.24.1 → 1.26.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 (206) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +38 -0
  4. package/README.md +21 -11
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/agui-types.d.ts.map +1 -1
  7. package/dist/aws-event-stream.cjs +2 -1
  8. package/dist/aws-event-stream.cjs.map +1 -1
  9. package/dist/aws-event-stream.d.cts +3 -1
  10. package/dist/aws-event-stream.d.cts.map +1 -1
  11. package/dist/aws-event-stream.d.ts +3 -1
  12. package/dist/aws-event-stream.d.ts.map +1 -1
  13. package/dist/aws-event-stream.js +2 -1
  14. package/dist/aws-event-stream.js.map +1 -1
  15. package/dist/bedrock-converse.cjs +8 -2
  16. package/dist/bedrock-converse.cjs.map +1 -1
  17. package/dist/bedrock-converse.d.cts.map +1 -1
  18. package/dist/bedrock-converse.d.ts.map +1 -1
  19. package/dist/bedrock-converse.js +8 -2
  20. package/dist/bedrock-converse.js.map +1 -1
  21. package/dist/bedrock.cjs +8 -2
  22. package/dist/bedrock.cjs.map +1 -1
  23. package/dist/bedrock.d.cts.map +1 -1
  24. package/dist/bedrock.d.ts.map +1 -1
  25. package/dist/bedrock.js +8 -2
  26. package/dist/bedrock.js.map +1 -1
  27. package/dist/cli.cjs +36 -1
  28. package/dist/cli.cjs.map +1 -1
  29. package/dist/cli.js +36 -1
  30. package/dist/cli.js.map +1 -1
  31. package/dist/cohere.cjs +206 -2
  32. package/dist/cohere.cjs.map +1 -1
  33. package/dist/cohere.d.cts.map +1 -1
  34. package/dist/cohere.d.ts.map +1 -1
  35. package/dist/cohere.js +207 -4
  36. package/dist/cohere.js.map +1 -1
  37. package/dist/config-loader.d.ts.map +1 -1
  38. package/dist/elevenlabs-audio.cjs +173 -1
  39. package/dist/elevenlabs-audio.cjs.map +1 -1
  40. package/dist/elevenlabs-audio.d.cts.map +1 -1
  41. package/dist/elevenlabs-audio.d.ts.map +1 -1
  42. package/dist/elevenlabs-audio.js +173 -2
  43. package/dist/elevenlabs-audio.js.map +1 -1
  44. package/dist/embeddings.cjs +1 -1
  45. package/dist/embeddings.cjs.map +1 -1
  46. package/dist/embeddings.js +1 -1
  47. package/dist/embeddings.js.map +1 -1
  48. package/dist/fal-audio.cjs +2 -4
  49. package/dist/fal-audio.cjs.map +1 -1
  50. package/dist/fal-audio.js +2 -4
  51. package/dist/fal-audio.js.map +1 -1
  52. package/dist/fal.cjs +2 -2
  53. package/dist/fal.cjs.map +1 -1
  54. package/dist/fal.d.cts.map +1 -1
  55. package/dist/fal.d.ts.map +1 -1
  56. package/dist/fal.js +2 -2
  57. package/dist/fal.js.map +1 -1
  58. package/dist/fixture-loader.cjs +16 -3
  59. package/dist/fixture-loader.cjs.map +1 -1
  60. package/dist/fixture-loader.d.cts.map +1 -1
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +16 -3
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-embeddings.cjs +166 -0
  65. package/dist/gemini-embeddings.cjs.map +1 -0
  66. package/dist/gemini-embeddings.js +166 -0
  67. package/dist/gemini-embeddings.js.map +1 -0
  68. package/dist/gemini-interactions.cjs +10 -2
  69. package/dist/gemini-interactions.cjs.map +1 -1
  70. package/dist/gemini-interactions.d.cts.map +1 -1
  71. package/dist/gemini-interactions.d.ts.map +1 -1
  72. package/dist/gemini-interactions.js +10 -2
  73. package/dist/gemini-interactions.js.map +1 -1
  74. package/dist/gemini.cjs +12 -2
  75. package/dist/gemini.cjs.map +1 -1
  76. package/dist/gemini.d.cts.map +1 -1
  77. package/dist/gemini.d.ts.map +1 -1
  78. package/dist/gemini.js +12 -2
  79. package/dist/gemini.js.map +1 -1
  80. package/dist/helpers.cjs +70 -33
  81. package/dist/helpers.cjs.map +1 -1
  82. package/dist/helpers.d.cts +9 -5
  83. package/dist/helpers.d.cts.map +1 -1
  84. package/dist/helpers.d.ts +9 -5
  85. package/dist/helpers.d.ts.map +1 -1
  86. package/dist/helpers.js +68 -34
  87. package/dist/helpers.js.map +1 -1
  88. package/dist/images.cjs +295 -13
  89. package/dist/images.cjs.map +1 -1
  90. package/dist/images.d.cts +9 -1
  91. package/dist/images.d.cts.map +1 -1
  92. package/dist/images.d.ts +9 -1
  93. package/dist/images.d.ts.map +1 -1
  94. package/dist/images.js +294 -14
  95. package/dist/images.js.map +1 -1
  96. package/dist/index.cjs +1 -1
  97. package/dist/index.d.cts +2 -2
  98. package/dist/index.d.ts +2 -2
  99. package/dist/index.js +1 -1
  100. package/dist/llmock.cjs +16 -1
  101. package/dist/llmock.cjs.map +1 -1
  102. package/dist/llmock.d.cts +2 -0
  103. package/dist/llmock.d.cts.map +1 -1
  104. package/dist/llmock.d.ts +2 -0
  105. package/dist/llmock.d.ts.map +1 -1
  106. package/dist/llmock.js +16 -1
  107. package/dist/llmock.js.map +1 -1
  108. package/dist/messages.cjs +9 -2
  109. package/dist/messages.cjs.map +1 -1
  110. package/dist/messages.d.cts.map +1 -1
  111. package/dist/messages.d.ts.map +1 -1
  112. package/dist/messages.js +9 -2
  113. package/dist/messages.js.map +1 -1
  114. package/dist/metrics.cjs +2 -0
  115. package/dist/metrics.cjs.map +1 -1
  116. package/dist/metrics.d.cts.map +1 -1
  117. package/dist/metrics.d.ts.map +1 -1
  118. package/dist/metrics.js +2 -0
  119. package/dist/metrics.js.map +1 -1
  120. package/dist/ndjson-writer.cjs +2 -1
  121. package/dist/ndjson-writer.cjs.map +1 -1
  122. package/dist/ndjson-writer.d.cts +3 -2
  123. package/dist/ndjson-writer.d.cts.map +1 -1
  124. package/dist/ndjson-writer.d.ts +3 -2
  125. package/dist/ndjson-writer.d.ts.map +1 -1
  126. package/dist/ndjson-writer.js +2 -1
  127. package/dist/ndjson-writer.js.map +1 -1
  128. package/dist/ollama.cjs +197 -2
  129. package/dist/ollama.cjs.map +1 -1
  130. package/dist/ollama.d.cts.map +1 -1
  131. package/dist/ollama.d.ts.map +1 -1
  132. package/dist/ollama.js +198 -4
  133. package/dist/ollama.js.map +1 -1
  134. package/dist/recorder.cjs +49 -5
  135. package/dist/recorder.cjs.map +1 -1
  136. package/dist/recorder.d.cts.map +1 -1
  137. package/dist/recorder.d.ts.map +1 -1
  138. package/dist/recorder.js +49 -5
  139. package/dist/recorder.js.map +1 -1
  140. package/dist/responses.cjs +11 -2
  141. package/dist/responses.cjs.map +1 -1
  142. package/dist/responses.d.cts.map +1 -1
  143. package/dist/responses.d.ts.map +1 -1
  144. package/dist/responses.js +11 -2
  145. package/dist/responses.js.map +1 -1
  146. package/dist/server.cjs +196 -49
  147. package/dist/server.cjs.map +1 -1
  148. package/dist/server.d.cts.map +1 -1
  149. package/dist/server.d.ts.map +1 -1
  150. package/dist/server.js +201 -54
  151. package/dist/server.js.map +1 -1
  152. package/dist/speech.cjs +1 -1
  153. package/dist/speech.cjs.map +1 -1
  154. package/dist/speech.js +1 -1
  155. package/dist/speech.js.map +1 -1
  156. package/dist/sse-writer.cjs +48 -10
  157. package/dist/sse-writer.cjs.map +1 -1
  158. package/dist/sse-writer.d.cts +12 -4
  159. package/dist/sse-writer.d.cts.map +1 -1
  160. package/dist/sse-writer.d.ts +12 -4
  161. package/dist/sse-writer.d.ts.map +1 -1
  162. package/dist/sse-writer.js +48 -10
  163. package/dist/sse-writer.js.map +1 -1
  164. package/dist/transcription.cjs +9 -6
  165. package/dist/transcription.cjs.map +1 -1
  166. package/dist/transcription.d.cts +2 -2
  167. package/dist/transcription.d.cts.map +1 -1
  168. package/dist/transcription.d.ts +2 -2
  169. package/dist/transcription.d.ts.map +1 -1
  170. package/dist/transcription.js +8 -7
  171. package/dist/transcription.js.map +1 -1
  172. package/dist/types.d.cts +45 -3
  173. package/dist/types.d.cts.map +1 -1
  174. package/dist/types.d.ts +45 -3
  175. package/dist/types.d.ts.map +1 -1
  176. package/dist/video.cjs +1 -1
  177. package/dist/video.cjs.map +1 -1
  178. package/dist/video.d.cts.map +1 -1
  179. package/dist/video.d.ts.map +1 -1
  180. package/dist/video.js +1 -1
  181. package/dist/video.js.map +1 -1
  182. package/dist/ws-gemini-live.cjs +14 -4
  183. package/dist/ws-gemini-live.cjs.map +1 -1
  184. package/dist/ws-gemini-live.d.cts +1 -0
  185. package/dist/ws-gemini-live.d.cts.map +1 -1
  186. package/dist/ws-gemini-live.d.ts +1 -0
  187. package/dist/ws-gemini-live.d.ts.map +1 -1
  188. package/dist/ws-gemini-live.js +15 -5
  189. package/dist/ws-gemini-live.js.map +1 -1
  190. package/dist/ws-realtime.cjs +21 -4
  191. package/dist/ws-realtime.cjs.map +1 -1
  192. package/dist/ws-realtime.d.cts +1 -0
  193. package/dist/ws-realtime.d.cts.map +1 -1
  194. package/dist/ws-realtime.d.ts +1 -0
  195. package/dist/ws-realtime.d.ts.map +1 -1
  196. package/dist/ws-realtime.js +22 -5
  197. package/dist/ws-realtime.js.map +1 -1
  198. package/dist/ws-responses.cjs +8 -5
  199. package/dist/ws-responses.cjs.map +1 -1
  200. package/dist/ws-responses.d.cts +1 -0
  201. package/dist/ws-responses.d.cts.map +1 -1
  202. package/dist/ws-responses.d.ts +1 -0
  203. package/dist/ws-responses.d.ts.map +1 -1
  204. package/dist/ws-responses.js +9 -6
  205. package/dist/ws-responses.js.map +1 -1
  206. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"speech.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","resolveStrictMode","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 serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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 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 };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"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 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","getTestId","matchFixture","applyChaos","resolveStrictMode","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 serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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 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 };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"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
@@ -137,7 +137,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
137
137
  fixture
138
138
  }
139
139
  });
140
- writeErrorResponse(res, status, serializeErrorResponse(response));
140
+ writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
141
141
  return;
142
142
  }
143
143
  if (!isAudioResponse(response)) {
@@ -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 serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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 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 };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"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 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 serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } 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 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 };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"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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"}
@@ -10,19 +10,39 @@ function delay(ms, signal) {
10
10
  }, { once: true });
11
11
  });
12
12
  }
13
- function calculateDelay(chunkIndex, profile, fallbackLatency) {
14
- if (!profile) return fallbackLatency ?? 0;
13
+ function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, replaySpeed) {
14
+ const speed = replaySpeed ?? 1;
15
15
  let delayMs;
16
- if (chunkIndex === 0 && profile.ttft !== void 0) delayMs = profile.ttft;
17
- else if (profile.tps !== void 0 && profile.tps > 0) delayMs = 1e3 / profile.tps;
18
- else return fallbackLatency ?? 0;
19
- if (profile.jitter && profile.jitter > 0) delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;
20
- return Math.max(0, delayMs);
16
+ if (profile) {
17
+ let fromProfile = true;
18
+ if (chunkIndex === 0 && profile.ttft !== void 0) delayMs = profile.ttft;
19
+ else if (profile.tps !== void 0 && profile.tps > 0) delayMs = 1e3 / profile.tps;
20
+ else {
21
+ delayMs = fallbackLatency ?? 0;
22
+ fromProfile = false;
23
+ }
24
+ if (fromProfile && profile.jitter && profile.jitter > 0) {
25
+ delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;
26
+ if (delayMs < 0) delayMs = 0;
27
+ }
28
+ } else if (recordedTimings) if (chunkIndex === 0) delayMs = recordedTimings.ttftMs;
29
+ else {
30
+ const idx = chunkIndex - 1;
31
+ if (idx < recordedTimings.interChunkDelaysMs.length) delayMs = recordedTimings.interChunkDelaysMs[idx];
32
+ else {
33
+ const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);
34
+ delayMs = recordedTimings.interChunkDelaysMs.length > 0 ? totalInterChunk / recordedTimings.interChunkDelaysMs.length : 0;
35
+ }
36
+ }
37
+ else delayMs = fallbackLatency ?? 0;
38
+ delayMs = Math.max(0, delayMs);
39
+ return speed > 0 ? delayMs / speed : delayMs;
21
40
  }
22
41
  async function writeSSEStream(res, chunks, optionsOrLatency) {
23
42
  const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
24
43
  const latency = opts.latency ?? 0;
25
44
  const profile = opts.streamingProfile;
45
+ const { recordedTimings, replaySpeed } = opts;
26
46
  const signal = opts.signal;
27
47
  const onChunkSent = opts.onChunkSent;
28
48
  if (res.writableEnded) return true;
@@ -31,7 +51,7 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
31
51
  res.setHeader("Connection", "keep-alive");
32
52
  let chunkIndex = 0;
33
53
  for (const chunk of chunks) {
34
- const chunkDelay = calculateDelay(chunkIndex, profile, latency);
54
+ const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
35
55
  if (chunkDelay > 0) await delay(chunkDelay, signal);
36
56
  if (signal?.aborted) return false;
37
57
  if (res.writableEnded) return true;
@@ -41,13 +61,31 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
41
61
  chunkIndex++;
42
62
  }
43
63
  if (!res.writableEnded) {
64
+ if (opts.usageChunk) res.write(`data: ${JSON.stringify(opts.usageChunk)}\n\n`);
44
65
  res.write("data: [DONE]\n\n");
45
66
  res.end();
46
67
  }
47
68
  return true;
48
69
  }
49
- function writeErrorResponse(res, status, body) {
50
- res.writeHead(status, { "Content-Type": "application/json" });
70
+ /**
71
+ * Default rate-limit response headers matching OpenAI's format.
72
+ * Values are static — aimock doesn't track actual request counts.
73
+ */
74
+ const RATE_LIMIT_HEADERS = {
75
+ "x-ratelimit-limit-requests": "60",
76
+ "x-ratelimit-limit-tokens": "150000",
77
+ "x-ratelimit-remaining-requests": "0",
78
+ "x-ratelimit-remaining-tokens": "0",
79
+ "x-ratelimit-reset-requests": "1s",
80
+ "x-ratelimit-reset-tokens": "6m0s"
81
+ };
82
+ function writeErrorResponse(res, status, body, options) {
83
+ const headers = { "Content-Type": "application/json" };
84
+ if (status === 429) {
85
+ headers["Retry-After"] = String(options?.retryAfter ?? 1);
86
+ Object.assign(headers, RATE_LIMIT_HEADERS);
87
+ }
88
+ res.writeHead(status, headers);
51
89
  res.end(body);
52
90
  }
53
91
 
@@ -1 +1 @@
1
- {"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n): number {\n if (!profile) return fallbackLatency ?? 0;\n\n let delayMs: number;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n return fallbackLatency ?? 0;\n }\n\n if (profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n }\n\n return Math.max(0, delayMs);\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\nexport function writeErrorResponse(res: http.ServerResponse, status: number, body: string): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(body);\n}\n"],"mappings":";;AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAUJ,SAAgB,eACd,YACA,SACA,iBACQ;AACR,KAAI,CAAC,QAAS,QAAO,mBAAmB;CAExC,IAAI;AACJ,KAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;UACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;KAEzB,QAAO,mBAAmB;AAG5B,KAAI,QAAQ,UAAU,QAAQ,SAAS,EACrC,YAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AAGnD,QAAO,KAAK,IAAI,GAAG,QAAQ;;AAG7B,eAAsB,eACpB,KACA,QACA,kBACkB;CAClB,MAAM,OACJ,OAAO,qBAAqB,WAAW,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE;CAChG,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK;CACrB,MAAM,SAAS,KAAK;CACpB,MAAM,cAAc,KAAK;AAEzB,KAAI,IAAI,cAAe,QAAO;AAC9B,KAAI,UAAU,gBAAgB,oBAAoB;AAClD,KAAI,UAAU,iBAAiB,WAAW;AAC1C,KAAI,UAAU,cAAc,aAAa;CAEzC,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,eAAe,YAAY,SAAS,QAAQ;AAC/D,MAAI,aAAa,EACf,OAAM,MAAM,YAAY,OAAO;AAEjC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,IAAI,cAAe,QAAO;AAC9B,MAAI,MAAM,SAAS,KAAK,UAAU,MAAM,CAAC,MAAM;AAC/C,iBAAe;AACf,MAAI,QAAQ,QAAS,QAAO;AAC5B;;AAGF,KAAI,CAAC,IAAI,eAAe;AACtB,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;;AAEX,QAAO;;AAGT,SAAgB,mBAAmB,KAA0B,QAAgB,MAAoB;AAC/F,KAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AAC7D,KAAI,IAAI,KAAK"}
1
+ {"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";;AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAcJ,SAAgB,eACd,YACA,SACA,iBACA,iBACA,aACQ;CACR,MAAM,QAAQ,eAAe;CAC7B,IAAI;AAEJ,KAAI,SAAS;EAEX,IAAI,cAAc;AAClB,MAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;WACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;OACpB;AACL,aAAU,mBAAmB;AAC7B,iBAAc;;AAGhB,MAAI,eAAe,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACvD,cAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AACjD,OAAI,UAAU,EAAG,WAAU;;YAEpB,gBAET,KAAI,eAAe,EACjB,WAAU,gBAAgB;MACrB;EACL,MAAM,MAAM,aAAa;AACzB,MAAI,MAAM,gBAAgB,mBAAmB,OAC3C,WAAU,gBAAgB,mBAAmB;OACxC;GAEL,MAAM,kBAAkB,gBAAgB,mBAAmB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;AACrF,aACE,gBAAgB,mBAAmB,SAAS,IACxC,kBAAkB,gBAAgB,mBAAmB,SACrD;;;KAIV,WAAU,mBAAmB;AAG/B,WAAU,KAAK,IAAI,GAAG,QAAQ;AAC9B,QAAO,QAAQ,IAAI,UAAU,QAAQ;;AAGvC,eAAsB,eACpB,KACA,QACA,kBACkB;CAClB,MAAM,OACJ,OAAO,qBAAqB,WAAW,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE;CAChG,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK;CACrB,MAAM,EAAE,iBAAiB,gBAAgB;CACzC,MAAM,SAAS,KAAK;CACpB,MAAM,cAAc,KAAK;AAEzB,KAAI,IAAI,cAAe,QAAO;AAC9B,KAAI,UAAU,gBAAgB,oBAAoB;AAClD,KAAI,UAAU,iBAAiB,WAAW;AAC1C,KAAI,UAAU,cAAc,aAAa;CAEzC,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,eAAe,YAAY,SAAS,SAAS,iBAAiB,YAAY;AAC7F,MAAI,aAAa,EACf,OAAM,MAAM,YAAY,OAAO;AAEjC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,IAAI,cAAe,QAAO;AAC9B,MAAI,MAAM,SAAS,KAAK,UAAU,MAAM,CAAC,MAAM;AAC/C,iBAAe;AACf,MAAI,QAAQ,QAAS,QAAO;AAC5B;;AAGF,KAAI,CAAC,IAAI,eAAe;AACtB,MAAI,KAAK,WACP,KAAI,MAAM,SAAS,KAAK,UAAU,KAAK,WAAW,CAAC,MAAM;AAE3D,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;;AAEX,QAAO;;;;;;AAOT,MAAM,qBAA6C;CACjD,8BAA8B;CAC9B,4BAA4B;CAC5B,kCAAkC;CAClC,gCAAgC;CAChC,8BAA8B;CAC9B,4BAA4B;CAC7B;AAOD,SAAgB,mBACd,KACA,QACA,MACA,SACM;CACN,MAAM,UAAkC,EAAE,gBAAgB,oBAAoB;AAC9E,KAAI,WAAW,KAAK;AAClB,UAAQ,iBAAiB,OAAO,SAAS,cAAc,EAAE;AACzD,SAAO,OAAO,SAAS,mBAAmB;;AAE5C,KAAI,UAAU,QAAQ,QAAQ;AAC9B,KAAI,IAAI,KAAK"}
@@ -1,4 +1,4 @@
1
- import { SSEChunk, StreamingProfile } from "./types.cjs";
1
+ import { RecordedTimings, SSEChunk, StreamingProfile } from "./types.cjs";
2
2
  import * as http$1 from "node:http";
3
3
 
4
4
  //#region src/sse-writer.d.ts
@@ -6,14 +6,22 @@ declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
6
6
  interface StreamOptions {
7
7
  latency?: number;
8
8
  streamingProfile?: StreamingProfile;
9
+ recordedTimings?: RecordedTimings;
10
+ replaySpeed?: number;
9
11
  signal?: AbortSignal;
10
12
  onChunkSent?: () => void;
13
+ /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
14
+ usageChunk?: SSEChunk;
11
15
  }
12
- declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number): number;
16
+ declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number, recordedTimings?: RecordedTimings, replaySpeed?: number): number;
13
17
  declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
14
- declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string): void;
18
+ interface ErrorResponseOptions {
19
+ /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */
20
+ retryAfter?: number;
21
+ }
22
+ declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string, options?: ErrorResponseOptions): void;
15
23
  //# sourceMappingURL=sse-writer.d.ts.map
16
24
 
17
25
  //#endregion
18
- export { StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
26
+ export { ErrorResponseOptions, StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
19
27
  //# sourceMappingURL=sse-writer.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse-writer.d.cts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;QAAsB,CAAA,EAkBhC,WAlBgC;aAAc,CAAA,EAAA,GAAA,GAAA,IAAA;;AAexC,iBAOD,cAAA,CAPc,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EASlB,gBATkB,EAAA,eAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAAA,iBA8BR,cAAA,CA9BQ,GAAA,EA+BvB,MAAA,CAAK,cA/BkB,EAAA,MAAA,EAgCpB,QAhCoB,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAiCA,aAjCA,CAAA,EAkC3B,OAlC2B,CAAA,OAAA,CAAA;AAET,iBAkEL,kBAAA,CAlEK,GAAA,EAkEmB,MAAA,CAAK,cAlExB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA"}
1
+ {"version":3,"file":"sse-writer.d.cts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;AAGV,iBAMK,cAAA,CANL,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAQC,gBARD,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EAUS,eAVT,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAGI,iBAsDO,cAAA,CAtDP,GAAA,EAuDR,MAAA,CAAK,cAvDG,EAAA,MAAA,EAwDL,QAxDK,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAyDe,aAzDf,CAAA,EA0DZ,OA1DY,CAAA,OAAA,CAAA;AAAQ,UA6GN,oBAAA,CA7GM;EAGP;EAAc,UAAA,CAAA,EAAA,MAAA;;AAIV,iBA2GJ,kBAAA,CA3GI,GAAA,EA4Gb,MAAA,CAAK,cA5GQ,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA+GR,oBA/GQ,CAAA,EAAA,IAAA"}
@@ -1,4 +1,4 @@
1
- import { SSEChunk, StreamingProfile } from "./types.js";
1
+ import { RecordedTimings, SSEChunk, StreamingProfile } from "./types.js";
2
2
  import * as http$1 from "node:http";
3
3
 
4
4
  //#region src/sse-writer.d.ts
@@ -6,14 +6,22 @@ declare function delay(ms: number, signal?: AbortSignal): Promise<void>;
6
6
  interface StreamOptions {
7
7
  latency?: number;
8
8
  streamingProfile?: StreamingProfile;
9
+ recordedTimings?: RecordedTimings;
10
+ replaySpeed?: number;
9
11
  signal?: AbortSignal;
10
12
  onChunkSent?: () => void;
13
+ /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
14
+ usageChunk?: SSEChunk;
11
15
  }
12
- declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number): number;
16
+ declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number, recordedTimings?: RecordedTimings, replaySpeed?: number): number;
13
17
  declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
14
- declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string): void;
18
+ interface ErrorResponseOptions {
19
+ /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */
20
+ retryAfter?: number;
21
+ }
22
+ declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string, options?: ErrorResponseOptions): void;
15
23
  //# sourceMappingURL=sse-writer.d.ts.map
16
24
 
17
25
  //#endregion
18
- export { StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
26
+ export { ErrorResponseOptions, StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
19
27
  //# sourceMappingURL=sse-writer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse-writer.d.ts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;QAAsB,CAAA,EAkBhC,WAlBgC;aAAc,CAAA,EAAA,GAAA,GAAA,IAAA;;AAexC,iBAOD,cAAA,CAPc,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EASlB,gBATkB,EAAA,eAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAAA,iBA8BR,cAAA,CA9BQ,GAAA,EA+BvB,MAAA,CAAK,cA/BkB,EAAA,MAAA,EAgCpB,QAhCoB,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAiCA,aAjCA,CAAA,EAkC3B,OAlC2B,CAAA,OAAA,CAAA;AAET,iBAkEL,kBAAA,CAlEK,GAAA,EAkEmB,MAAA,CAAK,cAlExB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA"}
1
+ {"version":3,"file":"sse-writer.d.ts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;AAGV,iBAMK,cAAA,CANL,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAQC,gBARD,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EAUS,eAVT,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAGI,iBAsDO,cAAA,CAtDP,GAAA,EAuDR,MAAA,CAAK,cAvDG,EAAA,MAAA,EAwDL,QAxDK,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAyDe,aAzDf,CAAA,EA0DZ,OA1DY,CAAA,OAAA,CAAA;AAAQ,UA6GN,oBAAA,CA7GM;EAGP;EAAc,UAAA,CAAA,EAAA,MAAA;;AAIV,iBA2GJ,kBAAA,CA3GI,GAAA,EA4Gb,MAAA,CAAK,cA5GQ,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA+GR,oBA/GQ,CAAA,EAAA,IAAA"}
@@ -9,19 +9,39 @@ function delay(ms, signal) {
9
9
  }, { once: true });
10
10
  });
11
11
  }
12
- function calculateDelay(chunkIndex, profile, fallbackLatency) {
13
- if (!profile) return fallbackLatency ?? 0;
12
+ function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, replaySpeed) {
13
+ const speed = replaySpeed ?? 1;
14
14
  let delayMs;
15
- if (chunkIndex === 0 && profile.ttft !== void 0) delayMs = profile.ttft;
16
- else if (profile.tps !== void 0 && profile.tps > 0) delayMs = 1e3 / profile.tps;
17
- else return fallbackLatency ?? 0;
18
- if (profile.jitter && profile.jitter > 0) delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;
19
- return Math.max(0, delayMs);
15
+ if (profile) {
16
+ let fromProfile = true;
17
+ if (chunkIndex === 0 && profile.ttft !== void 0) delayMs = profile.ttft;
18
+ else if (profile.tps !== void 0 && profile.tps > 0) delayMs = 1e3 / profile.tps;
19
+ else {
20
+ delayMs = fallbackLatency ?? 0;
21
+ fromProfile = false;
22
+ }
23
+ if (fromProfile && profile.jitter && profile.jitter > 0) {
24
+ delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;
25
+ if (delayMs < 0) delayMs = 0;
26
+ }
27
+ } else if (recordedTimings) if (chunkIndex === 0) delayMs = recordedTimings.ttftMs;
28
+ else {
29
+ const idx = chunkIndex - 1;
30
+ if (idx < recordedTimings.interChunkDelaysMs.length) delayMs = recordedTimings.interChunkDelaysMs[idx];
31
+ else {
32
+ const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);
33
+ delayMs = recordedTimings.interChunkDelaysMs.length > 0 ? totalInterChunk / recordedTimings.interChunkDelaysMs.length : 0;
34
+ }
35
+ }
36
+ else delayMs = fallbackLatency ?? 0;
37
+ delayMs = Math.max(0, delayMs);
38
+ return speed > 0 ? delayMs / speed : delayMs;
20
39
  }
21
40
  async function writeSSEStream(res, chunks, optionsOrLatency) {
22
41
  const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
23
42
  const latency = opts.latency ?? 0;
24
43
  const profile = opts.streamingProfile;
44
+ const { recordedTimings, replaySpeed } = opts;
25
45
  const signal = opts.signal;
26
46
  const onChunkSent = opts.onChunkSent;
27
47
  if (res.writableEnded) return true;
@@ -30,7 +50,7 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
30
50
  res.setHeader("Connection", "keep-alive");
31
51
  let chunkIndex = 0;
32
52
  for (const chunk of chunks) {
33
- const chunkDelay = calculateDelay(chunkIndex, profile, latency);
53
+ const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);
34
54
  if (chunkDelay > 0) await delay(chunkDelay, signal);
35
55
  if (signal?.aborted) return false;
36
56
  if (res.writableEnded) return true;
@@ -40,13 +60,31 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
40
60
  chunkIndex++;
41
61
  }
42
62
  if (!res.writableEnded) {
63
+ if (opts.usageChunk) res.write(`data: ${JSON.stringify(opts.usageChunk)}\n\n`);
43
64
  res.write("data: [DONE]\n\n");
44
65
  res.end();
45
66
  }
46
67
  return true;
47
68
  }
48
- function writeErrorResponse(res, status, body) {
49
- res.writeHead(status, { "Content-Type": "application/json" });
69
+ /**
70
+ * Default rate-limit response headers matching OpenAI's format.
71
+ * Values are static — aimock doesn't track actual request counts.
72
+ */
73
+ const RATE_LIMIT_HEADERS = {
74
+ "x-ratelimit-limit-requests": "60",
75
+ "x-ratelimit-limit-tokens": "150000",
76
+ "x-ratelimit-remaining-requests": "0",
77
+ "x-ratelimit-remaining-tokens": "0",
78
+ "x-ratelimit-reset-requests": "1s",
79
+ "x-ratelimit-reset-tokens": "6m0s"
80
+ };
81
+ function writeErrorResponse(res, status, body, options) {
82
+ const headers = { "Content-Type": "application/json" };
83
+ if (status === 429) {
84
+ headers["Retry-After"] = String(options?.retryAfter ?? 1);
85
+ Object.assign(headers, RATE_LIMIT_HEADERS);
86
+ }
87
+ res.writeHead(status, headers);
50
88
  res.end(body);
51
89
  }
52
90
 
@@ -1 +1 @@
1
- {"version":3,"file":"sse-writer.js","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n): number {\n if (!profile) return fallbackLatency ?? 0;\n\n let delayMs: number;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n return fallbackLatency ?? 0;\n }\n\n if (profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n }\n\n return Math.max(0, delayMs);\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\nexport function writeErrorResponse(res: http.ServerResponse, status: number, body: string): void {\n res.writeHead(status, { \"Content-Type\": \"application/json\" });\n res.end(body);\n}\n"],"mappings":";AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAUJ,SAAgB,eACd,YACA,SACA,iBACQ;AACR,KAAI,CAAC,QAAS,QAAO,mBAAmB;CAExC,IAAI;AACJ,KAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;UACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;KAEzB,QAAO,mBAAmB;AAG5B,KAAI,QAAQ,UAAU,QAAQ,SAAS,EACrC,YAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AAGnD,QAAO,KAAK,IAAI,GAAG,QAAQ;;AAG7B,eAAsB,eACpB,KACA,QACA,kBACkB;CAClB,MAAM,OACJ,OAAO,qBAAqB,WAAW,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE;CAChG,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK;CACrB,MAAM,SAAS,KAAK;CACpB,MAAM,cAAc,KAAK;AAEzB,KAAI,IAAI,cAAe,QAAO;AAC9B,KAAI,UAAU,gBAAgB,oBAAoB;AAClD,KAAI,UAAU,iBAAiB,WAAW;AAC1C,KAAI,UAAU,cAAc,aAAa;CAEzC,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,eAAe,YAAY,SAAS,QAAQ;AAC/D,MAAI,aAAa,EACf,OAAM,MAAM,YAAY,OAAO;AAEjC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,IAAI,cAAe,QAAO;AAC9B,MAAI,MAAM,SAAS,KAAK,UAAU,MAAM,CAAC,MAAM;AAC/C,iBAAe;AACf,MAAI,QAAQ,QAAS,QAAO;AAC5B;;AAGF,KAAI,CAAC,IAAI,eAAe;AACtB,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;;AAEX,QAAO;;AAGT,SAAgB,mBAAmB,KAA0B,QAAgB,MAAoB;AAC/F,KAAI,UAAU,QAAQ,EAAE,gBAAgB,oBAAoB,CAAC;AAC7D,KAAI,IAAI,KAAK"}
1
+ {"version":3,"file":"sse-writer.js","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n}\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;AAcJ,SAAgB,eACd,YACA,SACA,iBACA,iBACA,aACQ;CACR,MAAM,QAAQ,eAAe;CAC7B,IAAI;AAEJ,KAAI,SAAS;EAEX,IAAI,cAAc;AAClB,MAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;WACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;OACpB;AACL,aAAU,mBAAmB;AAC7B,iBAAc;;AAGhB,MAAI,eAAe,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACvD,cAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AACjD,OAAI,UAAU,EAAG,WAAU;;YAEpB,gBAET,KAAI,eAAe,EACjB,WAAU,gBAAgB;MACrB;EACL,MAAM,MAAM,aAAa;AACzB,MAAI,MAAM,gBAAgB,mBAAmB,OAC3C,WAAU,gBAAgB,mBAAmB;OACxC;GAEL,MAAM,kBAAkB,gBAAgB,mBAAmB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;AACrF,aACE,gBAAgB,mBAAmB,SAAS,IACxC,kBAAkB,gBAAgB,mBAAmB,SACrD;;;KAIV,WAAU,mBAAmB;AAG/B,WAAU,KAAK,IAAI,GAAG,QAAQ;AAC9B,QAAO,QAAQ,IAAI,UAAU,QAAQ;;AAGvC,eAAsB,eACpB,KACA,QACA,kBACkB;CAClB,MAAM,OACJ,OAAO,qBAAqB,WAAW,EAAE,SAAS,kBAAkB,GAAI,oBAAoB,EAAE;CAChG,MAAM,UAAU,KAAK,WAAW;CAChC,MAAM,UAAU,KAAK;CACrB,MAAM,EAAE,iBAAiB,gBAAgB;CACzC,MAAM,SAAS,KAAK;CACpB,MAAM,cAAc,KAAK;AAEzB,KAAI,IAAI,cAAe,QAAO;AAC9B,KAAI,UAAU,gBAAgB,oBAAoB;AAClD,KAAI,UAAU,iBAAiB,WAAW;AAC1C,KAAI,UAAU,cAAc,aAAa;CAEzC,IAAI,aAAa;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,aAAa,eAAe,YAAY,SAAS,SAAS,iBAAiB,YAAY;AAC7F,MAAI,aAAa,EACf,OAAM,MAAM,YAAY,OAAO;AAEjC,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,IAAI,cAAe,QAAO;AAC9B,MAAI,MAAM,SAAS,KAAK,UAAU,MAAM,CAAC,MAAM;AAC/C,iBAAe;AACf,MAAI,QAAQ,QAAS,QAAO;AAC5B;;AAGF,KAAI,CAAC,IAAI,eAAe;AACtB,MAAI,KAAK,WACP,KAAI,MAAM,SAAS,KAAK,UAAU,KAAK,WAAW,CAAC,MAAM;AAE3D,MAAI,MAAM,mBAAmB;AAC7B,MAAI,KAAK;;AAEX,QAAO;;;;;;AAOT,MAAM,qBAA6C;CACjD,8BAA8B;CAC9B,4BAA4B;CAC5B,kCAAkC;CAClC,gCAAgC;CAChC,8BAA8B;CAC9B,4BAA4B;CAC7B;AAOD,SAAgB,mBACd,KACA,QACA,MACA,SACM;CACN,MAAM,UAAkC,EAAE,gBAAgB,oBAAoB;AAC9E,KAAI,WAAW,KAAK;AAClB,UAAQ,iBAAiB,OAAO,SAAS,cAAc,EAAE;AACzD,SAAO,OAAO,SAAS,mBAAmB;;AAE5C,KAAI,UAAU,QAAQ,QAAQ;AAC9B,KAAI,IAAI,KAAK"}
@@ -36,9 +36,10 @@ function extractFormField(raw, fieldName, boundary) {
36
36
  if (cdMatch && cdMatch[1] === fieldName) return body.replace(/\r\n$/, "");
37
37
  }
38
38
  }
39
- async function handleTranscription(req, res, raw, fixtures, journal, defaults, setCorsHeaders) {
39
+ async function handleTranscription(req, res, raw, fixtures, journal, defaults, setCorsHeaders, endpointType = "transcription") {
40
40
  setCorsHeaders(res);
41
- const path = req.url ?? "/v1/audio/transcriptions";
41
+ const defaultPath = endpointType === "translation" ? "/v1/audio/translations" : "/v1/audio/transcriptions";
42
+ const path = req.url ?? defaultPath;
42
43
  const method = req.method ?? "POST";
43
44
  const boundary = extractBoundary(Array.isArray(req.headers["content-type"]) ? req.headers["content-type"][0] : req.headers["content-type"]);
44
45
  const model = extractFormField(raw, "model", boundary) ?? "whisper-1";
@@ -46,7 +47,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
46
47
  const syntheticReq = {
47
48
  model,
48
49
  messages: [],
49
- _endpointType: "transcription"
50
+ _endpointType: endpointType
50
51
  };
51
52
  const testId = require_helpers.getTestId(req);
52
53
  const fixture = require_router.matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
@@ -81,7 +82,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
81
82
  return;
82
83
  }
83
84
  if (defaults.record) {
84
- const outcome = await require_recorder.proxyAndRecord(req, res, syntheticReq, "openai", req.url ?? "/v1/audio/transcriptions", fixtures, defaults, raw);
85
+ const outcome = await require_recorder.proxyAndRecord(req, res, syntheticReq, "openai", req.url ?? defaultPath, fixtures, defaults, raw);
85
86
  if (outcome === "handled_by_hook") return;
86
87
  if (outcome !== "not_configured") {
87
88
  journal.add({
@@ -129,7 +130,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
129
130
  fixture
130
131
  }
131
132
  });
132
- require_sse_writer.writeErrorResponse(res, status, require_helpers.serializeErrorResponse(response));
133
+ require_sse_writer.writeErrorResponse(res, status, require_helpers.serializeErrorResponse(response), { retryAfter: response.retryAfter });
133
134
  return;
134
135
  }
135
136
  if (!require_helpers.isTranscriptionResponse(response)) {
@@ -162,7 +163,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
162
163
  const t = response.transcription;
163
164
  if (responseFormat === "verbose_json" || t.words != null || t.segments != null) {
164
165
  const verboseBody = {
165
- task: "transcribe",
166
+ task: endpointType === "translation" ? "translate" : "transcribe",
166
167
  language: t.language ?? "english",
167
168
  duration: t.duration ?? 0,
168
169
  text: t.text
@@ -178,5 +179,7 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
178
179
  }
179
180
 
180
181
  //#endregion
182
+ exports.extractBoundary = extractBoundary;
183
+ exports.extractFormField = extractFormField;
181
184
  exports.handleTranscription = handleTranscription;
182
185
  //# sourceMappingURL=transcription.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.cjs","names":["getTestId","matchFixture","applyChaos","flattenHeaders","resolveStrictMode","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse"],"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} from \"./helpers.js\";\nimport { matchFixture } 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\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nfunction extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\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 */\nfunction 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 const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${fieldName}\"[^\\\\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): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/transcriptions\";\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\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: \"transcription\",\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/transcriptions\",\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 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 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 const useVerbose = responseFormat === \"verbose_json\" || t.words != null || t.segments != null;\n\n if (useVerbose) {\n const verboseBody: Record<string, unknown> = {\n task: \"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(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"transcription.cjs","names":["getTestId","matchFixture","applyChaos","flattenHeaders","resolveStrictMode","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isTranscriptionResponse"],"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} from \"./helpers.js\";\nimport { matchFixture } 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\n/**\n * Extract the multipart boundary string from a Content-Type header.\n */\nexport function extractBoundary(contentType: string | undefined): string | undefined {\n if (!contentType) return undefined;\n const match = contentType.match(/boundary=([^\\s;]+)/i);\n return match?.[1];\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 const pattern = new RegExp(\n `Content-Disposition:\\\\s*form-data;[^\\\\r\\\\n]*name=\"${fieldName}\"[^\\\\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\n const syntheticReq: ChatCompletionRequest = {\n model,\n messages: [],\n _endpointType: endpointType,\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\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 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 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"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 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 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(JSON.stringify({ text: t.text }));\n }\n}\n"],"mappings":"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"}
@@ -3,9 +3,9 @@ import { Fixture, HandlerDefaults } from "./types.cjs";
3
3
  import * as http$1 from "node:http";
4
4
 
5
5
  //#region src/transcription.d.ts
6
- declare function handleTranscription(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void): Promise<void>;
7
- //# sourceMappingURL=transcription.d.ts.map
8
6
 
7
+ declare function handleTranscription(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, endpointType?: "transcription" | "translation"): Promise<void>;
8
+ //# sourceMappingURL=transcription.d.ts.map
9
9
  //#endregion
10
10
  export { handleTranscription };
11
11
  //# sourceMappingURL=transcription.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.d.cts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;iBA0EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0BAC1B"}
1
+ {"version":3,"file":"transcription.d.cts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;iBA0EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0EAE1B"}
@@ -3,9 +3,9 @@ import { Fixture, HandlerDefaults } from "./types.js";
3
3
  import * as http$1 from "node:http";
4
4
 
5
5
  //#region src/transcription.d.ts
6
- declare function handleTranscription(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void): Promise<void>;
7
- //# sourceMappingURL=transcription.d.ts.map
8
6
 
7
+ declare function handleTranscription(req: http$1.IncomingMessage, res: http$1.ServerResponse, raw: string, fixtures: Fixture[], journal: Journal, defaults: HandlerDefaults, setCorsHeaders: (res: http$1.ServerResponse) => void, endpointType?: "transcription" | "translation"): Promise<void>;
8
+ //# sourceMappingURL=transcription.d.ts.map
9
9
  //#endregion
10
10
  export { handleTranscription };
11
11
  //# sourceMappingURL=transcription.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transcription.d.ts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;iBA0EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0BAC1B"}
1
+ {"version":3,"file":"transcription.d.ts","names":[],"sources":["../src/transcription.ts"],"sourcesContent":[],"mappings":";;;;;;iBA0EsB,mBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0EAE1B"}