@copilotkit/aimock 1.42.1 → 1.44.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 +65 -0
- package/README.md +108 -16
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +1 -1
- 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 +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +44 -12
- 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 +46 -14
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- 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 +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +625 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +616 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +131 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +132 -31
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +37 -9
- 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 +39 -11
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +1 -1
- package/dist/config-loader.js +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +26 -5
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +27 -6
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1069 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1056 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1033 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1026 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +131 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +131 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +44 -4
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +46 -6
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +51 -7
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +53 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +19 -8
- 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 +21 -10
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +497 -10
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +487 -12
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +57 -125
- 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 +58 -126
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +71 -9
- package/dist/index.d.cts +17 -7
- package/dist/index.d.ts +17 -7
- package/dist/index.js +21 -11
- package/dist/jest.cjs +24 -12
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +6 -3
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +6 -3
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +25 -13
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +61 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +60 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +60 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +61 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +132 -49
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +133 -50
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +216 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +210 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes.cjs +1619 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +101 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +101 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1614 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +331 -17
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +330 -18
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +217 -49
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +66 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +66 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +218 -50
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +49 -7
- 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 +51 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +278 -45
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +17 -7
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +17 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +270 -29
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +10 -6
- 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 +11 -7
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +9 -9
- 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 +10 -10
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +54 -10
- 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 +52 -12
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +47 -7
- 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 +48 -9
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +21 -34
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -35
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1252 -777
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1185 -710
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +6 -4
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +7 -5
- package/dist/speech.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +6 -6
- 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 +7 -7
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +320 -17
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +320 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +983 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +982 -0
- package/dist/validate-cli.js.map +1 -0
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +23 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +24 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +23 -11
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +5 -2
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +5 -2
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +25 -13
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- 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 +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +16 -4
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +16 -4
- package/dist/ws-responses.js.map +1 -1
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-responses.cjs","names":["requestWantsEncryptedReasoning","requestIncludesEncryptedReasoning","responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","resolveReasoningForModel","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","calculateDelay","delay"],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws-responses.cjs","names":["requestWantsEncryptedReasoning","requestIncludesEncryptedReasoning","responsesToCompletionRequest","DEFAULT_TEST_ID","matchFixtureDiagnostic","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","flattenHeaders","strictOverrideField","resolveResponse","isErrorResponse","isContentWithToolCallsResponse","buildContentWithToolCallsStreamEvents","resolveReasoningForModel","extractOverrides","createInterruptionSignal","isTextResponse","buildTextStreamEvents","isToolCallResponse","buildToolCallStreamEvents","calculateDelay","delay"],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n let events: ResponsesSSEEvent[];\n try {\n events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n } catch (error) {\n journalEntry.response.status = 500;\n throw error;\n }\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n let events: ResponsesSSEEvent[];\n try {\n events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n } catch (error) {\n journalEntry.response.status = 500;\n throw error;\n }\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|
package/dist/ws-responses.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_TEST_ID } from "./constants.js";
|
|
2
2
|
import { extractOverrides, flattenHeaders, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse, resolveReasoningForModel, resolveResponse, resolveStrictMode, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField } from "./helpers.js";
|
|
3
|
-
import "./journal.js";
|
|
4
|
-
import { matchFixtureDiagnostic } from "./router.js";
|
|
5
3
|
import { calculateDelay, delay } from "./sse-writer.js";
|
|
4
|
+
import { matchFixtureDiagnostic } from "./router.js";
|
|
5
|
+
import "./journal.js";
|
|
6
6
|
import { createInterruptionSignal } from "./interruption.js";
|
|
7
7
|
import { buildContentWithToolCallsStreamEvents, buildTextStreamEvents, buildToolCallStreamEvents, requestIncludesEncryptedReasoning, requestWantsEncryptedReasoning, responsesToCompletionRequest } from "./responses.js";
|
|
8
8
|
|
|
@@ -131,7 +131,13 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
|
|
|
131
131
|
fixture
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
|
-
|
|
134
|
+
let events;
|
|
135
|
+
try {
|
|
136
|
+
events = buildContentWithToolCallsStreamEvents(response.content ?? "", response.toolCalls ?? [], completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), response.blocks, emitEncryptedReasoning, synthesizeSummarylessReasoning);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
journalEntry.response.status = 500;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
135
141
|
const interruption = createInterruptionSignal(fixture);
|
|
136
142
|
if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed)) {
|
|
137
143
|
ws.destroy();
|
|
@@ -173,7 +179,13 @@ async function processMessage(raw, ws, fixtures, journal, defaults) {
|
|
|
173
179
|
fixture
|
|
174
180
|
}
|
|
175
181
|
});
|
|
176
|
-
|
|
182
|
+
let events;
|
|
183
|
+
try {
|
|
184
|
+
events = buildToolCallStreamEvents(response.toolCalls, completionReq.model, chunkSize, resolveReasoningForModel(response.reasoning, completionReq.model, effectiveStrict, defaults.logger), response.webSearches, extractOverrides(response), emitEncryptedReasoning, synthesizeSummarylessReasoning);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
journalEntry.response.status = 500;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
177
189
|
const interruption = createInterruptionSignal(fixture);
|
|
178
190
|
if (!await sendEvents(ws, events, latency, interruption?.signal, interruption?.tick, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed)) {
|
|
179
191
|
ws.destroy();
|
package/dist/ws-responses.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-responses.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n const events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws-responses.js","names":[],"sources":["../src/ws-responses.ts"],"sourcesContent":["/**\n * WebSocket handler for OpenAI Responses API.\n *\n * Accepts `{ type: \"response.create\", model: \"...\", input: [...] }` messages over\n * WebSocket and sends back the same Responses API SSE events as the HTTP\n * handler, but as individual WebSocket text frames.\n */\n\nimport type { ChatCompletionRequest, Fixture } from \"./types.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport {\n responsesToCompletionRequest,\n requestIncludesEncryptedReasoning,\n requestWantsEncryptedReasoning,\n buildTextStreamEvents,\n buildToolCallStreamEvents,\n buildContentWithToolCallsStreamEvents,\n type ResponsesSSEEvent,\n} from \"./responses.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n extractOverrides,\n resolveResponse,\n resolveStrictMode,\n resolveReasoningForModel,\n strictOverrideField,\n flattenHeaders,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay, calculateDelay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\ninterface ResponseCreateMessage {\n type: \"response.create\";\n model?: string;\n input?: unknown[];\n instructions?: string;\n tools?: unknown[];\n tool_choice?: string | object;\n stream?: boolean;\n temperature?: number;\n max_output_tokens?: number;\n [key: string]: unknown;\n}\n\nfunction isResponseCreateMessage(msg: unknown): msg is ResponseCreateMessage {\n return (\n typeof msg === \"object\" &&\n msg !== null &&\n (msg as ResponseCreateMessage).type === \"response.create\"\n );\n}\n\nfunction buildErrorEvent(\n message: string,\n type = \"invalid_request_error\",\n code?: string,\n): ResponsesSSEEvent {\n return {\n type: \"error\",\n error: { message, type, code },\n };\n}\n\nexport function handleWebSocketResponses(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): void {\n const { logger } = defaults;\n // Serialize message processing to prevent event interleaving\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket responses error: ${msg}`);\n try {\n ws.send(JSON.stringify(buildErrorEvent(msg, \"server_error\")));\n } catch (sendErr) {\n defaults.logger.debug(\n `Failed to send error to client: ${sendErr instanceof Error ? sendErr.message : \"unknown\"}`,\n );\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n replaySpeed?: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n upgradeHeaders?: import(\"node:http\").IncomingHttpHeaders;\n },\n): Promise<void> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n ws.send(\n JSON.stringify(\n buildErrorEvent(`Malformed JSON: ${detail}`, \"invalid_request_error\", \"invalid_json\"),\n ),\n );\n return;\n }\n\n if (!isResponseCreateMessage(parsed)) {\n ws.send(\n JSON.stringify(\n buildErrorEvent(\n 'Expected message type \"response.create\"',\n \"invalid_request_error\",\n \"invalid_message_type\",\n ),\n ),\n );\n return;\n }\n\n const responsesReq = {\n model: parsed.model ?? defaults.model,\n input: (parsed.input ?? []) as {\n role?: string;\n type?: string;\n content?: string | { type: string; text?: string }[];\n call_id?: string;\n name?: string;\n arguments?: string;\n output?: string;\n id?: string;\n }[],\n instructions: parsed.instructions,\n tools: parsed.tools as\n | {\n type: \"function\";\n name: string;\n description?: string;\n parameters?: object;\n strict?: boolean;\n }[]\n | undefined,\n tool_choice: parsed.tool_choice,\n stream: parsed.stream,\n temperature: parsed.temperature,\n max_output_tokens: parsed.max_output_tokens,\n include: (parsed as { include?: string[] }).include,\n store: (parsed as { store?: boolean }).store,\n };\n\n // Gate encrypted-reasoning emission identically to the HTTP transport so the\n // agent-framework#7233 stateless-replay path works over WebSocket too.\n const emitEncryptedReasoning = requestWantsEncryptedReasoning(responsesReq);\n // Synthesizing a reasoning item the fixture never declared takes the NARROWER\n // explicit-`include` gate, transport-identically to HTTP — a `store: false`\n // request with a summary-less fixture still gets no reasoning item.\n const synthesizeSummarylessReasoning = requestIncludesEncryptedReasoning(responsesReq);\n\n const completionReq = responsesToCompletionRequest(responsesReq);\n completionReq._endpointType = \"chat\";\n const contextHeader = defaults.upgradeHeaders?.[\"x-aimock-context\"];\n completionReq._context =\n typeof contextHeader === \"string\"\n ? contextHeader\n : Array.isArray(contextHeader) && contextHeader.length > 0\n ? contextHeader[0]\n : undefined;\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n defaults.logger.error(strictNoMatchLogLine(\"WS\", \"/v1/responses\", skippedBySequenceOrTurn));\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.close(1008, strictMessage);\n return;\n }\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, defaults.upgradeHeaders),\n },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"No fixture matched\", \"invalid_request_error\", \"no_fixture_match\"),\n ),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, completionReq);\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // The WS path has no per-request `req.headers`; strict is resolved from the\n // connection's upgrade headers (see the `!fixture` branch above). Used below\n // to gate the synthesized reasoning channel on the requested model's capability.\n const effectiveStrict = resolveStrictMode(defaults.strict, defaults.upgradeHeaders);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(response.error.message, response.error.type, response.error.code),\n ),\n );\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n let events: ResponsesSSEEvent[];\n try {\n events = buildContentWithToolCallsStreamEvents(\n response.content ?? \"\",\n response.toolCalls ?? [],\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n response.blocks,\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n } catch (error) {\n journalEntry.response.status = 500;\n throw error;\n }\n\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Text response\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const events = buildTextStreamEvents(\n response.content,\n completionReq.model,\n chunkSize,\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 200, fixture },\n });\n let events: ResponsesSSEEvent[];\n try {\n events = buildToolCallStreamEvents(\n response.toolCalls,\n completionReq.model,\n chunkSize,\n // Gate the synthesized reasoning channel on the requested model's\n // capability, matching the WS text / content+tool branches and the HTTP\n // tool-only path so reasoning emission is transport-independent.\n resolveReasoningForModel(\n response.reasoning,\n completionReq.model,\n effectiveStrict,\n defaults.logger,\n ),\n response.webSearches,\n extractOverrides(response),\n emitEncryptedReasoning,\n synthesizeSummarylessReasoning,\n );\n } catch (error) {\n journalEntry.response.status = 500;\n throw error;\n }\n const interruption = createInterruptionSignal(fixture);\n const completed = await sendEvents(\n ws,\n events,\n latency,\n interruption?.signal,\n interruption?.tick,\n fixture.recordedTimings,\n fixture.replaySpeed ?? defaults.replaySpeed,\n );\n if (!completed) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n }\n interruption?.cleanup();\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path: \"/v1/responses\",\n headers: flattenHeaders(defaults.upgradeHeaders ?? {}),\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify(\n buildErrorEvent(\"Fixture response did not match any known type\", \"server_error\"),\n ),\n );\n}\n\nasync function sendEvents(\n ws: WebSocketConnection,\n events: ResponsesSSEEvent[],\n latency: number,\n signal?: AbortSignal,\n onChunkSent?: () => void,\n recordedTimings?: import(\"./types.js\").RecordedTimings,\n replaySpeed?: number,\n): Promise<boolean> {\n let eventIndex = 0;\n for (const event of events) {\n if (ws.isClosed) return false;\n const chunkDelay = calculateDelay(eventIndex, undefined, latency, recordedTimings, replaySpeed);\n if (chunkDelay > 0) await delay(chunkDelay, signal);\n if (signal?.aborted) return false;\n if (ws.isClosed) return false;\n ws.send(JSON.stringify(event));\n eventIndex++;\n onChunkSent?.();\n if (signal?.aborted) return false;\n }\n return true;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
const require_ws_framing = require('./ws-framing.cjs');
|
|
3
|
+
let node_http = require("node:http");
|
|
4
|
+
node_http = require_runtime.__toESM(node_http);
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
6
|
+
let node_https = require("node:https");
|
|
7
|
+
node_https = require_runtime.__toESM(node_https);
|
|
8
|
+
|
|
9
|
+
//#region src/ws-upstream.ts
|
|
10
|
+
const REFUSAL_BODY_LIMIT = 64 * 1024;
|
|
11
|
+
const UPGRADE_TIMEOUT_MS = 3e4;
|
|
12
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
13
|
+
const PROVIDER_HEADERS = new Set(["authorization"]);
|
|
14
|
+
/** Connect to a provider using verified TLS, or an explicitly configured loopback test peer.
|
|
15
|
+
* The caller's signal owns the session lifetime; the upgrade budget defaults to thirty seconds.
|
|
16
|
+
*/
|
|
17
|
+
async function connectUpstreamWebSocket(url, headers, limits, signal, upstreamTimeoutMs = UPGRADE_TIMEOUT_MS) {
|
|
18
|
+
if (!Number.isSafeInteger(upstreamTimeoutMs) || upstreamTimeoutMs <= 0) throw new RangeError("Upstream WebSocket timeout must be a positive safe integer");
|
|
19
|
+
const target = new URL(url.href);
|
|
20
|
+
const secure = target.protocol === "https:" || target.protocol === "wss:";
|
|
21
|
+
const loopback = target.hostname === "localhost" || target.hostname === "[::1]" || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(target.hostname);
|
|
22
|
+
if (target.username || target.password || target.hash || !secure && (!(target.protocol === "http:" || target.protocol === "ws:") || !loopback)) throw new Error("Invalid upstream WebSocket URL");
|
|
23
|
+
for (const value of [
|
|
24
|
+
limits.maxMessageBytes,
|
|
25
|
+
limits.maxBufferedBytes,
|
|
26
|
+
limits.maxWriteBytes
|
|
27
|
+
]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new RangeError("WebSocket limits must be positive safe integers");
|
|
28
|
+
if (signal.aborted) throw new Error("Upstream WebSocket aborted");
|
|
29
|
+
target.protocol = secure ? "https:" : "http:";
|
|
30
|
+
const key = (0, node_crypto.randomBytes)(16).toString("base64");
|
|
31
|
+
const outgoing = {};
|
|
32
|
+
for (const [name, value] of Object.entries(headers)) if (PROVIDER_HEADERS.has(name.toLowerCase())) outgoing[name.toLowerCase()] = value;
|
|
33
|
+
Object.assign(outgoing, {
|
|
34
|
+
Upgrade: "websocket",
|
|
35
|
+
Connection: "Upgrade",
|
|
36
|
+
"Sec-WebSocket-Key": key,
|
|
37
|
+
"Sec-WebSocket-Version": "13"
|
|
38
|
+
});
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let settled = false;
|
|
41
|
+
let connection;
|
|
42
|
+
const request = (secure ? node_https : node_http).request(target, {
|
|
43
|
+
method: "GET",
|
|
44
|
+
headers: outgoing,
|
|
45
|
+
agent: false,
|
|
46
|
+
...secure ? { rejectUnauthorized: true } : {}
|
|
47
|
+
});
|
|
48
|
+
const started = performance.now();
|
|
49
|
+
const onTimeout = () => {
|
|
50
|
+
const remaining = upstreamTimeoutMs - (performance.now() - started);
|
|
51
|
+
if (remaining > 0) timer = setTimeout(onTimeout, Math.min(remaining, MAX_TIMER_DELAY_MS));
|
|
52
|
+
else fail("Upstream WebSocket upgrade timed out");
|
|
53
|
+
};
|
|
54
|
+
let timer = setTimeout(onTimeout, Math.min(upstreamTimeoutMs, MAX_TIMER_DELAY_MS));
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
signal.removeEventListener("abort", abort);
|
|
58
|
+
};
|
|
59
|
+
const fail = (message) => {
|
|
60
|
+
if (settled) return;
|
|
61
|
+
settled = true;
|
|
62
|
+
cleanup();
|
|
63
|
+
request.destroy();
|
|
64
|
+
reject(new Error(message));
|
|
65
|
+
};
|
|
66
|
+
const abort = () => {
|
|
67
|
+
if (connection) connection.destroy();
|
|
68
|
+
else fail("Upstream WebSocket aborted");
|
|
69
|
+
};
|
|
70
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
71
|
+
request.on("error", () => fail("Upstream WebSocket connection failed"));
|
|
72
|
+
request.once("response", (response) => {
|
|
73
|
+
let bytes = 0;
|
|
74
|
+
response.on("data", (chunk) => {
|
|
75
|
+
bytes += chunk.length;
|
|
76
|
+
if (bytes > REFUSAL_BODY_LIMIT) {
|
|
77
|
+
fail("Upstream WebSocket refusal body limit exceeded");
|
|
78
|
+
response.destroy();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
response.once("end", () => fail(`Upstream WebSocket refused (HTTP ${response.statusCode ?? 0})`));
|
|
82
|
+
response.once("error", () => fail("Upstream WebSocket response failed"));
|
|
83
|
+
response.once("aborted", () => fail("Upstream WebSocket response failed"));
|
|
84
|
+
});
|
|
85
|
+
request.once("upgrade", (response, socket, head) => {
|
|
86
|
+
if (settled) {
|
|
87
|
+
socket.destroy();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const tokens = (value) => value?.split(",").map((token) => token.trim().toLowerCase()) ?? [];
|
|
91
|
+
if (response.statusCode !== 101 || !tokens(response.headers.upgrade).includes("websocket") || !tokens(response.headers.connection).includes("upgrade") || response.headers["sec-websocket-accept"] !== require_ws_framing.computeAcceptKey(key) || response.headers["sec-websocket-extensions"] !== void 0 || response.headers["sec-websocket-protocol"] !== void 0) {
|
|
92
|
+
socket.destroy();
|
|
93
|
+
fail("Invalid upstream WebSocket upgrade");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
socket.pause();
|
|
97
|
+
connection = new require_ws_framing.WebSocketConnection(socket, {
|
|
98
|
+
maxMessageBytes: limits.maxMessageBytes ?? 1024 * 1024,
|
|
99
|
+
maxBufferedBytes: limits.maxBufferedBytes ?? 2 * 1024 * 1024,
|
|
100
|
+
maxWriteBytes: limits.maxWriteBytes ?? 1024 * 1024,
|
|
101
|
+
maskOutgoing: true
|
|
102
|
+
});
|
|
103
|
+
connection.once("close", cleanup);
|
|
104
|
+
settled = true;
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
if (head.length) socket.unshift(head);
|
|
107
|
+
resolve(connection);
|
|
108
|
+
setImmediate(() => {
|
|
109
|
+
if (!socket.destroyed) socket.resume();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
request.end();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
exports.connectUpstreamWebSocket = connectUpstreamWebSocket;
|
|
118
|
+
//# sourceMappingURL=ws-upstream.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-upstream.cjs","names":["https","http","computeAcceptKey","WebSocketConnection"],"sources":["../src/ws-upstream.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { randomBytes } from \"node:crypto\";\nimport { WebSocketConnection, computeAcceptKey, type WebSocketLimits } from \"./ws-framing.js\";\n\nconst REFUSAL_BODY_LIMIT = 64 * 1024;\nconst UPGRADE_TIMEOUT_MS = 30_000;\nconst MAX_TIMER_DELAY_MS = 2_147_483_647;\n// Reviewed Live startup uses Authorization plus connector-owned RFC 6455 headers.\nconst PROVIDER_HEADERS = new Set([\"authorization\"]);\n\n/** Connect to a provider using verified TLS, or an explicitly configured loopback test peer.\n * The caller's signal owns the session lifetime; the upgrade budget defaults to thirty seconds.\n */\nexport async function connectUpstreamWebSocket(\n url: URL,\n headers: Record<string, string>,\n limits: WebSocketLimits,\n signal: AbortSignal,\n upstreamTimeoutMs = UPGRADE_TIMEOUT_MS,\n): Promise<WebSocketConnection> {\n if (!Number.isSafeInteger(upstreamTimeoutMs) || upstreamTimeoutMs <= 0) {\n throw new RangeError(\"Upstream WebSocket timeout must be a positive safe integer\");\n }\n const target = new URL(url.href);\n const secure = target.protocol === \"https:\" || target.protocol === \"wss:\";\n const loopback =\n target.hostname === \"localhost\" ||\n target.hostname === \"[::1]\" ||\n /^127\\.(?:\\d{1,3}\\.){2}\\d{1,3}$/.test(target.hostname);\n if (\n target.username ||\n target.password ||\n target.hash ||\n (!secure && (!(target.protocol === \"http:\" || target.protocol === \"ws:\") || !loopback))\n ) {\n throw new Error(\"Invalid upstream WebSocket URL\");\n }\n for (const value of [limits.maxMessageBytes, limits.maxBufferedBytes, limits.maxWriteBytes]) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))\n throw new RangeError(\"WebSocket limits must be positive safe integers\");\n }\n if (signal.aborted) throw new Error(\"Upstream WebSocket aborted\");\n target.protocol = secure ? \"https:\" : \"http:\";\n const key = randomBytes(16).toString(\"base64\");\n const outgoing: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (PROVIDER_HEADERS.has(name.toLowerCase())) outgoing[name.toLowerCase()] = value;\n }\n Object.assign(outgoing, {\n Upgrade: \"websocket\",\n Connection: \"Upgrade\",\n \"Sec-WebSocket-Key\": key,\n \"Sec-WebSocket-Version\": \"13\",\n });\n return new Promise<WebSocketConnection>((resolve, reject) => {\n let settled = false;\n let connection: WebSocketConnection | undefined;\n const request = (secure ? https : http).request(target, {\n method: \"GET\",\n headers: outgoing,\n agent: false,\n ...(secure ? { rejectUnauthorized: true } : {}),\n });\n const started = performance.now();\n const onTimeout = () => {\n const remaining = upstreamTimeoutMs - (performance.now() - started);\n if (remaining > 0) timer = setTimeout(onTimeout, Math.min(remaining, MAX_TIMER_DELAY_MS));\n else fail(\"Upstream WebSocket upgrade timed out\");\n };\n // Node clamps overflowing setTimeout delays to one millisecond. Keep the\n // full validated budget by scheduling bounded segments against elapsed time.\n let timer = setTimeout(onTimeout, Math.min(upstreamTimeoutMs, MAX_TIMER_DELAY_MS));\n const cleanup = () => {\n clearTimeout(timer);\n signal.removeEventListener(\"abort\", abort);\n };\n const fail = (message: string) => {\n if (settled) return;\n settled = true;\n cleanup();\n request.destroy();\n reject(new Error(message));\n };\n const abort = () => {\n if (connection) connection.destroy();\n else fail(\"Upstream WebSocket aborted\");\n };\n signal.addEventListener(\"abort\", abort, { once: true });\n request.on(\"error\", () => fail(\"Upstream WebSocket connection failed\"));\n request.once(\"response\", (response) => {\n let bytes = 0;\n response.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n if (bytes > REFUSAL_BODY_LIMIT) {\n fail(\"Upstream WebSocket refusal body limit exceeded\");\n response.destroy();\n }\n });\n response.once(\"end\", () =>\n fail(`Upstream WebSocket refused (HTTP ${response.statusCode ?? 0})`),\n );\n response.once(\"error\", () => fail(\"Upstream WebSocket response failed\"));\n response.once(\"aborted\", () => fail(\"Upstream WebSocket response failed\"));\n });\n request.once(\"upgrade\", (response, socket, head) => {\n if (settled) {\n socket.destroy();\n return;\n }\n const tokens = (value: string | undefined) =>\n value?.split(\",\").map((token) => token.trim().toLowerCase()) ?? [];\n if (\n response.statusCode !== 101 ||\n !tokens(response.headers.upgrade).includes(\"websocket\") ||\n !tokens(response.headers.connection).includes(\"upgrade\") ||\n response.headers[\"sec-websocket-accept\"] !== computeAcceptKey(key) ||\n response.headers[\"sec-websocket-extensions\"] !== undefined ||\n response.headers[\"sec-websocket-protocol\"] !== undefined\n ) {\n socket.destroy();\n fail(\"Invalid upstream WebSocket upgrade\");\n return;\n }\n socket.pause();\n connection = new WebSocketConnection(socket, {\n maxMessageBytes: limits.maxMessageBytes ?? 1024 * 1024,\n maxBufferedBytes: limits.maxBufferedBytes ?? 2 * 1024 * 1024,\n maxWriteBytes: limits.maxWriteBytes ?? 1024 * 1024,\n maskOutgoing: true,\n });\n connection.once(\"close\", cleanup);\n settled = true;\n clearTimeout(timer);\n // Pause until the promise consumer can register its message/error listeners.\n // unshift preserves ordering between upgrade head and subsequent socket bytes.\n if (head.length) socket.unshift(head);\n resolve(connection);\n setImmediate(() => {\n if (!socket.destroyed) socket.resume();\n });\n });\n request.end();\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { WebSocketConnection, computeAcceptKey } from "./ws-framing.js";
|
|
2
|
+
import * as http$1 from "node:http";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import * as https from "node:https";
|
|
5
|
+
|
|
6
|
+
//#region src/ws-upstream.ts
|
|
7
|
+
const REFUSAL_BODY_LIMIT = 64 * 1024;
|
|
8
|
+
const UPGRADE_TIMEOUT_MS = 3e4;
|
|
9
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
10
|
+
const PROVIDER_HEADERS = new Set(["authorization"]);
|
|
11
|
+
/** Connect to a provider using verified TLS, or an explicitly configured loopback test peer.
|
|
12
|
+
* The caller's signal owns the session lifetime; the upgrade budget defaults to thirty seconds.
|
|
13
|
+
*/
|
|
14
|
+
async function connectUpstreamWebSocket(url, headers, limits, signal, upstreamTimeoutMs = UPGRADE_TIMEOUT_MS) {
|
|
15
|
+
if (!Number.isSafeInteger(upstreamTimeoutMs) || upstreamTimeoutMs <= 0) throw new RangeError("Upstream WebSocket timeout must be a positive safe integer");
|
|
16
|
+
const target = new URL(url.href);
|
|
17
|
+
const secure = target.protocol === "https:" || target.protocol === "wss:";
|
|
18
|
+
const loopback = target.hostname === "localhost" || target.hostname === "[::1]" || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/.test(target.hostname);
|
|
19
|
+
if (target.username || target.password || target.hash || !secure && (!(target.protocol === "http:" || target.protocol === "ws:") || !loopback)) throw new Error("Invalid upstream WebSocket URL");
|
|
20
|
+
for (const value of [
|
|
21
|
+
limits.maxMessageBytes,
|
|
22
|
+
limits.maxBufferedBytes,
|
|
23
|
+
limits.maxWriteBytes
|
|
24
|
+
]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new RangeError("WebSocket limits must be positive safe integers");
|
|
25
|
+
if (signal.aborted) throw new Error("Upstream WebSocket aborted");
|
|
26
|
+
target.protocol = secure ? "https:" : "http:";
|
|
27
|
+
const key = randomBytes(16).toString("base64");
|
|
28
|
+
const outgoing = {};
|
|
29
|
+
for (const [name, value] of Object.entries(headers)) if (PROVIDER_HEADERS.has(name.toLowerCase())) outgoing[name.toLowerCase()] = value;
|
|
30
|
+
Object.assign(outgoing, {
|
|
31
|
+
Upgrade: "websocket",
|
|
32
|
+
Connection: "Upgrade",
|
|
33
|
+
"Sec-WebSocket-Key": key,
|
|
34
|
+
"Sec-WebSocket-Version": "13"
|
|
35
|
+
});
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let settled = false;
|
|
38
|
+
let connection;
|
|
39
|
+
const request = (secure ? https : http$1).request(target, {
|
|
40
|
+
method: "GET",
|
|
41
|
+
headers: outgoing,
|
|
42
|
+
agent: false,
|
|
43
|
+
...secure ? { rejectUnauthorized: true } : {}
|
|
44
|
+
});
|
|
45
|
+
const started = performance.now();
|
|
46
|
+
const onTimeout = () => {
|
|
47
|
+
const remaining = upstreamTimeoutMs - (performance.now() - started);
|
|
48
|
+
if (remaining > 0) timer = setTimeout(onTimeout, Math.min(remaining, MAX_TIMER_DELAY_MS));
|
|
49
|
+
else fail("Upstream WebSocket upgrade timed out");
|
|
50
|
+
};
|
|
51
|
+
let timer = setTimeout(onTimeout, Math.min(upstreamTimeoutMs, MAX_TIMER_DELAY_MS));
|
|
52
|
+
const cleanup = () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
signal.removeEventListener("abort", abort);
|
|
55
|
+
};
|
|
56
|
+
const fail = (message) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
cleanup();
|
|
60
|
+
request.destroy();
|
|
61
|
+
reject(new Error(message));
|
|
62
|
+
};
|
|
63
|
+
const abort = () => {
|
|
64
|
+
if (connection) connection.destroy();
|
|
65
|
+
else fail("Upstream WebSocket aborted");
|
|
66
|
+
};
|
|
67
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
68
|
+
request.on("error", () => fail("Upstream WebSocket connection failed"));
|
|
69
|
+
request.once("response", (response) => {
|
|
70
|
+
let bytes = 0;
|
|
71
|
+
response.on("data", (chunk) => {
|
|
72
|
+
bytes += chunk.length;
|
|
73
|
+
if (bytes > REFUSAL_BODY_LIMIT) {
|
|
74
|
+
fail("Upstream WebSocket refusal body limit exceeded");
|
|
75
|
+
response.destroy();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
response.once("end", () => fail(`Upstream WebSocket refused (HTTP ${response.statusCode ?? 0})`));
|
|
79
|
+
response.once("error", () => fail("Upstream WebSocket response failed"));
|
|
80
|
+
response.once("aborted", () => fail("Upstream WebSocket response failed"));
|
|
81
|
+
});
|
|
82
|
+
request.once("upgrade", (response, socket, head) => {
|
|
83
|
+
if (settled) {
|
|
84
|
+
socket.destroy();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const tokens = (value) => value?.split(",").map((token) => token.trim().toLowerCase()) ?? [];
|
|
88
|
+
if (response.statusCode !== 101 || !tokens(response.headers.upgrade).includes("websocket") || !tokens(response.headers.connection).includes("upgrade") || response.headers["sec-websocket-accept"] !== computeAcceptKey(key) || response.headers["sec-websocket-extensions"] !== void 0 || response.headers["sec-websocket-protocol"] !== void 0) {
|
|
89
|
+
socket.destroy();
|
|
90
|
+
fail("Invalid upstream WebSocket upgrade");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
socket.pause();
|
|
94
|
+
connection = new WebSocketConnection(socket, {
|
|
95
|
+
maxMessageBytes: limits.maxMessageBytes ?? 1024 * 1024,
|
|
96
|
+
maxBufferedBytes: limits.maxBufferedBytes ?? 2 * 1024 * 1024,
|
|
97
|
+
maxWriteBytes: limits.maxWriteBytes ?? 1024 * 1024,
|
|
98
|
+
maskOutgoing: true
|
|
99
|
+
});
|
|
100
|
+
connection.once("close", cleanup);
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (head.length) socket.unshift(head);
|
|
104
|
+
resolve(connection);
|
|
105
|
+
setImmediate(() => {
|
|
106
|
+
if (!socket.destroyed) socket.resume();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
request.end();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
export { connectUpstreamWebSocket };
|
|
115
|
+
//# sourceMappingURL=ws-upstream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-upstream.js","names":["http"],"sources":["../src/ws-upstream.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport { randomBytes } from \"node:crypto\";\nimport { WebSocketConnection, computeAcceptKey, type WebSocketLimits } from \"./ws-framing.js\";\n\nconst REFUSAL_BODY_LIMIT = 64 * 1024;\nconst UPGRADE_TIMEOUT_MS = 30_000;\nconst MAX_TIMER_DELAY_MS = 2_147_483_647;\n// Reviewed Live startup uses Authorization plus connector-owned RFC 6455 headers.\nconst PROVIDER_HEADERS = new Set([\"authorization\"]);\n\n/** Connect to a provider using verified TLS, or an explicitly configured loopback test peer.\n * The caller's signal owns the session lifetime; the upgrade budget defaults to thirty seconds.\n */\nexport async function connectUpstreamWebSocket(\n url: URL,\n headers: Record<string, string>,\n limits: WebSocketLimits,\n signal: AbortSignal,\n upstreamTimeoutMs = UPGRADE_TIMEOUT_MS,\n): Promise<WebSocketConnection> {\n if (!Number.isSafeInteger(upstreamTimeoutMs) || upstreamTimeoutMs <= 0) {\n throw new RangeError(\"Upstream WebSocket timeout must be a positive safe integer\");\n }\n const target = new URL(url.href);\n const secure = target.protocol === \"https:\" || target.protocol === \"wss:\";\n const loopback =\n target.hostname === \"localhost\" ||\n target.hostname === \"[::1]\" ||\n /^127\\.(?:\\d{1,3}\\.){2}\\d{1,3}$/.test(target.hostname);\n if (\n target.username ||\n target.password ||\n target.hash ||\n (!secure && (!(target.protocol === \"http:\" || target.protocol === \"ws:\") || !loopback))\n ) {\n throw new Error(\"Invalid upstream WebSocket URL\");\n }\n for (const value of [limits.maxMessageBytes, limits.maxBufferedBytes, limits.maxWriteBytes]) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))\n throw new RangeError(\"WebSocket limits must be positive safe integers\");\n }\n if (signal.aborted) throw new Error(\"Upstream WebSocket aborted\");\n target.protocol = secure ? \"https:\" : \"http:\";\n const key = randomBytes(16).toString(\"base64\");\n const outgoing: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (PROVIDER_HEADERS.has(name.toLowerCase())) outgoing[name.toLowerCase()] = value;\n }\n Object.assign(outgoing, {\n Upgrade: \"websocket\",\n Connection: \"Upgrade\",\n \"Sec-WebSocket-Key\": key,\n \"Sec-WebSocket-Version\": \"13\",\n });\n return new Promise<WebSocketConnection>((resolve, reject) => {\n let settled = false;\n let connection: WebSocketConnection | undefined;\n const request = (secure ? https : http).request(target, {\n method: \"GET\",\n headers: outgoing,\n agent: false,\n ...(secure ? { rejectUnauthorized: true } : {}),\n });\n const started = performance.now();\n const onTimeout = () => {\n const remaining = upstreamTimeoutMs - (performance.now() - started);\n if (remaining > 0) timer = setTimeout(onTimeout, Math.min(remaining, MAX_TIMER_DELAY_MS));\n else fail(\"Upstream WebSocket upgrade timed out\");\n };\n // Node clamps overflowing setTimeout delays to one millisecond. Keep the\n // full validated budget by scheduling bounded segments against elapsed time.\n let timer = setTimeout(onTimeout, Math.min(upstreamTimeoutMs, MAX_TIMER_DELAY_MS));\n const cleanup = () => {\n clearTimeout(timer);\n signal.removeEventListener(\"abort\", abort);\n };\n const fail = (message: string) => {\n if (settled) return;\n settled = true;\n cleanup();\n request.destroy();\n reject(new Error(message));\n };\n const abort = () => {\n if (connection) connection.destroy();\n else fail(\"Upstream WebSocket aborted\");\n };\n signal.addEventListener(\"abort\", abort, { once: true });\n request.on(\"error\", () => fail(\"Upstream WebSocket connection failed\"));\n request.once(\"response\", (response) => {\n let bytes = 0;\n response.on(\"data\", (chunk: Buffer) => {\n bytes += chunk.length;\n if (bytes > REFUSAL_BODY_LIMIT) {\n fail(\"Upstream WebSocket refusal body limit exceeded\");\n response.destroy();\n }\n });\n response.once(\"end\", () =>\n fail(`Upstream WebSocket refused (HTTP ${response.statusCode ?? 0})`),\n );\n response.once(\"error\", () => fail(\"Upstream WebSocket response failed\"));\n response.once(\"aborted\", () => fail(\"Upstream WebSocket response failed\"));\n });\n request.once(\"upgrade\", (response, socket, head) => {\n if (settled) {\n socket.destroy();\n return;\n }\n const tokens = (value: string | undefined) =>\n value?.split(\",\").map((token) => token.trim().toLowerCase()) ?? [];\n if (\n response.statusCode !== 101 ||\n !tokens(response.headers.upgrade).includes(\"websocket\") ||\n !tokens(response.headers.connection).includes(\"upgrade\") ||\n response.headers[\"sec-websocket-accept\"] !== computeAcceptKey(key) ||\n response.headers[\"sec-websocket-extensions\"] !== undefined ||\n response.headers[\"sec-websocket-protocol\"] !== undefined\n ) {\n socket.destroy();\n fail(\"Invalid upstream WebSocket upgrade\");\n return;\n }\n socket.pause();\n connection = new WebSocketConnection(socket, {\n maxMessageBytes: limits.maxMessageBytes ?? 1024 * 1024,\n maxBufferedBytes: limits.maxBufferedBytes ?? 2 * 1024 * 1024,\n maxWriteBytes: limits.maxWriteBytes ?? 1024 * 1024,\n maskOutgoing: true,\n });\n connection.once(\"close\", cleanup);\n settled = true;\n clearTimeout(timer);\n // Pause until the promise consumer can register its message/error listeners.\n // unshift preserves ordering between upgrade head and subsequent socket bytes.\n if (head.length) socket.unshift(head);\n resolve(connection);\n setImmediate(() => {\n if (!socket.destroyed) socket.resume();\n });\n });\n request.end();\n });\n}\n"],"mappings":";;;;;;AAKA,MAAM,qBAAqB,KAAK;AAChC,MAAM,qBAAqB;AAC3B,MAAM,qBAAqB;AAE3B,MAAM,mBAAmB,IAAI,IAAI,CAAC,gBAAgB,CAAC;;;;AAKnD,eAAsB,yBACpB,KACA,SACA,QACA,QACA,oBAAoB,oBACU;AAC9B,KAAI,CAAC,OAAO,cAAc,kBAAkB,IAAI,qBAAqB,EACnE,OAAM,IAAI,WAAW,6DAA6D;CAEpF,MAAM,SAAS,IAAI,IAAI,IAAI,KAAK;CAChC,MAAM,SAAS,OAAO,aAAa,YAAY,OAAO,aAAa;CACnE,MAAM,WACJ,OAAO,aAAa,eACpB,OAAO,aAAa,WACpB,iCAAiC,KAAK,OAAO,SAAS;AACxD,KACE,OAAO,YACP,OAAO,YACP,OAAO,QACN,CAAC,WAAW,EAAE,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU,CAAC,UAE7E,OAAM,IAAI,MAAM,iCAAiC;AAEnD,MAAK,MAAM,SAAS;EAAC,OAAO;EAAiB,OAAO;EAAkB,OAAO;EAAc,CACzF,KAAI,UAAU,WAAc,CAAC,OAAO,cAAc,MAAM,IAAI,SAAS,GACnE,OAAM,IAAI,WAAW,kDAAkD;AAE3E,KAAI,OAAO,QAAS,OAAM,IAAI,MAAM,6BAA6B;AACjE,QAAO,WAAW,SAAS,WAAW;CACtC,MAAM,MAAM,YAAY,GAAG,CAAC,SAAS,SAAS;CAC9C,MAAM,WAAmC,EAAE;AAC3C,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,QAAQ,CACjD,KAAI,iBAAiB,IAAI,KAAK,aAAa,CAAC,CAAE,UAAS,KAAK,aAAa,IAAI;AAE/E,QAAO,OAAO,UAAU;EACtB,SAAS;EACT,YAAY;EACZ,qBAAqB;EACrB,yBAAyB;EAC1B,CAAC;AACF,QAAO,IAAI,SAA8B,SAAS,WAAW;EAC3D,IAAI,UAAU;EACd,IAAI;EACJ,MAAM,WAAW,SAAS,QAAQA,QAAM,QAAQ,QAAQ;GACtD,QAAQ;GACR,SAAS;GACT,OAAO;GACP,GAAI,SAAS,EAAE,oBAAoB,MAAM,GAAG,EAAE;GAC/C,CAAC;EACF,MAAM,UAAU,YAAY,KAAK;EACjC,MAAM,kBAAkB;GACtB,MAAM,YAAY,qBAAqB,YAAY,KAAK,GAAG;AAC3D,OAAI,YAAY,EAAG,SAAQ,WAAW,WAAW,KAAK,IAAI,WAAW,mBAAmB,CAAC;OACpF,MAAK,uCAAuC;;EAInD,IAAI,QAAQ,WAAW,WAAW,KAAK,IAAI,mBAAmB,mBAAmB,CAAC;EAClF,MAAM,gBAAgB;AACpB,gBAAa,MAAM;AACnB,UAAO,oBAAoB,SAAS,MAAM;;EAE5C,MAAM,QAAQ,YAAoB;AAChC,OAAI,QAAS;AACb,aAAU;AACV,YAAS;AACT,WAAQ,SAAS;AACjB,UAAO,IAAI,MAAM,QAAQ,CAAC;;EAE5B,MAAM,cAAc;AAClB,OAAI,WAAY,YAAW,SAAS;OAC/B,MAAK,6BAA6B;;AAEzC,SAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,MAAM,CAAC;AACvD,UAAQ,GAAG,eAAe,KAAK,uCAAuC,CAAC;AACvE,UAAQ,KAAK,aAAa,aAAa;GACrC,IAAI,QAAQ;AACZ,YAAS,GAAG,SAAS,UAAkB;AACrC,aAAS,MAAM;AACf,QAAI,QAAQ,oBAAoB;AAC9B,UAAK,iDAAiD;AACtD,cAAS,SAAS;;KAEpB;AACF,YAAS,KAAK,aACZ,KAAK,oCAAoC,SAAS,cAAc,EAAE,GAAG,CACtE;AACD,YAAS,KAAK,eAAe,KAAK,qCAAqC,CAAC;AACxE,YAAS,KAAK,iBAAiB,KAAK,qCAAqC,CAAC;IAC1E;AACF,UAAQ,KAAK,YAAY,UAAU,QAAQ,SAAS;AAClD,OAAI,SAAS;AACX,WAAO,SAAS;AAChB;;GAEF,MAAM,UAAU,UACd,OAAO,MAAM,IAAI,CAAC,KAAK,UAAU,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,EAAE;AACpE,OACE,SAAS,eAAe,OACxB,CAAC,OAAO,SAAS,QAAQ,QAAQ,CAAC,SAAS,YAAY,IACvD,CAAC,OAAO,SAAS,QAAQ,WAAW,CAAC,SAAS,UAAU,IACxD,SAAS,QAAQ,4BAA4B,iBAAiB,IAAI,IAClE,SAAS,QAAQ,gCAAgC,UACjD,SAAS,QAAQ,8BAA8B,QAC/C;AACA,WAAO,SAAS;AAChB,SAAK,qCAAqC;AAC1C;;AAEF,UAAO,OAAO;AACd,gBAAa,IAAI,oBAAoB,QAAQ;IAC3C,iBAAiB,OAAO,mBAAmB,OAAO;IAClD,kBAAkB,OAAO,oBAAoB,IAAI,OAAO;IACxD,eAAe,OAAO,iBAAiB,OAAO;IAC9C,cAAc;IACf,CAAC;AACF,cAAW,KAAK,SAAS,QAAQ;AACjC,aAAU;AACV,gBAAa,MAAM;AAGnB,OAAI,KAAK,OAAQ,QAAO,QAAQ,KAAK;AACrC,WAAQ,WAAW;AACnB,sBAAmB;AACjB,QAAI,CAAC,OAAO,UAAW,QAAO,QAAQ;KACtC;IACF;AACF,UAAQ,KAAK;GACb"}
|