@copilotkit/aimock 1.45.1 → 1.46.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 +13 -0
- package/README.md +7 -1
- package/dist/agui-mock.cjs +25 -25
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +61 -80
- 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 +58 -77
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +65 -97
- 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 +62 -94
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +49 -74
- 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 +47 -72
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- 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 +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +37 -42
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +37 -42
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +50 -35
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +49 -34
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +57 -67
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +56 -67
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +11 -68
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +12 -67
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-handler.cjs +7 -3
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +7 -3
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +111 -96
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +109 -95
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- 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 +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +128 -68
- 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 +124 -64
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +51 -74
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +37 -60
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +176 -95
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +45 -58
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +44 -57
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +4 -0
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3 -0
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +222 -146
- 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 +223 -147
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +7 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +7 -1
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +12 -10
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +11 -9
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +62 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +62 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +11 -3
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +10 -2
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +28 -54
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +25 -51
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +8 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +7 -5
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +25 -16
package/dist/speech.cjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const require_errors = require('./response/errors.cjs');
|
|
2
|
+
const require_policy = require('./response/policy.cjs');
|
|
1
3
|
const require_helpers = require('./helpers.cjs');
|
|
2
4
|
const require_sse_writer = require('./sse-writer.cjs');
|
|
3
5
|
const require_chaos = require('./chaos.cjs');
|
|
@@ -24,11 +26,11 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
24
26
|
fixture: null
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
29
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("openai", "validation", {
|
|
28
30
|
message: `Malformed JSON: ${detail}`,
|
|
29
31
|
type: "invalid_request_error",
|
|
30
32
|
code: "invalid_json"
|
|
31
|
-
}
|
|
33
|
+
}).body);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
if (!require_helpers.isJsonObject(speechReq)) {
|
|
@@ -42,10 +44,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
42
44
|
fixture: null
|
|
43
45
|
}
|
|
44
46
|
});
|
|
45
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
47
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("openai", "validation", {
|
|
46
48
|
message: "Request body must be a JSON object",
|
|
47
49
|
type: "invalid_request_error"
|
|
48
|
-
}
|
|
50
|
+
}).body);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
const input = speechReq.input;
|
|
@@ -61,10 +63,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
61
63
|
fixture: null
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
66
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("openai", "validation", {
|
|
65
67
|
message: !input ? "Missing required parameter: 'input'" : "Invalid parameter: 'input' must be a string or a nonempty array of content parts",
|
|
66
68
|
type: "invalid_request_error"
|
|
67
|
-
}
|
|
69
|
+
}).body);
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
const syntheticReq = {
|
|
@@ -89,7 +91,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
89
91
|
body: syntheticReq
|
|
90
92
|
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
91
93
|
if (!fixture) {
|
|
92
|
-
if (
|
|
94
|
+
if (require_policy.resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
93
95
|
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
94
96
|
defaults.logger.error(require_helpers.strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
|
|
95
97
|
journal.add({
|
|
@@ -100,14 +102,14 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
100
102
|
response: {
|
|
101
103
|
status: 503,
|
|
102
104
|
fixture: null,
|
|
103
|
-
...
|
|
105
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
|
-
require_sse_writer.writeErrorResponse(res, 503,
|
|
108
|
+
require_sse_writer.writeErrorResponse(res, 503, require_errors.renderError("openai", "strict-miss", {
|
|
107
109
|
message: strictMessage,
|
|
108
110
|
type: "invalid_request_error",
|
|
109
111
|
code: "no_fixture_match"
|
|
110
|
-
}
|
|
112
|
+
}).body);
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
115
|
if (defaults.record) {
|
|
@@ -136,14 +138,14 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
136
138
|
response: {
|
|
137
139
|
status: 404,
|
|
138
140
|
fixture: null,
|
|
139
|
-
...
|
|
141
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
140
142
|
}
|
|
141
143
|
});
|
|
142
|
-
require_sse_writer.writeErrorResponse(res, 404,
|
|
144
|
+
require_sse_writer.writeErrorResponse(res, 404, require_errors.renderError("openai", "fixture-miss", {
|
|
143
145
|
message: "No fixture matched",
|
|
144
146
|
type: "invalid_request_error",
|
|
145
147
|
code: "no_fixture_match"
|
|
146
|
-
}
|
|
148
|
+
}).body);
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
151
|
const response = await require_helpers.resolveResponse(fixture, syntheticReq);
|
|
@@ -159,7 +161,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
159
161
|
fixture
|
|
160
162
|
}
|
|
161
163
|
});
|
|
162
|
-
require_sse_writer.writeErrorResponse(res, status,
|
|
164
|
+
require_sse_writer.writeErrorResponse(res, status, require_errors.renderError("openai", "fixture", response.error).body, { retryAfter: response.retryAfter });
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
167
|
if (!require_helpers.isAudioResponse(response)) {
|
|
@@ -173,10 +175,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
173
175
|
fixture
|
|
174
176
|
}
|
|
175
177
|
});
|
|
176
|
-
require_sse_writer.writeErrorResponse(res, 500,
|
|
178
|
+
require_sse_writer.writeErrorResponse(res, 500, require_errors.renderError("openai", "unknown-response", {
|
|
177
179
|
message: "Fixture response is not an audio type",
|
|
178
180
|
type: "server_error"
|
|
179
|
-
}
|
|
181
|
+
}).body);
|
|
180
182
|
return;
|
|
181
183
|
}
|
|
182
184
|
if (typeof response.audio !== "string") {
|
|
@@ -190,10 +192,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
190
192
|
fixture
|
|
191
193
|
}
|
|
192
194
|
});
|
|
193
|
-
require_sse_writer.writeErrorResponse(res, 500,
|
|
195
|
+
require_sse_writer.writeErrorResponse(res, 500, require_errors.renderError("openai", "unknown-response", {
|
|
194
196
|
message: "Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }",
|
|
195
197
|
type: "server_error"
|
|
196
|
-
}
|
|
198
|
+
}).body);
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
201
|
journal.add({
|
package/dist/speech.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"speech.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","serializeErrorResponse","isAudioResponse","FORMAT_TO_CONTENT_TYPE"],"sources":["../src/speech.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const input: unknown = speechReq.input;\n const validInput =\n typeof input === \"string\" ||\n (Array.isArray(input) &&\n input.length > 0 &&\n input.every(\n (part: unknown) =>\n isJsonObject(part) &&\n typeof part.type === \"string\" &&\n (!(\"text\" in part) || typeof part.text === \"string\"),\n ));\n if (!input || !validInput) {\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: !input\n ? \"Missing required parameter: 'input'\"\n : \"Invalid parameter: 'input' must be a string or a nonempty array of content parts\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"speech.cjs","names":["flattenHeaders","renderError","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveRequestDecisions","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","resolveResponse","isErrorResponse","isAudioResponse","FORMAT_TO_CONTENT_TYPE"],"sources":["../src/speech.ts"],"sourcesContent":["import { renderError } from \"./response/errors.js\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"openai\", \"validation\", {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"openai\", \"validation\", {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n const input: unknown = speechReq.input;\n const validInput =\n typeof input === \"string\" ||\n (Array.isArray(input) &&\n input.length > 0 &&\n input.every(\n (part: unknown) =>\n isJsonObject(part) &&\n typeof part.type === \"string\" &&\n (!(\"text\" in part) || typeof part.text === \"string\"),\n ));\n if (!input || !validInput) {\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 renderError(\"openai\", \"validation\", {\n message: !input\n ? \"Missing required parameter: 'input'\"\n : \"Invalid parameter: 'input' must be a string or a nonempty array of content parts\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss;\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n renderError(\"openai\", \"strict-miss\", {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\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 renderError(\"openai\", \"fixture-miss\", {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\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, renderError(\"openai\", \"fixture\", response.error).body, {\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 renderError(\"openai\", \"unknown-response\", {\n message: \"Fixture response is not an audio type\",\n type: \"server_error\",\n }).body,\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 renderError(\"openai\", \"unknown-response\", {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n }).body,\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.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"speech.d.cts","names":[],"sources":["../src/speech.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"speech.d.cts","names":[],"sources":["../src/speech.ts"],"sourcesContent":[],"mappings":";;;;;iBA+BsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0BAC1B"}
|
package/dist/speech.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"speech.d.ts","names":[],"sources":["../src/speech.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"speech.d.ts","names":[],"sources":["../src/speech.ts"],"sourcesContent":[],"mappings":";;;;;iBA+BsB,YAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uCAEA,oBACD,mBACC,uCACY,MAAA,CAAK,0BAC1B"}
|
package/dist/speech.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderError } from "./response/errors.js";
|
|
2
|
+
import { resolveRequestDecisions, strictOverrideField } from "./response/policy.js";
|
|
3
|
+
import { FORMAT_TO_CONTENT_TYPE, flattenHeaders, getContext, getTestId, isAudioResponse, isErrorResponse, isJsonObject, resolveResponse, strictNoMatchLogLine, strictNoMatchMessage } from "./helpers.js";
|
|
2
4
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
3
5
|
import { applyChaosAsync } from "./chaos.js";
|
|
4
6
|
import { matchFixtureDiagnostic } from "./router.js";
|
|
@@ -24,11 +26,11 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
24
26
|
fixture: null
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
|
-
writeErrorResponse(res, 400,
|
|
29
|
+
writeErrorResponse(res, 400, renderError("openai", "validation", {
|
|
28
30
|
message: `Malformed JSON: ${detail}`,
|
|
29
31
|
type: "invalid_request_error",
|
|
30
32
|
code: "invalid_json"
|
|
31
|
-
}
|
|
33
|
+
}).body);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
if (!isJsonObject(speechReq)) {
|
|
@@ -42,10 +44,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
42
44
|
fixture: null
|
|
43
45
|
}
|
|
44
46
|
});
|
|
45
|
-
writeErrorResponse(res, 400,
|
|
47
|
+
writeErrorResponse(res, 400, renderError("openai", "validation", {
|
|
46
48
|
message: "Request body must be a JSON object",
|
|
47
49
|
type: "invalid_request_error"
|
|
48
|
-
}
|
|
50
|
+
}).body);
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
const input = speechReq.input;
|
|
@@ -61,10 +63,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
61
63
|
fixture: null
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
|
-
writeErrorResponse(res, 400,
|
|
66
|
+
writeErrorResponse(res, 400, renderError("openai", "validation", {
|
|
65
67
|
message: !input ? "Missing required parameter: 'input'" : "Invalid parameter: 'input' must be a string or a nonempty array of content parts",
|
|
66
68
|
type: "invalid_request_error"
|
|
67
|
-
}
|
|
69
|
+
}).body);
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
const syntheticReq = {
|
|
@@ -89,7 +91,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
89
91
|
body: syntheticReq
|
|
90
92
|
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
91
93
|
if (!fixture) {
|
|
92
|
-
if (
|
|
94
|
+
if (resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
93
95
|
const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
94
96
|
defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));
|
|
95
97
|
journal.add({
|
|
@@ -103,11 +105,11 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
103
105
|
...strictOverrideField(defaults.strict, req.headers)
|
|
104
106
|
}
|
|
105
107
|
});
|
|
106
|
-
writeErrorResponse(res, 503,
|
|
108
|
+
writeErrorResponse(res, 503, renderError("openai", "strict-miss", {
|
|
107
109
|
message: strictMessage,
|
|
108
110
|
type: "invalid_request_error",
|
|
109
111
|
code: "no_fixture_match"
|
|
110
|
-
}
|
|
112
|
+
}).body);
|
|
111
113
|
return;
|
|
112
114
|
}
|
|
113
115
|
if (defaults.record) {
|
|
@@ -139,11 +141,11 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
139
141
|
...strictOverrideField(defaults.strict, req.headers)
|
|
140
142
|
}
|
|
141
143
|
});
|
|
142
|
-
writeErrorResponse(res, 404,
|
|
144
|
+
writeErrorResponse(res, 404, renderError("openai", "fixture-miss", {
|
|
143
145
|
message: "No fixture matched",
|
|
144
146
|
type: "invalid_request_error",
|
|
145
147
|
code: "no_fixture_match"
|
|
146
|
-
}
|
|
148
|
+
}).body);
|
|
147
149
|
return;
|
|
148
150
|
}
|
|
149
151
|
const response = await resolveResponse(fixture, syntheticReq);
|
|
@@ -159,7 +161,7 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
159
161
|
fixture
|
|
160
162
|
}
|
|
161
163
|
});
|
|
162
|
-
writeErrorResponse(res, status,
|
|
164
|
+
writeErrorResponse(res, status, renderError("openai", "fixture", response.error).body, { retryAfter: response.retryAfter });
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
167
|
if (!isAudioResponse(response)) {
|
|
@@ -173,10 +175,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
173
175
|
fixture
|
|
174
176
|
}
|
|
175
177
|
});
|
|
176
|
-
writeErrorResponse(res, 500,
|
|
178
|
+
writeErrorResponse(res, 500, renderError("openai", "unknown-response", {
|
|
177
179
|
message: "Fixture response is not an audio type",
|
|
178
180
|
type: "server_error"
|
|
179
|
-
}
|
|
181
|
+
}).body);
|
|
180
182
|
return;
|
|
181
183
|
}
|
|
182
184
|
if (typeof response.audio !== "string") {
|
|
@@ -190,10 +192,10 @@ async function handleSpeech(req, res, raw, fixtures, journal, defaults, setCorsH
|
|
|
190
192
|
fixture
|
|
191
193
|
}
|
|
192
194
|
});
|
|
193
|
-
writeErrorResponse(res, 500,
|
|
195
|
+
writeErrorResponse(res, 500, renderError("openai", "unknown-response", {
|
|
194
196
|
message: "Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }",
|
|
195
197
|
type: "server_error"
|
|
196
|
-
}
|
|
198
|
+
}).body);
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
201
|
journal.add({
|
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 isJsonObject,\n serializeErrorResponse,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const input: unknown = speechReq.input;\n const validInput =\n typeof input === \"string\" ||\n (Array.isArray(input) &&\n input.length > 0 &&\n input.every(\n (part: unknown) =>\n isJsonObject(part) &&\n typeof part.type === \"string\" &&\n (!(\"text\" in part) || typeof part.text === \"string\"),\n ));\n if (!input || !validInput) {\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: !input\n ? \"Missing required parameter: 'input'\"\n : \"Invalid parameter: 'input' must be a string or a nonempty array of content parts\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/audio/speech\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: { message: \"Fixture response is not an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Object-form audio is not supported for the speech endpoint — reject early\n if (typeof response.audio !== \"string\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const format = response.format ?? \"mp3\";\n const contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n const audioBytes = Buffer.from(response.audio, \"base64\");\n\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"speech.js","names":[],"sources":["../src/speech.ts"],"sourcesContent":["import { renderError } from \"./response/errors.js\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isErrorResponse,\n isJsonObject,\n flattenHeaders,\n getTestId,\n FORMAT_TO_CONTENT_TYPE,\n resolveResponse,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface SpeechRequest {\n model?: string;\n input: string;\n voice?: string;\n response_format?: string;\n speed?: number;\n [key: string]: unknown;\n}\n\nexport async function handleSpeech(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/audio/speech\";\n const method = req.method ?? \"POST\";\n\n let speechReq: SpeechRequest;\n try {\n speechReq = JSON.parse(raw) as SpeechRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"openai\", \"validation\", {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `speechReq.input` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(speechReq)) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"openai\", \"validation\", {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n const input: unknown = speechReq.input;\n const validInput =\n typeof input === \"string\" ||\n (Array.isArray(input) &&\n input.length > 0 &&\n input.every(\n (part: unknown) =>\n isJsonObject(part) &&\n typeof part.type === \"string\" &&\n (!(\"text\" in part) || typeof part.text === \"string\"),\n ));\n if (!input || !validInput) {\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 renderError(\"openai\", \"validation\", {\n message: !input\n ? \"Missing required parameter: 'input'\"\n : \"Invalid parameter: 'input' must be a string or a nonempty array of content parts\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: speechReq.model ?? \"tts-1\",\n messages: [{ role: \"user\", content: speechReq.input }],\n _endpointType: \"speech\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss;\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(method, path, skippedBySequenceOrTurn));\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n renderError(\"openai\", \"strict-miss\", {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\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 renderError(\"openai\", \"fixture-miss\", {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\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, renderError(\"openai\", \"fixture\", response.error).body, {\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 renderError(\"openai\", \"unknown-response\", {\n message: \"Fixture response is not an audio type\",\n type: \"server_error\",\n }).body,\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 renderError(\"openai\", \"unknown-response\", {\n message:\n \"Object-form audio not supported for speech endpoint. Use string-form: { audio: '<base64>' }\",\n type: \"server_error\",\n }).body,\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
|
@@ -40,6 +40,12 @@ function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, r
|
|
|
40
40
|
delayMs = Math.max(0, delayMs);
|
|
41
41
|
return speed > 0 ? delayMs / speed : delayMs;
|
|
42
42
|
}
|
|
43
|
+
function calculateTotalDelay(contentLength, chunkSize, profile, recordedTimings, replaySpeed) {
|
|
44
|
+
const chunkCount = Math.max(1, Math.ceil(contentLength / chunkSize));
|
|
45
|
+
let total = 0;
|
|
46
|
+
for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) total += calculateDelay(chunkIndex, profile, void 0, recordedTimings, replaySpeed);
|
|
47
|
+
return total;
|
|
48
|
+
}
|
|
43
49
|
async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
44
50
|
const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
|
|
45
51
|
const latency = opts.latency ?? 0;
|
|
@@ -94,6 +100,7 @@ function writeErrorResponse(res, status, body, options) {
|
|
|
94
100
|
|
|
95
101
|
//#endregion
|
|
96
102
|
exports.calculateDelay = calculateDelay;
|
|
103
|
+
exports.calculateTotalDelay = calculateTotalDelay;
|
|
97
104
|
exports.delay = delay;
|
|
98
105
|
exports.writeErrorResponse = writeErrorResponse;
|
|
99
106
|
exports.writeSSEStream = writeSSEStream;
|
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, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";;AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;;AAuBJ,MAAM,gCAAgC;AAEtC,SAAgB,eACd,YACA,SACA,iBACA,iBACA,aACQ;CACR,MAAM,QAAQ,eAAe;CAC7B,IAAI;AAEJ,KAAI,SAAS;EAEX,IAAI,cAAc;AAClB,MAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;WACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;OACpB;AACL,aAAU,mBAAmB;AAC7B,iBAAc;;AAGhB,MAAI,eAAe,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACvD,cAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AACjD,OAAI,UAAU,EAAG,WAAU;;YAEpB,gBAET,KAAI,eAAe,EACjB,WAAU,gBAAgB;MACrB;EACL,MAAM,MAAM,aAAa;AACzB,MAAI,MAAM,gBAAgB,mBAAmB,OAC3C,WAAU,gBAAgB,mBAAmB;OACxC;GAEL,MAAM,kBAAkB,gBAAgB,mBAAmB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;AACrF,aACE,gBAAgB,mBAAmB,SAAS,IACxC,kBAAkB,gBAAgB,mBAAmB,SACrD;;;KAIV,WAAU,mBAAmB;AAG/B,WAAU,KAAK,IAAI,GAAG,QAAQ;AAC9B,QAAO,QAAQ,IAAI,UAAU,QAAQ;;
|
|
1
|
+
{"version":3,"file":"sse-writer.cjs","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\n// Internal (not part of the public API): the total wait before a non-streaming\n// (`stream: false`) JSON response is written. Sums calculateDelay over the\n// content chunk count that `contentLength` at `chunkSize` would produce, so a\n// fixture with an explicit `streamingProfile` or `recordedTimings` pays roughly\n// the same ttft/tps cost as its streamed content chunks. It is an\n// approximation, not an exact match: a streamed reply also pays delays for its\n// role and finish chunks. There is deliberately no fallback-latency parameter:\n// bare `latency` / `--latency` is an inter-SSE-chunk gap and never delays a\n// non-streaming response. With neither a profile nor recorded timings, this\n// returns 0.\n// Kept as a line comment, not JSDoc: the bundled .d.ts drops this non-public\n// declaration but keeps a JSDoc block, which would then attach to writeSSEStream.\nexport function calculateTotalDelay(\n contentLength: number,\n chunkSize: number,\n profile?: StreamingProfile,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const chunkCount = Math.max(1, Math.ceil(contentLength / chunkSize));\n let total = 0;\n for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {\n total += calculateDelay(chunkIndex, profile, undefined, recordedTimings, replaySpeed);\n }\n return total;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":"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"}
|
|
@@ -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;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;
|
|
1
|
+
{"version":3,"file":"sse-writer.d.cts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;AA0EE,iBA9EN,cAAA,CA8EoB,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA5ExB,gBA4EwB,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EA1EhB,eA0EgB,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAC7B,iBADe,cAAA,CACV,GAAA,EAAL,MAAA,CAAK,cAAA,EAAA,MAAA,EACF,QADE,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAEkB,aAFlB,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;AACF,UA4DO,oBAAA,CA5DP;;YAEP,CAAA,EAAA,MAAA;;AA0Dc,iBAKD,kBAAA,CALqB,GAAA,EAM9B,MAAA,CAAK,cANyB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EASzB,oBATyB,CAAA,EAAA,IAAA;AAKrC"}
|
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;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;
|
|
1
|
+
{"version":3,"file":"sse-writer.d.ts","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":[],"mappings":";;;;iBAGgB,KAAA,sBAA2B,cAAc;UAexC,aAAA;EAfD,OAAA,CAAK,EAAA,MAAA;EAAA,gBAAA,CAAA,EAiBA,gBAjBA;iBAAsB,CAAA,EAkBvB,eAlBuB;aAAc,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAoBrD,WApBqD;EAe/C,WAAA,CAAA,EAAA,GAAa,GAAA,IAAA;EAAA;YAET,CAAA,EAMN,QANM;;;;;AAoBrB;;;sBAIoB,CAAA,EAAA,OAAA;;AA0EE,iBA9EN,cAAA,CA8EoB,UAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EA5ExB,gBA4EwB,EAAA,eAAA,CAAA,EAAA,MAAA,EAAA,eAAA,CAAA,EA1EhB,eA0EgB,EAAA,WAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;AAC7B,iBADe,cAAA,CACV,GAAA,EAAL,MAAA,CAAK,cAAA,EAAA,MAAA,EACF,QADE,EAAA,EAAA,gBAAA,CAAA,EAAA,MAAA,GAEkB,aAFlB,CAAA,EAGT,OAHS,CAAA,OAAA,CAAA;AACF,UA4DO,oBAAA,CA5DP;;YAEP,CAAA,EAAA,MAAA;;AA0Dc,iBAKD,kBAAA,CALqB,GAAA,EAM9B,MAAA,CAAK,cANyB,EAAA,MAAA,EAAA,MAAA,EAAA,IAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EASzB,oBATyB,CAAA,EAAA,IAAA;AAKrC"}
|
package/dist/sse-writer.js
CHANGED
|
@@ -39,6 +39,12 @@ function calculateDelay(chunkIndex, profile, fallbackLatency, recordedTimings, r
|
|
|
39
39
|
delayMs = Math.max(0, delayMs);
|
|
40
40
|
return speed > 0 ? delayMs / speed : delayMs;
|
|
41
41
|
}
|
|
42
|
+
function calculateTotalDelay(contentLength, chunkSize, profile, recordedTimings, replaySpeed) {
|
|
43
|
+
const chunkCount = Math.max(1, Math.ceil(contentLength / chunkSize));
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) total += calculateDelay(chunkIndex, profile, void 0, recordedTimings, replaySpeed);
|
|
46
|
+
return total;
|
|
47
|
+
}
|
|
42
48
|
async function writeSSEStream(res, chunks, optionsOrLatency) {
|
|
43
49
|
const opts = typeof optionsOrLatency === "number" ? { latency: optionsOrLatency } : optionsOrLatency ?? {};
|
|
44
50
|
const latency = opts.latency ?? 0;
|
|
@@ -92,5 +98,5 @@ function writeErrorResponse(res, status, body, options) {
|
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
//#endregion
|
|
95
|
-
export { calculateDelay, delay, writeErrorResponse, writeSSEStream };
|
|
101
|
+
export { calculateDelay, calculateTotalDelay, delay, writeErrorResponse, writeSSEStream };
|
|
96
102
|
//# sourceMappingURL=sse-writer.js.map
|
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, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":";AAGA,SAAgB,MAAM,IAAY,QAAqC;AACrE,KAAI,MAAM,KAAK,QAAQ,QAAS,QAAO,QAAQ,SAAS;AACxD,QAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,QAAQ,WAAW,SAAS,GAAG;AACrC,UAAQ,iBACN,eACM;AACJ,gBAAa,MAAM;AACnB,YAAS;KAEX,EAAE,MAAM,MAAM,CACf;GACD;;;AAuBJ,MAAM,gCAAgC;AAEtC,SAAgB,eACd,YACA,SACA,iBACA,iBACA,aACQ;CACR,MAAM,QAAQ,eAAe;CAC7B,IAAI;AAEJ,KAAI,SAAS;EAEX,IAAI,cAAc;AAClB,MAAI,eAAe,KAAK,QAAQ,SAAS,OACvC,WAAU,QAAQ;WACT,QAAQ,QAAQ,UAAa,QAAQ,MAAM,EACpD,WAAU,MAAO,QAAQ;OACpB;AACL,aAAU,mBAAmB;AAC7B,iBAAc;;AAGhB,MAAI,eAAe,QAAQ,UAAU,QAAQ,SAAS,GAAG;AACvD,cAAW,KAAK,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ;AACjD,OAAI,UAAU,EAAG,WAAU;;YAEpB,gBAET,KAAI,eAAe,EACjB,WAAU,gBAAgB;MACrB;EACL,MAAM,MAAM,aAAa;AACzB,MAAI,MAAM,gBAAgB,mBAAmB,OAC3C,WAAU,gBAAgB,mBAAmB;OACxC;GAEL,MAAM,kBAAkB,gBAAgB,mBAAmB,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE;AACrF,aACE,gBAAgB,mBAAmB,SAAS,IACxC,kBAAkB,gBAAgB,mBAAmB,SACrD;;;KAIV,WAAU,mBAAmB;AAG/B,WAAU,KAAK,IAAI,GAAG,QAAQ;AAC9B,QAAO,QAAQ,IAAI,UAAU,QAAQ;;
|
|
1
|
+
{"version":3,"file":"sse-writer.js","names":[],"sources":["../src/sse-writer.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { SSEChunk, StreamingProfile, RecordedTimings } from \"./types.js\";\n\nexport function delay(ms: number, signal?: AbortSignal): Promise<void> {\n if (ms <= 0 || signal?.aborted) return Promise.resolve();\n return new Promise((resolve) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nexport interface StreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n /** When set, emitted as the final chunk before [DONE] (OpenAI stream_options.include_usage). */\n usageChunk?: SSEChunk;\n /**\n * When true, a single OpenRouter `: OPENROUTER PROCESSING` SSE comment\n * (keepalive) line is emitted FIRST — before the first data chunk — matching\n * real OpenRouter bytes (or-capture/chat-stream.sse). A faithful client skips\n * `:`-prefixed lines; opt-in, default off (so strict OpenAI parsers aren't\n * surprised). Used only by the OpenRouter chat surface.\n */\n openRouterProcessing?: boolean;\n}\n\n/** OpenRouter's SSE keepalive comment line (colon-prefixed, per SSE syntax). */\nconst OPENROUTER_PROCESSING_COMMENT = \": OPENROUTER PROCESSING\\n\\n\";\n\nexport function calculateDelay(\n chunkIndex: number,\n profile?: StreamingProfile,\n fallbackLatency?: number,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const speed = replaySpeed ?? 1.0;\n let delayMs: number;\n\n if (profile) {\n // StreamingProfile has highest precedence\n let fromProfile = true;\n if (chunkIndex === 0 && profile.ttft !== undefined) {\n delayMs = profile.ttft;\n } else if (profile.tps !== undefined && profile.tps > 0) {\n delayMs = 1000 / profile.tps;\n } else {\n delayMs = fallbackLatency ?? 0;\n fromProfile = false;\n }\n // Jitter only applies when the delay came from ttft/tps, not fallback\n if (fromProfile && profile.jitter && profile.jitter > 0) {\n delayMs *= 1 + (Math.random() * 2 - 1) * profile.jitter;\n if (delayMs < 0) delayMs = 0;\n }\n } else if (recordedTimings) {\n // Recorded timings (second precedence)\n if (chunkIndex === 0) {\n delayMs = recordedTimings.ttftMs;\n } else {\n const idx = chunkIndex - 1;\n if (idx < recordedTimings.interChunkDelaysMs.length) {\n delayMs = recordedTimings.interChunkDelaysMs[idx];\n } else {\n // Excess chunks: derive average from recorded inter-chunk delays\n const totalInterChunk = recordedTimings.interChunkDelaysMs.reduce((a, b) => a + b, 0);\n delayMs =\n recordedTimings.interChunkDelaysMs.length > 0\n ? totalInterChunk / recordedTimings.interChunkDelaysMs.length\n : 0;\n }\n }\n } else {\n delayMs = fallbackLatency ?? 0;\n }\n\n delayMs = Math.max(0, delayMs);\n return speed > 0 ? delayMs / speed : delayMs;\n}\n\n// Internal (not part of the public API): the total wait before a non-streaming\n// (`stream: false`) JSON response is written. Sums calculateDelay over the\n// content chunk count that `contentLength` at `chunkSize` would produce, so a\n// fixture with an explicit `streamingProfile` or `recordedTimings` pays roughly\n// the same ttft/tps cost as its streamed content chunks. It is an\n// approximation, not an exact match: a streamed reply also pays delays for its\n// role and finish chunks. There is deliberately no fallback-latency parameter:\n// bare `latency` / `--latency` is an inter-SSE-chunk gap and never delays a\n// non-streaming response. With neither a profile nor recorded timings, this\n// returns 0.\n// Kept as a line comment, not JSDoc: the bundled .d.ts drops this non-public\n// declaration but keeps a JSDoc block, which would then attach to writeSSEStream.\nexport function calculateTotalDelay(\n contentLength: number,\n chunkSize: number,\n profile?: StreamingProfile,\n recordedTimings?: RecordedTimings,\n replaySpeed?: number,\n): number {\n const chunkCount = Math.max(1, Math.ceil(contentLength / chunkSize));\n let total = 0;\n for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {\n total += calculateDelay(chunkIndex, profile, undefined, recordedTimings, replaySpeed);\n }\n return total;\n}\n\nexport async function writeSSEStream(\n res: http.ServerResponse,\n chunks: SSEChunk[],\n optionsOrLatency?: number | StreamOptions,\n): Promise<boolean> {\n const opts: StreamOptions =\n typeof optionsOrLatency === \"number\" ? { latency: optionsOrLatency } : (optionsOrLatency ?? {});\n const latency = opts.latency ?? 0;\n const profile = opts.streamingProfile;\n const { recordedTimings, replaySpeed } = opts;\n const signal = opts.signal;\n const onChunkSent = opts.onChunkSent;\n\n if (res.writableEnded) return true;\n res.setHeader(\"Content-Type\", \"text/event-stream\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.setHeader(\"Connection\", \"keep-alive\");\n\n // OpenRouter emits exactly ONE `: OPENROUTER PROCESSING` keepalive comment\n // first — before any data frame (opt-in; see or-capture/chat-stream.sse). A\n // faithful client discards `:`-prefixed lines before JSON-parsing a `data:`.\n if (opts.openRouterProcessing && !res.writableEnded) {\n res.write(OPENROUTER_PROCESSING_COMMENT);\n }\n\n let chunkIndex = 0;\n for (const chunk of chunks) {\n const chunkDelay = calculateDelay(chunkIndex, profile, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) {\n await delay(chunkDelay, signal);\n }\n if (signal?.aborted) return false;\n if (res.writableEnded) return true;\n res.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n onChunkSent?.();\n if (signal?.aborted) return false;\n chunkIndex++;\n }\n\n if (!res.writableEnded) {\n if (opts.usageChunk) {\n res.write(`data: ${JSON.stringify(opts.usageChunk)}\\n\\n`);\n }\n res.write(\"data: [DONE]\\n\\n\");\n res.end();\n }\n return true;\n}\n\n/**\n * Default rate-limit response headers matching OpenAI's format.\n * Values are static — aimock doesn't track actual request counts.\n */\nconst RATE_LIMIT_HEADERS: Record<string, string> = {\n \"x-ratelimit-limit-requests\": \"60\",\n \"x-ratelimit-limit-tokens\": \"150000\",\n \"x-ratelimit-remaining-requests\": \"0\",\n \"x-ratelimit-remaining-tokens\": \"0\",\n \"x-ratelimit-reset-requests\": \"1s\",\n \"x-ratelimit-reset-tokens\": \"6m0s\",\n};\n\nexport interface ErrorResponseOptions {\n /** Override the Retry-After header value (seconds). Default: 1. Only applied on 429. */\n retryAfter?: number;\n}\n\nexport function writeErrorResponse(\n res: http.ServerResponse,\n status: number,\n body: string,\n options?: ErrorResponseOptions,\n): void {\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n if (status === 429) {\n headers[\"Retry-After\"] = String(options?.retryAfter ?? 1);\n Object.assign(headers, RATE_LIMIT_HEADERS);\n }\n res.writeHead(status, headers);\n res.end(body);\n}\n"],"mappings":"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"}
|