@copilotkit/aimock 1.24.1 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +17 -11
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/bedrock-converse.cjs +2 -2
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +2 -2
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +2 -2
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +2 -2
- package/dist/bedrock.js.map +1 -1
- package/dist/cli.cjs +25 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +25 -1
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +198 -1
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +199 -3
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/elevenlabs-audio.cjs +173 -1
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +173 -2
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +1 -1
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.js +1 -1
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +2 -4
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +2 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +2 -2
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +2 -2
- package/dist/fal.js.map +1 -1
- package/dist/gemini-embeddings.cjs +166 -0
- package/dist/gemini-embeddings.cjs.map +1 -0
- package/dist/gemini-embeddings.js +166 -0
- package/dist/gemini-embeddings.js.map +1 -0
- package/dist/gemini-interactions.cjs +1 -1
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.js +1 -1
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +1 -1
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.js +1 -1
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +70 -33
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +9 -5
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +9 -5
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +68 -34
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +295 -13
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts +9 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts +9 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +294 -14
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/llmock.cjs +15 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +2 -0
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +2 -0
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +15 -0
- package/dist/llmock.js.map +1 -1
- package/dist/messages.cjs +1 -1
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.js +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +2 -0
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +2 -0
- package/dist/metrics.js.map +1 -1
- package/dist/ollama.cjs +189 -2
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +190 -4
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +11 -4
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.js +11 -4
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +1 -1
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.js +1 -1
- package/dist/responses.js.map +1 -1
- package/dist/server.cjs +188 -48
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +193 -53
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +1 -1
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +1 -1
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +20 -2
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +8 -2
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +8 -2
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +20 -2
- package/dist/sse-writer.js.map +1 -1
- package/dist/transcription.cjs +9 -6
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts +2 -2
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts +2 -2
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +8 -7
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +28 -2
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +28 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/video.cjs +1 -1
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +1 -1
- package/dist/video.js.map +1 -1
- package/package.json +2 -2
package/dist/speech.cjs.map
CHANGED
|
@@ -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)) {
|
package/dist/speech.js.map
CHANGED
|
@@ -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":";;;;;;;AA4BA,eAAsB,aACpB,KACA,KACA,KACA,UACA,SACA,UACA,gBACe;AACf,gBAAe,IAAI;CACnB,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,SAAS,IAAI,UAAU;CAE7B,IAAI;AACJ,KAAI;AACF,cAAY,KAAK,MAAM,IAAI;UACpB,UAAU;EACjB,MAAM,SAAS,oBAAoB,QAAQ,SAAS,UAAU;AAC9D,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS,mBAAmB;GAC5B,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;AAGF,KAAI,CAAC,UAAU,OAAO;AACpB,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GAAE,SAAS;GAAuC,MAAM;GAAyB,EACzF,CAAC,CACH;AACD;;CAGF,MAAM,eAAsC;EAC1C,OAAO,UAAU,SAAS;EAC1B,UAAU,CAAC;GAAE,MAAM;GAAQ,SAAS,UAAU;GAAO,CAAC;EACtD,eAAe;EAChB;CAED,MAAM,SAAS,UAAU,IAAI;CAC7B,MAAM,UAAU,aACd,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,WACE,KACA,SACA,SAAS,OACT,IAAI,SACJ,SACA;EAAE;EAAQ;EAAM,SAAS,eAAe,IAAI,QAAQ;EAAE,MAAM;EAAc,EAC1E,UAAU,YAAY,SACtB,SAAS,UACT,SAAS,OACV,CAED;AAEF,KAAI,CAAC,SAAS;AAEZ,MADwB,kBAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;AACnB,WAAQ,IAAI;IACV;IACA;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KACR,QAAQ;KACR,SAAS;KACT,GAAG,oBAAoB,SAAS,QAAQ,IAAI,QAAQ;KACrD;IACF,CAAC;AACF,sBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;IACL,SAAS;IACT,MAAM;IACN,MAAM;IACP,EACF,CAAC,CACH;AACD;;AAEF,MAAI,SAAS,QAAQ;GACnB,MAAM,UAAU,MAAM,eACpB,KACA,KACA,cACA,UACA,IAAI,OAAO,oBACX,UACA,UACA,IACD;AACD,OAAI,YAAY,kBAAmB;AACnC,OAAI,YAAY,kBAAkB;AAChC,YAAQ,IAAI;KACV;KACA;KACA,SAAS,eAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ,IAAI,cAAc;MAAK,SAAS;MAAM,QAAQ;MAAS;KAC5E,CAAC;AACF;;;AAIJ,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IACR,QAAQ;IACR,SAAS;IACT,GAAG,oBAAoB,SAAS,QAAQ,IAAI,QAAQ;IACrD;GACF,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,WAAW,MAAM,gBAAgB,SAAS,aAAa;AAE7D,KAAI,gBAAgB,SAAS,EAAE;EAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE;IAAQ;IAAS;GAC9B,CAAC;AACF,qBAAmB,KAAK,QAAQ,uBAAuB,SAAS,CAAC;
|
|
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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"}
|
package/dist/sse-writer.cjs
CHANGED
|
@@ -41,13 +41,31 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
|
41
41
|
chunkIndex++;
|
|
42
42
|
}
|
|
43
43
|
if (!res.writableEnded) {
|
|
44
|
+
if (opts.usageChunk) res.write(`data: ${JSON.stringify(opts.usageChunk)}\n\n`);
|
|
44
45
|
res.write("data: [DONE]\n\n");
|
|
45
46
|
res.end();
|
|
46
47
|
}
|
|
47
48
|
return true;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Default rate-limit response headers matching OpenAI's format.
|
|
52
|
+
* Values are static — aimock doesn't track actual request counts.
|
|
53
|
+
*/
|
|
54
|
+
const RATE_LIMIT_HEADERS = {
|
|
55
|
+
"x-ratelimit-limit-requests": "60",
|
|
56
|
+
"x-ratelimit-limit-tokens": "150000",
|
|
57
|
+
"x-ratelimit-remaining-requests": "0",
|
|
58
|
+
"x-ratelimit-remaining-tokens": "0",
|
|
59
|
+
"x-ratelimit-reset-requests": "1s",
|
|
60
|
+
"x-ratelimit-reset-tokens": "6m0s"
|
|
61
|
+
};
|
|
62
|
+
function writeErrorResponse(res, status, body, options) {
|
|
63
|
+
const headers = { "Content-Type": "application/json" };
|
|
64
|
+
if (status === 429) {
|
|
65
|
+
headers["Retry-After"] = String(options?.retryAfter ?? 1);
|
|
66
|
+
Object.assign(headers, RATE_LIMIT_HEADERS);
|
|
67
|
+
}
|
|
68
|
+
res.writeHead(status, headers);
|
|
51
69
|
res.end(body);
|
|
52
70
|
}
|
|
53
71
|
|
package/dist/sse-writer.cjs.map
CHANGED
|
@@ -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
|
|
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 /** 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): 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 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;;AAYJ,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,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"}
|
package/dist/sse-writer.d.cts
CHANGED
|
@@ -8,12 +8,18 @@ interface StreamOptions {
|
|
|
8
8
|
streamingProfile?: StreamingProfile;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
10
|
onChunkSent?: () => void;
|
|
11
|
+
/** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
|
|
12
|
+
usageChunk?: SSEChunk;
|
|
11
13
|
}
|
|
12
14
|
declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number): number;
|
|
13
15
|
declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
|
|
14
|
-
|
|
16
|
+
interface ErrorResponseOptions {
|
|
17
|
+
/** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */
|
|
18
|
+
retryAfter?: number;
|
|
19
|
+
}
|
|
20
|
+
declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string, options?: ErrorResponseOptions): void;
|
|
15
21
|
//# sourceMappingURL=sse-writer.d.ts.map
|
|
16
22
|
|
|
17
23
|
//#endregion
|
|
18
|
-
export { StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
|
|
24
|
+
export { ErrorResponseOptions, StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
|
|
19
25
|
//# 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;;
|
|
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;EAAO;EAe/C,UAAA,CAAA,EAMF,QANe;;AAET,iBAOL,cAAA,CAPK,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAST,gBATS,EAAA,eAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AACV,iBA6BW,cAAA,CA7BX,GAAA,EA8BJ,MAAA,CAAK,cA9BD,EAAA,MAAA,EA+BD,QA/BC,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAgCmB,aAhCnB,CAAA,EAiCR,OAjCQ,CAAA,OAAA,CAAA;AAGI,UAgFE,oBAAA,CAhFF;EAAQ;EAGP,UAAA,CAAA,EAAA,MAAc;AAuB9B;AAAoC,iBA2DpB,kBAAA,CA3DoB,GAAA,EA4D7B,MAAA,CAAK,cA5DwB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA+DxB,oBA/DwB,CAAA,EAAA,IAAA"}
|
package/dist/sse-writer.d.ts
CHANGED
|
@@ -8,12 +8,18 @@ interface StreamOptions {
|
|
|
8
8
|
streamingProfile?: StreamingProfile;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
10
|
onChunkSent?: () => void;
|
|
11
|
+
/** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */
|
|
12
|
+
usageChunk?: SSEChunk;
|
|
11
13
|
}
|
|
12
14
|
declare function calculateDelay(chunkIndex: number, profile?: StreamingProfile, fallbackLatency?: number): number;
|
|
13
15
|
declare function writeSSEStream(res: http$1.ServerResponse, chunks: SSEChunk[], optionsOrLatency?: number | StreamOptions): Promise<boolean>;
|
|
14
|
-
|
|
16
|
+
interface ErrorResponseOptions {
|
|
17
|
+
/** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */
|
|
18
|
+
retryAfter?: number;
|
|
19
|
+
}
|
|
20
|
+
declare function writeErrorResponse(res: http$1.ServerResponse, status: number, body: string, options?: ErrorResponseOptions): void;
|
|
15
21
|
//# sourceMappingURL=sse-writer.d.ts.map
|
|
16
22
|
|
|
17
23
|
//#endregion
|
|
18
|
-
export { StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
|
|
24
|
+
export { ErrorResponseOptions, StreamOptions, calculateDelay, delay, writeErrorResponse, writeSSEStream };
|
|
19
25
|
//# sourceMappingURL=sse-writer.d.ts.map
|
package/dist/sse-writer.d.ts.map
CHANGED
|
@@ -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;;
|
|
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;EAAO;EAe/C,UAAA,CAAA,EAMF,QANe;;AAET,iBAOL,cAAA,CAPK,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EAST,gBATS,EAAA,eAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AACV,iBA6BW,cAAA,CA7BX,GAAA,EA8BJ,MAAA,CAAK,cA9BD,EAAA,MAAA,EA+BD,QA/BC,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAgCmB,aAhCnB,CAAA,EAiCR,OAjCQ,CAAA,OAAA,CAAA;AAGI,UAgFE,oBAAA,CAhFF;EAAQ;EAGP,UAAA,CAAA,EAAA,MAAc;AAuB9B;AAAoC,iBA2DpB,kBAAA,CA3DoB,GAAA,EA4D7B,MAAA,CAAK,cA5DwB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA+DxB,oBA/DwB,CAAA,EAAA,IAAA"}
|
package/dist/sse-writer.js
CHANGED
|
@@ -40,13 +40,31 @@ async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
|
40
40
|
chunkIndex++;
|
|
41
41
|
}
|
|
42
42
|
if (!res.writableEnded) {
|
|
43
|
+
if (opts.usageChunk) res.write(`data: ${JSON.stringify(opts.usageChunk)}\n\n`);
|
|
43
44
|
res.write("data: [DONE]\n\n");
|
|
44
45
|
res.end();
|
|
45
46
|
}
|
|
46
47
|
return true;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Default rate-limit response headers matching OpenAI's format.
|
|
51
|
+
* Values are static — aimock doesn't track actual request counts.
|
|
52
|
+
*/
|
|
53
|
+
const RATE_LIMIT_HEADERS = {
|
|
54
|
+
"x-ratelimit-limit-requests": "60",
|
|
55
|
+
"x-ratelimit-limit-tokens": "150000",
|
|
56
|
+
"x-ratelimit-remaining-requests": "0",
|
|
57
|
+
"x-ratelimit-remaining-tokens": "0",
|
|
58
|
+
"x-ratelimit-reset-requests": "1s",
|
|
59
|
+
"x-ratelimit-reset-tokens": "6m0s"
|
|
60
|
+
};
|
|
61
|
+
function writeErrorResponse(res, status, body, options) {
|
|
62
|
+
const headers = { "Content-Type": "application/json" };
|
|
63
|
+
if (status === 429) {
|
|
64
|
+
headers["Retry-After"] = String(options?.retryAfter ?? 1);
|
|
65
|
+
Object.assign(headers, RATE_LIMIT_HEADERS);
|
|
66
|
+
}
|
|
67
|
+
res.writeHead(status, headers);
|
|
50
68
|
res.end(body);
|
|
51
69
|
}
|
|
52
70
|
|
package/dist/sse-writer.js.map
CHANGED
|
@@ -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
|
|
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 /** 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): 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 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;;AAYJ,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,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"}
|
package/dist/transcription.cjs
CHANGED
|
@@ -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
|
|
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:
|
|
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 ??
|
|
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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"}
|
package/dist/transcription.d.cts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/transcription.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/transcription.js
CHANGED
|
@@ -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
|
|
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:
|
|
50
|
+
_endpointType: endpointType
|
|
50
51
|
};
|
|
51
52
|
const testId = getTestId(req);
|
|
52
53
|
const fixture = 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 proxyAndRecord(req, res, syntheticReq, "openai", req.url ??
|
|
85
|
+
const outcome = await 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
|
-
writeErrorResponse(res, status, serializeErrorResponse(response));
|
|
133
|
+
writeErrorResponse(res, status, serializeErrorResponse(response), { retryAfter: response.retryAfter });
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
if (!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,5 @@ async function handleTranscription(req, res, raw, fixtures, journal, defaults, s
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
//#endregion
|
|
181
|
-
export { handleTranscription };
|
|
182
|
+
export { extractBoundary, extractFormField, handleTranscription };
|
|
182
183
|
//# sourceMappingURL=transcription.js.map
|
|
@@ -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} 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.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} 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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"}
|