@copilotkit/aimock 1.45.1 → 1.47.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 +25 -1
- package/README.md +11 -2
- package/dist/agui-mock.cjs +25 -25
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/aws-event-stream.cjs +20 -23
- package/dist/aws-event-stream.cjs.map +1 -1
- package/dist/aws-event-stream.d.cts +0 -1
- package/dist/aws-event-stream.d.ts +0 -1
- package/dist/aws-event-stream.js +20 -24
- package/dist/aws-event-stream.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +139 -161
- 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 +137 -159
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +175 -216
- 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 +172 -213
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +107 -180
- 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 +106 -179
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +140 -44
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +141 -46
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +113 -156
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +113 -156
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +295 -218
- 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 +295 -219
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +411 -75
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +51 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +51 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +402 -74
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-handler.cjs +7 -3
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +7 -3
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +235 -202
- 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 +234 -202
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +62 -3
- package/dist/misbehavior.cjs.map +1 -1
- package/dist/misbehavior.d.cts.map +1 -1
- package/dist/misbehavior.d.ts.map +1 -1
- package/dist/misbehavior.js +63 -4
- package/dist/misbehavior.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ndjson-writer.cjs +21 -23
- package/dist/ndjson-writer.cjs.map +1 -1
- package/dist/ndjson-writer.d.cts +0 -1
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +0 -1
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ndjson-writer.js +21 -24
- package/dist/ndjson-writer.js.map +1 -1
- package/dist/ollama.cjs +276 -210
- 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 +273 -207
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +51 -74
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +37 -60
- package/dist/openrouter-video.js.map +1 -1
- package/dist/raw-validation.cjs +177 -0
- package/dist/raw-validation.cjs.map +1 -0
- package/dist/raw-validation.js +176 -0
- package/dist/raw-validation.js.map +1 -0
- package/dist/recorder.cjs +176 -95
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/deliver.cjs +144 -0
- package/dist/response/deliver.cjs.map +1 -0
- package/dist/response/deliver.js +143 -0
- package/dist/response/deliver.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/frame-loop.cjs +102 -0
- package/dist/response/frame-loop.cjs.map +1 -0
- package/dist/response/frame-loop.js +100 -0
- package/dist/response/frame-loop.js.map +1 -0
- package/dist/response/lifecycle.cjs +223 -0
- package/dist/response/lifecycle.cjs.map +1 -0
- package/dist/response/lifecycle.js +211 -0
- package/dist/response/lifecycle.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/proxy.cjs +71 -0
- package/dist/response/proxy.cjs.map +1 -0
- package/dist/response/proxy.js +70 -0
- package/dist/response/proxy.js.map +1 -0
- package/dist/response/raw.cjs +228 -0
- package/dist/response/raw.cjs.map +1 -0
- package/dist/response/raw.js +227 -0
- package/dist/response/raw.js.map +1 -0
- package/dist/response/timing.cjs +157 -0
- package/dist/response/timing.cjs.map +1 -0
- package/dist/response/timing.js +150 -0
- package/dist/response/timing.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +304 -196
- 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 +303 -195
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +24 -4
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +7 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -5
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +293 -248
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +294 -249
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +56 -30
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +0 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +0 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +55 -31
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +31 -43
- 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 +31 -43
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +231 -9
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +231 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-framing.cjs +12 -6
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +2 -0
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -0
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +12 -6
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +131 -96
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +130 -95
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +108 -124
- 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 +105 -121
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +80 -59
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +79 -58
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +57 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-framing.d.cts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"ws-framing.d.cts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AAkGmC,UA5ElB,eAAA,CA4EkB;iBAAc,CAAA,EAAA,MAAA;kBApER,CAAA,EAAA,MAAA;EAAY,aAAA,CAAA,EAAA,MAAA;EA+VrC;EAMA,YAAA,CAAA,EAAA,OAAkB;;AAC3B,cAtWM,mBAAA,SAA4B,YAAA,CAsW7B;UACE,MAAA;UACH,MAAA;UACR,MAAA;EAAmB,QAAA,SAAA;;;;;;sBA3VA,GAAA,CAAI,iBAAgB;;mCAsDP,cAAc;;;;;;;;;;;;iBA2RjC,gBAAA;iBAMA,kBAAA,MACT,MAAA,CAAK,yBACF,GAAA,CAAI,iBACH,kBACR"}
|
package/dist/ws-framing.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ declare class WebSocketConnection extends EventEmitter {
|
|
|
24
24
|
constructor(socket: net.Socket, limits?: WebSocketLimits);
|
|
25
25
|
send(data: string): void;
|
|
26
26
|
sendAsync(data: string, signal?: AbortSignal): Promise<void>;
|
|
27
|
+
/** Mark an unannounced connection loss and emit close once. */
|
|
28
|
+
private lost;
|
|
27
29
|
private release;
|
|
28
30
|
close(code?: number, reason?: string): void;
|
|
29
31
|
destroy(): void;
|
package/dist/ws-framing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-framing.d.ts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"ws-framing.d.ts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AAkGmC,UA5ElB,eAAA,CA4EkB;iBAAc,CAAA,EAAA,MAAA;kBApER,CAAA,EAAA,MAAA;EAAY,aAAA,CAAA,EAAA,MAAA;EA+VrC;EAMA,YAAA,CAAA,EAAA,OAAkB;;AAC3B,cAtWM,mBAAA,SAA4B,YAAA,CAsW7B;UACE,MAAA;UACH,MAAA;UACR,MAAA;EAAmB,QAAA,SAAA;;;;;;sBA3VA,GAAA,CAAI,iBAAgB;;mCAsDP,cAAc;;;;;;;;;;;;iBA2RjC,gBAAA;iBAMA,kBAAA,MACT,MAAA,CAAK,yBACF,GAAA,CAAI,iBACH,kBACR"}
|
package/dist/ws-framing.js
CHANGED
|
@@ -49,12 +49,11 @@ var WebSocketConnection = class extends EventEmitter {
|
|
|
49
49
|
this.parseFrames();
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
socket.on("close", () =>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
52
|
+
socket.on("close", () => this.lost());
|
|
53
|
+
socket.on("end", () => {
|
|
54
|
+
if (this.closed) return;
|
|
55
|
+
this.socket.destroy();
|
|
56
|
+
this.lost();
|
|
58
57
|
});
|
|
59
58
|
socket.on("error", (err) => {
|
|
60
59
|
this.emit("error", err);
|
|
@@ -102,6 +101,13 @@ var WebSocketConnection = class extends EventEmitter {
|
|
|
102
101
|
}
|
|
103
102
|
});
|
|
104
103
|
}
|
|
104
|
+
/** Mark an unannounced connection loss and emit close once. */
|
|
105
|
+
lost() {
|
|
106
|
+
if (this.closed) return;
|
|
107
|
+
this.closed = true;
|
|
108
|
+
this.release(/* @__PURE__ */ new Error("WebSocket closed"));
|
|
109
|
+
this.emit("close", 1006, "Connection lost");
|
|
110
|
+
}
|
|
105
111
|
release(error) {
|
|
106
112
|
this.buffer = Buffer.alloc(0);
|
|
107
113
|
this.fragments = [];
|
package/dist/ws-framing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-framing.js","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport interface WebSocketLimits {\n maxMessageBytes?: number;\n maxBufferedBytes?: number;\n maxWriteBytes?: number;\n /** Client role: mask outgoing frames and require unmasked incoming frames. */\n maskOutgoing?: boolean;\n}\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n private fragmentBytes = 0;\n private fragmented = false;\n private pendingWriteBytes = 0;\n private pendingWrites = new Set<(error: Error) => void>();\n private limits: WebSocketLimits;\n\n constructor(socket: net.Socket, limits: WebSocketLimits = {}) {\n super();\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 }\n this.limits = { ...limits };\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n // Byte-idle consumers must see partial frames and control traffic too.\n // Emit no payload, and notify before parsing can dispatch a message.\n if (data.length > 0 && !this.closed) this.emit(\"activity\");\n let offset = 0;\n while (offset < data.length && !this.closed) {\n const room =\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes - this.buffer.length;\n if (room <= 0) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const end = offset + Math.min(room, data.length - offset);\n this.buffer = Buffer.concat([this.buffer, data.subarray(offset, end)]);\n offset = end;\n this.parseFrames();\n }\n });\n\n socket.on(\"close\", () => {\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n void this.sendAsync(data).catch(() => {\n /* Legacy fire-and-forget API. */\n });\n }\n\n sendAsync(data: string, signal?: AbortSignal): Promise<void> {\n if (this.closed || this.socket.destroyed) return Promise.reject(new Error(\"WebSocket closed\"));\n if (signal?.aborted) return Promise.reject(new Error(\"WebSocket send aborted\"));\n const length = Buffer.byteLength(data);\n const frameBytes =\n length + (length < 126 ? 2 : length < 65536 ? 4 : 10) + (this.limits.maskOutgoing ? 4 : 0);\n if (\n frameBytes >\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength)\n ) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return Promise.reject(new Error(\"WebSocket write limit exceeded\"));\n }\n this.pendingWriteBytes += frameBytes;\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n this.pendingWriteBytes -= frameBytes;\n this.pendingWrites.delete(fail);\n signal?.removeEventListener(\"abort\", abort);\n if (error) reject(error);\n else resolve();\n };\n const fail = (error: Error) => finish(error);\n const abort = () => {\n finish(new Error(\"WebSocket send aborted\"));\n this.destroy();\n };\n this.pendingWrites.add(fail);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.socket.write(this.encodeFrame(OP_TEXT, Buffer.from(data)), (error?: Error | null) => {\n if (error) finish(new Error(\"WebSocket write failed\"));\n else finish();\n });\n } catch {\n finish(new Error(\"WebSocket write failed\"));\n }\n });\n }\n\n private release(error: Error): void {\n this.buffer = Buffer.alloc(0);\n this.fragments = [];\n this.fragmentBytes = 0;\n for (const fail of this.pendingWrites) fail(error);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n\n const encodedReason = Buffer.from(reason, \"utf-8\");\n let reasonEnd = Math.min(encodedReason.length, 123);\n // A continuation byte at the cut means its code point straddles the limit.\n while (reasonEnd < encodedReason.length && (encodedReason[reasonEnd] & 0xc0) === 0x80) {\n reasonEnd--;\n }\n const reasonBuf = encodedReason.subarray(0, reasonEnd);\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket destroyed\"));\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private encodeFrame(opcode: number, payload: Buffer): Buffer {\n const length = payload.length;\n const headerSize = length < 126 ? 2 : length < 65536 ? 4 : 10;\n const maskSize = this.limits.maskOutgoing ? 4 : 0;\n const frame = Buffer.allocUnsafe(headerSize + maskSize + length);\n frame[0] = 0x80 | opcode;\n frame[1] = (maskSize ? 0x80 : 0) | (length < 126 ? length : length < 65536 ? 126 : 127);\n if (headerSize === 4) frame.writeUInt16BE(length, 2);\n if (headerSize === 10) frame.writeBigUInt64BE(BigInt(length), 2);\n if (maskSize) {\n const mask = randomBytes(4);\n mask.copy(frame, headerSize);\n for (let i = 0; i < length; i++) frame[headerSize + 4 + i] = payload[i] ^ mask[i % 4];\n } else payload.copy(frame, headerSize);\n return frame;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n const available =\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength);\n const headerBytes = 2 + (this.limits.maskOutgoing ? 4 : 0);\n if (opcode === OP_CLOSE && payload.length + headerBytes > available) {\n // Keep even the final control frame inside the configured socket budget.\n if (available < headerBytes + 2) {\n this.socket.destroy();\n return;\n }\n payload = payload.subarray(0, 2);\n } else if (payload.length + headerBytes > available) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return;\n }\n const frame = this.encodeFrame(opcode, payload);\n this.socket.write(frame);\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n if (\n byte0 & 0x70 ||\n masked === Boolean(this.limits.maskOutgoing) ||\n ![OP_CONTINUATION, OP_TEXT, 2, OP_CLOSE, OP_PING, OP_PONG].includes(opcode) ||\n (opcode >= 8 && (!fin || (byte1 & 0x7f) > 125)) ||\n (opcode === OP_CONTINUATION && !this.fragmented) ||\n (opcode === OP_TEXT && this.fragmented)\n ) {\n this.close(1002, \"Invalid WebSocket frame\");\n return;\n }\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n const advertised = this.buffer.readBigUInt64BE(2);\n if (advertised & (1n << 63n)) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (advertised > BigInt(Number.MAX_SAFE_INTEGER)) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n payloadLength = Number(advertised);\n offset = 10;\n }\n\n if (\n ((byte1 & 0x7f) === 126 && payloadLength < 126) ||\n ((byte1 & 0x7f) === 127 && payloadLength < 65536)\n ) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (\n opcode < 8 &&\n payloadLength > (this.limits.maxMessageBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n if (\n payloadLength + offset + (masked ? 4 : 0) >\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n if (payload.length === 1) {\n this.close(1002, \"Invalid WebSocket close\");\n return;\n }\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragmented = !fin;\n this.fragmentBytes += payload.length;\n if (payload.length) this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.fragmentBytes = 0;\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n limits?: WebSocketLimits,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket, limits);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ws-framing.js","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":["/**\n * Minimal RFC 6455 WebSocket server implementation.\n *\n * Zero dependencies — uses only Node.js builtins (node:crypto, node:events).\n * Supports text frames, ping/pong, close handshake, and client frame unmasking.\n * Designed for a mock server — no extensions, no binary frames, no compression.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { EventEmitter } from \"node:events\";\nimport type * as net from \"node:net\";\nimport type * as http from \"node:http\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n\n// Opcodes\nconst OP_CONTINUATION = 0x0;\nconst OP_TEXT = 0x1;\nconst OP_CLOSE = 0x8;\nconst OP_PING = 0x9;\nconst OP_PONG = 0xa;\n\nexport interface WebSocketLimits {\n maxMessageBytes?: number;\n maxBufferedBytes?: number;\n maxWriteBytes?: number;\n /** Client role: mask outgoing frames and require unmasked incoming frames. */\n maskOutgoing?: boolean;\n}\n\nexport class WebSocketConnection extends EventEmitter {\n private socket: net.Socket;\n private buffer: Buffer = Buffer.alloc(0);\n private closed = false;\n\n // For fragmented messages (continuation frames)\n private fragments: Buffer[] = [];\n\n private fragmentBytes = 0;\n private fragmented = false;\n private pendingWriteBytes = 0;\n private pendingWrites = new Set<(error: Error) => void>();\n private limits: WebSocketLimits;\n\n constructor(socket: net.Socket, limits: WebSocketLimits = {}) {\n super();\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 }\n this.limits = { ...limits };\n this.socket = socket;\n\n socket.on(\"data\", (data: Buffer) => {\n // Byte-idle consumers must see partial frames and control traffic too.\n // Emit no payload, and notify before parsing can dispatch a message.\n if (data.length > 0 && !this.closed) this.emit(\"activity\");\n let offset = 0;\n while (offset < data.length && !this.closed) {\n const room =\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes - this.buffer.length;\n if (room <= 0) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const end = offset + Math.min(room, data.length - offset);\n this.buffer = Buffer.concat([this.buffer, data.subarray(offset, end)]);\n offset = end;\n this.parseFrames();\n }\n });\n\n socket.on(\"close\", () => this.lost());\n\n // A peer FIN without a close frame is a disconnect. HTTP upgrade sockets\n // allow half-open, so without this the loss surfaces only on a later\n // failed write. Data events precede \"end\", so frames that arrive just\n // before the FIN still parse, but any turn they start is cancelled at once\n // because the connection is then lost.\n socket.on(\"end\", () => {\n if (this.closed) return;\n this.socket.destroy();\n this.lost();\n });\n\n socket.on(\"error\", (err: Error) => {\n this.emit(\"error\", err);\n });\n }\n\n send(data: string): void {\n if (this.closed) return;\n void this.sendAsync(data).catch(() => {\n /* Legacy fire-and-forget API. */\n });\n }\n\n sendAsync(data: string, signal?: AbortSignal): Promise<void> {\n if (this.closed || this.socket.destroyed) return Promise.reject(new Error(\"WebSocket closed\"));\n if (signal?.aborted) return Promise.reject(new Error(\"WebSocket send aborted\"));\n const length = Buffer.byteLength(data);\n const frameBytes =\n length + (length < 126 ? 2 : length < 65536 ? 4 : 10) + (this.limits.maskOutgoing ? 4 : 0);\n if (\n frameBytes >\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength)\n ) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return Promise.reject(new Error(\"WebSocket write limit exceeded\"));\n }\n this.pendingWriteBytes += frameBytes;\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n this.pendingWriteBytes -= frameBytes;\n this.pendingWrites.delete(fail);\n signal?.removeEventListener(\"abort\", abort);\n if (error) reject(error);\n else resolve();\n };\n const fail = (error: Error) => finish(error);\n const abort = () => {\n finish(new Error(\"WebSocket send aborted\"));\n this.destroy();\n };\n this.pendingWrites.add(fail);\n signal?.addEventListener(\"abort\", abort, { once: true });\n try {\n this.socket.write(this.encodeFrame(OP_TEXT, Buffer.from(data)), (error?: Error | null) => {\n if (error) finish(new Error(\"WebSocket write failed\"));\n else finish();\n });\n } catch {\n finish(new Error(\"WebSocket write failed\"));\n }\n });\n }\n\n /** Mark an unannounced connection loss and emit close once. */\n private lost(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n this.emit(\"close\", 1006, \"Connection lost\");\n }\n\n private release(error: Error): void {\n this.buffer = Buffer.alloc(0);\n this.fragments = [];\n this.fragmentBytes = 0;\n for (const fail of this.pendingWrites) fail(error);\n }\n\n close(code = 1000, reason = \"\"): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n\n const encodedReason = Buffer.from(reason, \"utf-8\");\n let reasonEnd = Math.min(encodedReason.length, 123);\n // A continuation byte at the cut means its code point straddles the limit.\n while (reasonEnd < encodedReason.length && (encodedReason[reasonEnd] & 0xc0) === 0x80) {\n reasonEnd--;\n }\n const reasonBuf = encodedReason.subarray(0, reasonEnd);\n const payload = Buffer.alloc(2 + reasonBuf.length);\n payload.writeUInt16BE(code, 0);\n reasonBuf.copy(payload, 2);\n this.writeFrame(OP_CLOSE, payload);\n\n // Give the client a moment to receive the close frame before destroying.\n // If writeFrame failed (socket already destroyed), this is a no-op.\n setTimeout(() => {\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n // Emit close event for server-initiated closes so listeners\n // (e.g. activeConnections.delete) always fire.\n this.emit(\"close\", code, reason);\n }, 100);\n }\n\n destroy(): void {\n if (this.closed) return;\n this.closed = true;\n this.release(new Error(\"WebSocket destroyed\"));\n if (!this.socket.destroyed) {\n this.socket.destroy();\n }\n this.emit(\"close\", 1006, \"Connection destroyed\");\n }\n\n get isClosed(): boolean {\n return this.closed;\n }\n\n private encodeFrame(opcode: number, payload: Buffer): Buffer {\n const length = payload.length;\n const headerSize = length < 126 ? 2 : length < 65536 ? 4 : 10;\n const maskSize = this.limits.maskOutgoing ? 4 : 0;\n const frame = Buffer.allocUnsafe(headerSize + maskSize + length);\n frame[0] = 0x80 | opcode;\n frame[1] = (maskSize ? 0x80 : 0) | (length < 126 ? length : length < 65536 ? 126 : 127);\n if (headerSize === 4) frame.writeUInt16BE(length, 2);\n if (headerSize === 10) frame.writeBigUInt64BE(BigInt(length), 2);\n if (maskSize) {\n const mask = randomBytes(4);\n mask.copy(frame, headerSize);\n for (let i = 0; i < length; i++) frame[headerSize + 4 + i] = payload[i] ^ mask[i % 4];\n } else payload.copy(frame, headerSize);\n return frame;\n }\n\n private writeFrame(opcode: number, payload: Buffer): void {\n if (this.socket.destroyed) return;\n const available =\n (this.limits.maxWriteBytes ?? Infinity) -\n Math.max(this.pendingWriteBytes, this.socket.writableLength);\n const headerBytes = 2 + (this.limits.maskOutgoing ? 4 : 0);\n if (opcode === OP_CLOSE && payload.length + headerBytes > available) {\n // Keep even the final control frame inside the configured socket budget.\n if (available < headerBytes + 2) {\n this.socket.destroy();\n return;\n }\n payload = payload.subarray(0, 2);\n } else if (payload.length + headerBytes > available) {\n this.close(1009, \"WebSocket write limit exceeded\");\n return;\n }\n const frame = this.encodeFrame(opcode, payload);\n this.socket.write(frame);\n }\n\n private parseFrames(): void {\n while (this.buffer.length >= 2 && !this.closed) {\n const byte0 = this.buffer[0];\n const byte1 = this.buffer[1];\n\n const fin = (byte0 & 0x80) !== 0;\n const opcode = byte0 & 0x0f;\n const masked = (byte1 & 0x80) !== 0;\n if (\n byte0 & 0x70 ||\n masked === Boolean(this.limits.maskOutgoing) ||\n ![OP_CONTINUATION, OP_TEXT, 2, OP_CLOSE, OP_PING, OP_PONG].includes(opcode) ||\n (opcode >= 8 && (!fin || (byte1 & 0x7f) > 125)) ||\n (opcode === OP_CONTINUATION && !this.fragmented) ||\n (opcode === OP_TEXT && this.fragmented)\n ) {\n this.close(1002, \"Invalid WebSocket frame\");\n return;\n }\n let payloadLength = byte1 & 0x7f;\n let offset = 2;\n\n if (payloadLength === 126) {\n if (this.buffer.length < 4) return; // need more data\n payloadLength = this.buffer.readUInt16BE(2);\n offset = 4;\n } else if (payloadLength === 127) {\n if (this.buffer.length < 10) return;\n const advertised = this.buffer.readBigUInt64BE(2);\n if (advertised & (1n << 63n)) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (advertised > BigInt(Number.MAX_SAFE_INTEGER)) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n payloadLength = Number(advertised);\n offset = 10;\n }\n\n if (\n ((byte1 & 0x7f) === 126 && payloadLength < 126) ||\n ((byte1 & 0x7f) === 127 && payloadLength < 65536)\n ) {\n this.close(1002, \"Invalid WebSocket length\");\n return;\n }\n if (\n opcode < 8 &&\n payloadLength > (this.limits.maxMessageBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket message limit exceeded\");\n return;\n }\n if (\n payloadLength + offset + (masked ? 4 : 0) >\n (this.limits.maxBufferedBytes ?? Infinity) - this.fragmentBytes\n ) {\n this.close(1009, \"WebSocket buffer limit exceeded\");\n return;\n }\n const maskSize = masked ? 4 : 0;\n const totalFrameSize = offset + maskSize + payloadLength;\n\n if (this.buffer.length < totalFrameSize) return; // need more data\n\n let maskKey: Buffer | null = null;\n if (masked) {\n maskKey = this.buffer.subarray(offset, offset + 4);\n offset += 4;\n }\n\n let payload = this.buffer.subarray(offset, offset + payloadLength);\n\n // Unmask client payload\n if (maskKey) {\n payload = Buffer.from(payload); // copy before mutating\n for (let i = 0; i < payload.length; i++) {\n payload[i] ^= maskKey[i % 4];\n }\n }\n\n // Consume the frame from the buffer\n this.buffer = this.buffer.subarray(totalFrameSize);\n\n this.handleFrame(fin, opcode, payload);\n }\n }\n\n private handleFrame(fin: boolean, opcode: number, payload: Buffer): void {\n // Control frames (opcode >= 0x8) must not be fragmented\n if (opcode === OP_PING) {\n this.writeFrame(OP_PONG, payload);\n return;\n }\n\n if (opcode === OP_PONG) {\n // Ignore unsolicited pongs\n return;\n }\n\n if (opcode === OP_CLOSE) {\n if (payload.length === 1) {\n this.close(1002, \"Invalid WebSocket close\");\n return;\n }\n const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;\n const reason = payload.length > 2 ? payload.subarray(2).toString(\"utf-8\") : \"\";\n\n if (!this.closed) {\n this.closed = true;\n this.release(new Error(\"WebSocket closed\"));\n // Echo close frame back\n this.writeFrame(OP_CLOSE, payload);\n this.socket.end();\n this.emit(\"close\", code, reason);\n }\n // If already closed (server-initiated or duplicate), ignore — the\n // close event was already emitted by close() or the first OP_CLOSE.\n return;\n }\n\n // Text or continuation frames\n if (opcode === OP_TEXT || opcode === OP_CONTINUATION) {\n this.fragmented = !fin;\n this.fragmentBytes += payload.length;\n if (payload.length) this.fragments.push(payload);\n\n if (fin) {\n const message = Buffer.concat(this.fragments).toString(\"utf-8\");\n this.fragments = [];\n this.fragmentBytes = 0;\n this.emit(\"message\", message);\n }\n // If !fin, wait for more continuation frames\n return;\n }\n\n // Binary or unknown — just ignore for a mock server\n }\n}\n\nexport function computeAcceptKey(wsKey: string): string {\n return createHash(\"sha1\")\n .update(wsKey + WS_GUID)\n .digest(\"base64\");\n}\n\nexport function upgradeToWebSocket(\n req: http.IncomingMessage,\n socket: net.Socket,\n limits?: WebSocketLimits,\n): WebSocketConnection {\n const key = req.headers[\"sec-websocket-key\"];\n if (!key) {\n socket.write(\"HTTP/1.1 400 Bad Request\\r\\n\\r\\n\");\n socket.destroy();\n throw new Error(\"Missing Sec-WebSocket-Key header\");\n }\n\n const acceptKey = computeAcceptKey(key);\n\n let responseHeaders =\n \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n \"Upgrade: websocket\\r\\n\" +\n \"Connection: Upgrade\\r\\n\" +\n `Sec-WebSocket-Accept: ${acceptKey}\\r\\n`;\n\n // Echo back requested subprotocol if present\n const protocol = req.headers[\"sec-websocket-protocol\"];\n if (protocol) {\n // Take the first offered protocol\n const first = protocol.split(\",\")[0].trim();\n responseHeaders += `Sec-WebSocket-Protocol: ${first}\\r\\n`;\n }\n\n responseHeaders += \"\\r\\n\";\n\n socket.write(responseHeaders);\n\n return new WebSocketConnection(socket, limits);\n}\n"],"mappings":"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"}
|
package/dist/ws-gemini-live.cjs
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
const require_constants = require('./constants.cjs');
|
|
2
|
+
const require_policy = require('./response/policy.cjs');
|
|
2
3
|
const require_helpers = require('./helpers.cjs');
|
|
3
4
|
const require_sse_writer = require('./sse-writer.cjs');
|
|
4
5
|
const require_misbehavior = require('./misbehavior.cjs');
|
|
5
6
|
const require_router = require('./router.cjs');
|
|
6
7
|
require('./journal.cjs');
|
|
7
|
-
const
|
|
8
|
+
const require_lifecycle = require('./response/lifecycle.cjs');
|
|
8
9
|
|
|
9
10
|
//#region src/ws-gemini-live.ts
|
|
11
|
+
/**
|
|
12
|
+
* WebSocket handler for Gemini Live BidiGenerateContent API.
|
|
13
|
+
*
|
|
14
|
+
* Accepts setup, clientContent, and toolResponse messages over WebSocket
|
|
15
|
+
* and responds with setupComplete, serverContent, toolCall, and error
|
|
16
|
+
* messages in the Gemini Live streaming format.
|
|
17
|
+
*/
|
|
10
18
|
const WS_PATH = "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
|
11
19
|
/**
|
|
12
20
|
* Map HTTP status codes to gRPC error codes.
|
|
@@ -193,6 +201,7 @@ function preflightBlocks(response, journalEntry) {
|
|
|
193
201
|
}
|
|
194
202
|
function handleWebSocketGeminiLive(ws, fixtures, journal, defaults) {
|
|
195
203
|
const { logger } = defaults;
|
|
204
|
+
require_lifecycle.watchWsClose(ws);
|
|
196
205
|
const session = {
|
|
197
206
|
setupDone: false,
|
|
198
207
|
model: defaults.model,
|
|
@@ -301,7 +310,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
301
310
|
const path = WS_PATH;
|
|
302
311
|
if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
303
312
|
if (!fixture) {
|
|
304
|
-
if (
|
|
313
|
+
if (require_policy.resolveRequestDecisions(defaults.strict, defaults.upgradeHeaders).fixtureMiss) {
|
|
305
314
|
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
306
315
|
defaults.logger.error(require_helpers.strictNoMatchLogLine("WS", path, skippedBySequenceOrTurn));
|
|
307
316
|
journal.add({
|
|
@@ -312,7 +321,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
312
321
|
response: {
|
|
313
322
|
status: 503,
|
|
314
323
|
fixture: null,
|
|
315
|
-
...
|
|
324
|
+
...require_policy.strictOverrideField(defaults.strict, defaults.upgradeHeaders)
|
|
316
325
|
}
|
|
317
326
|
});
|
|
318
327
|
ws.close(1008, strictMessage);
|
|
@@ -326,7 +335,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
326
335
|
response: {
|
|
327
336
|
status: 404,
|
|
328
337
|
fixture: null,
|
|
329
|
-
...
|
|
338
|
+
...require_policy.strictOverrideField(defaults.strict, defaults.upgradeHeaders)
|
|
330
339
|
}
|
|
331
340
|
});
|
|
332
341
|
ws.send(JSON.stringify({ error: {
|
|
@@ -389,40 +398,45 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
389
398
|
const latency = fixture.latency ?? defaults.latency;
|
|
390
399
|
if (misbehavior.kind === "applied" && (misbehavior.summary.fault === "tool-args-invalid-json" || misbehavior.summary.fault === "empty-response")) {
|
|
391
400
|
const journalEntry = addResponseEntry(200);
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
journalEntry
|
|
395
|
-
journalEntry.response.interruptReason = interruption?.reason();
|
|
401
|
+
const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
|
|
402
|
+
const interruptTurn = () => {
|
|
403
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
396
404
|
ws.destroy();
|
|
397
405
|
};
|
|
398
406
|
try {
|
|
399
|
-
if (ws.isClosed)
|
|
407
|
+
if (ws.isClosed) {
|
|
408
|
+
interruptTurn();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
400
411
|
const chunkDelay = require_sse_writer.calculateDelay(0, void 0, latency, fixture.recordedTimings, fixture.replaySpeed ?? defaults.replaySpeed);
|
|
401
|
-
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay,
|
|
402
|
-
if (
|
|
403
|
-
|
|
412
|
+
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
|
|
413
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
414
|
+
interruptTurn();
|
|
404
415
|
return;
|
|
405
416
|
}
|
|
406
|
-
if (ws.isClosed) return;
|
|
407
417
|
const malformed = misbehavior.summary.fault === "tool-args-invalid-json";
|
|
408
418
|
ws.send(JSON.stringify({ serverContent: malformed ? {
|
|
409
419
|
turnComplete: true,
|
|
410
420
|
turnCompleteReason: "MALFORMED_FUNCTION_CALL"
|
|
411
421
|
} : { modelTurn: { parts: [{ text: "" }] } } }));
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
|
|
422
|
+
lifecycle.tick();
|
|
423
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
424
|
+
interruptTurn();
|
|
415
425
|
return;
|
|
416
426
|
}
|
|
417
|
-
if (!malformed
|
|
427
|
+
if (!malformed) {
|
|
418
428
|
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
429
|
+
if (ws.isClosed) {
|
|
430
|
+
interruptTurn();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
419
433
|
session.conversationHistory.push({
|
|
420
434
|
role: "assistant",
|
|
421
435
|
content: ""
|
|
422
436
|
});
|
|
423
437
|
}
|
|
424
438
|
} finally {
|
|
425
|
-
|
|
439
|
+
lifecycle.dispose();
|
|
426
440
|
}
|
|
427
441
|
return;
|
|
428
442
|
}
|
|
@@ -459,25 +473,32 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
459
473
|
...tc,
|
|
460
474
|
resolvedId: tc.id ?? require_helpers.generateToolCallId()
|
|
461
475
|
}));
|
|
476
|
+
const stoppedByClose = () => {
|
|
477
|
+
if (!ws.isClosed) return false;
|
|
478
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws);
|
|
479
|
+
return true;
|
|
480
|
+
};
|
|
481
|
+
if (stoppedByClose()) return;
|
|
462
482
|
if (resolvedToolCalls.length === 0) ws.send(JSON.stringify({ serverContent: {
|
|
463
483
|
modelTurn: { parts },
|
|
464
484
|
turnComplete: true
|
|
465
485
|
} }));
|
|
466
486
|
else {
|
|
467
487
|
ws.send(JSON.stringify({ serverContent: { modelTurn: { parts } } }));
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
488
|
+
if (stoppedByClose()) return;
|
|
489
|
+
const functionCalls = resolvedToolCalls.map((tc) => {
|
|
490
|
+
const argsObj = liveToolArguments(tc);
|
|
491
|
+
return {
|
|
492
|
+
name: tc.name,
|
|
493
|
+
args: argsObj,
|
|
494
|
+
id: tc.resolvedId
|
|
495
|
+
};
|
|
496
|
+
});
|
|
497
|
+
ws.send(JSON.stringify({ toolCall: { functionCalls } }));
|
|
498
|
+
if (stoppedByClose()) return;
|
|
499
|
+
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
480
500
|
}
|
|
501
|
+
if (stoppedByClose()) return;
|
|
481
502
|
session.conversationHistory.push({
|
|
482
503
|
role: "assistant",
|
|
483
504
|
content: audioResp.content ?? "[audio]",
|
|
@@ -496,7 +517,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
496
517
|
const journalEntry = addResponseEntry(200);
|
|
497
518
|
if (response.blocks && response.blocks.length > 0) {
|
|
498
519
|
const resolvedBlocks = preflightBlocks(response, journalEntry);
|
|
499
|
-
const
|
|
520
|
+
const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
|
|
500
521
|
const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
|
|
501
522
|
const { recordedTimings } = fixture;
|
|
502
523
|
let chunkIndex = 0;
|
|
@@ -506,8 +527,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
506
527
|
outer: for (const block of resolvedBlocks) if (block.type === "toolCall") {
|
|
507
528
|
if (ws.isClosed) break;
|
|
508
529
|
const tcDelay = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
|
|
509
|
-
if (tcDelay > 0) await require_sse_writer.delay(tcDelay,
|
|
510
|
-
if (
|
|
530
|
+
if (tcDelay > 0) await require_sse_writer.delay(tcDelay, lifecycle.signal);
|
|
531
|
+
if (lifecycle.signal.aborted) {
|
|
511
532
|
interrupted = true;
|
|
512
533
|
break;
|
|
513
534
|
}
|
|
@@ -533,8 +554,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
533
554
|
arguments: block.arguments
|
|
534
555
|
}
|
|
535
556
|
});
|
|
536
|
-
|
|
537
|
-
if (
|
|
557
|
+
lifecycle.tick();
|
|
558
|
+
if (lifecycle.signal.aborted) {
|
|
538
559
|
interrupted = true;
|
|
539
560
|
break;
|
|
540
561
|
}
|
|
@@ -545,8 +566,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
545
566
|
for (let i = 0; i < text.length; i += chunkSize) {
|
|
546
567
|
if (ws.isClosed) break outer;
|
|
547
568
|
const chunkDelay = require_sse_writer.calculateDelay(chunkIndex, void 0, latency, recordedTimings, replaySpeed);
|
|
548
|
-
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay,
|
|
549
|
-
if (
|
|
569
|
+
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
|
|
570
|
+
if (lifecycle.signal.aborted) {
|
|
550
571
|
interrupted = true;
|
|
551
572
|
break outer;
|
|
552
573
|
}
|
|
@@ -558,22 +579,25 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
558
579
|
break outer;
|
|
559
580
|
}
|
|
560
581
|
chunkIndex++;
|
|
561
|
-
|
|
562
|
-
if (
|
|
582
|
+
lifecycle.tick();
|
|
583
|
+
if (lifecycle.signal.aborted) {
|
|
563
584
|
interrupted = true;
|
|
564
585
|
break outer;
|
|
565
586
|
}
|
|
566
587
|
}
|
|
567
588
|
}
|
|
568
|
-
if (interrupted) {
|
|
589
|
+
if (interrupted || ws.isClosed) {
|
|
569
590
|
ws.destroy();
|
|
570
|
-
journalEntry
|
|
571
|
-
|
|
572
|
-
|
|
591
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
592
|
+
lifecycle.dispose();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
lifecycle.dispose();
|
|
596
|
+
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
597
|
+
if (ws.isClosed) {
|
|
598
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
573
599
|
return;
|
|
574
600
|
}
|
|
575
|
-
interruption?.cleanup();
|
|
576
|
-
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
577
601
|
session.conversationHistory.push({
|
|
578
602
|
role: "assistant",
|
|
579
603
|
content: historyContent || null,
|
|
@@ -585,7 +609,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
585
609
|
const content = response.content ?? "";
|
|
586
610
|
const chunkList = [];
|
|
587
611
|
for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
|
|
588
|
-
const
|
|
612
|
+
const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
|
|
589
613
|
const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
|
|
590
614
|
const { recordedTimings } = fixture;
|
|
591
615
|
let interrupted = false;
|
|
@@ -594,8 +618,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
594
618
|
} else for (let i = 0; i < chunkList.length; i++) {
|
|
595
619
|
if (ws.isClosed) break;
|
|
596
620
|
const chunkDelay = require_sse_writer.calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
|
|
597
|
-
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay,
|
|
598
|
-
if (
|
|
621
|
+
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
|
|
622
|
+
if (lifecycle.signal.aborted) {
|
|
599
623
|
interrupted = true;
|
|
600
624
|
break;
|
|
601
625
|
}
|
|
@@ -606,17 +630,16 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
606
630
|
defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
|
|
607
631
|
break;
|
|
608
632
|
}
|
|
609
|
-
|
|
610
|
-
if (
|
|
633
|
+
lifecycle.tick();
|
|
634
|
+
if (lifecycle.signal.aborted) {
|
|
611
635
|
interrupted = true;
|
|
612
636
|
break;
|
|
613
637
|
}
|
|
614
638
|
}
|
|
615
|
-
if (interrupted) {
|
|
639
|
+
if (interrupted || ws.isClosed) {
|
|
616
640
|
ws.destroy();
|
|
617
|
-
journalEntry
|
|
618
|
-
|
|
619
|
-
interruption?.cleanup();
|
|
641
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
642
|
+
lifecycle.dispose();
|
|
620
643
|
return;
|
|
621
644
|
}
|
|
622
645
|
const resolvedToolCalls = functionToolCalls.map((tc) => ({
|
|
@@ -625,12 +648,11 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
625
648
|
}));
|
|
626
649
|
if (!ws.isClosed) {
|
|
627
650
|
const tcDelay = require_sse_writer.calculateDelay(chunkList.length, void 0, latency, recordedTimings, replaySpeed);
|
|
628
|
-
if (tcDelay > 0) await require_sse_writer.delay(tcDelay,
|
|
629
|
-
if (
|
|
651
|
+
if (tcDelay > 0) await require_sse_writer.delay(tcDelay, lifecycle.signal);
|
|
652
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
630
653
|
ws.destroy();
|
|
631
|
-
journalEntry
|
|
632
|
-
|
|
633
|
-
interruption?.cleanup();
|
|
654
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
655
|
+
lifecycle.dispose();
|
|
634
656
|
return;
|
|
635
657
|
}
|
|
636
658
|
const functionCalls = resolvedToolCalls.map((tc) => {
|
|
@@ -642,17 +664,20 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
642
664
|
};
|
|
643
665
|
});
|
|
644
666
|
ws.send(JSON.stringify({ toolCall: { functionCalls } }));
|
|
645
|
-
|
|
667
|
+
lifecycle.tick();
|
|
646
668
|
}
|
|
647
|
-
if (
|
|
669
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
648
670
|
ws.destroy();
|
|
649
|
-
journalEntry
|
|
650
|
-
|
|
651
|
-
|
|
671
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
672
|
+
lifecycle.dispose();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
lifecycle.dispose();
|
|
676
|
+
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
677
|
+
if (ws.isClosed) {
|
|
678
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
652
679
|
return;
|
|
653
680
|
}
|
|
654
|
-
interruption?.cleanup();
|
|
655
|
-
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
656
681
|
session.conversationHistory.push({
|
|
657
682
|
role: "assistant",
|
|
658
683
|
content: content || null,
|
|
@@ -671,22 +696,30 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
671
696
|
const journalEntry = addResponseEntry(200);
|
|
672
697
|
const content = response.content;
|
|
673
698
|
if (content.length === 0) {
|
|
674
|
-
if (ws.isClosed)
|
|
699
|
+
if (ws.isClosed) {
|
|
700
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
675
703
|
ws.send(JSON.stringify({ serverContent: { modelTurn: { parts: [{ text: "" }] } } }));
|
|
704
|
+
if (ws.isClosed) {
|
|
705
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
676
708
|
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
709
|
+
if (ws.isClosed) require_lifecycle.markWsTurnStopped(journalEntry, ws);
|
|
677
710
|
return;
|
|
678
711
|
}
|
|
679
712
|
const chunks = [];
|
|
680
713
|
for (let i = 0; i < content.length; i += chunkSize) chunks.push(content.slice(i, i + chunkSize));
|
|
681
|
-
const
|
|
714
|
+
const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
|
|
682
715
|
const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
|
|
683
716
|
const { recordedTimings } = fixture;
|
|
684
717
|
let interrupted = false;
|
|
685
718
|
for (let i = 0; i < chunks.length; i++) {
|
|
686
719
|
if (ws.isClosed) break;
|
|
687
720
|
const chunkDelay = require_sse_writer.calculateDelay(i, void 0, latency, recordedTimings, replaySpeed);
|
|
688
|
-
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay,
|
|
689
|
-
if (
|
|
721
|
+
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, lifecycle.signal);
|
|
722
|
+
if (lifecycle.signal.aborted) {
|
|
690
723
|
interrupted = true;
|
|
691
724
|
break;
|
|
692
725
|
}
|
|
@@ -697,21 +730,24 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
697
730
|
defaults.logger.debug("[gemini-live] send failed during text streaming, closing", err);
|
|
698
731
|
break;
|
|
699
732
|
}
|
|
700
|
-
|
|
701
|
-
if (
|
|
733
|
+
lifecycle.tick();
|
|
734
|
+
if (lifecycle.signal.aborted) {
|
|
702
735
|
interrupted = true;
|
|
703
736
|
break;
|
|
704
737
|
}
|
|
705
738
|
}
|
|
706
|
-
if (interrupted) {
|
|
739
|
+
if (interrupted || ws.isClosed) {
|
|
707
740
|
ws.destroy();
|
|
708
|
-
journalEntry
|
|
709
|
-
|
|
710
|
-
|
|
741
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
742
|
+
lifecycle.dispose();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
lifecycle.dispose();
|
|
746
|
+
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
747
|
+
if (ws.isClosed) {
|
|
748
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
711
749
|
return;
|
|
712
750
|
}
|
|
713
|
-
interruption?.cleanup();
|
|
714
|
-
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
715
751
|
session.conversationHistory.push({
|
|
716
752
|
role: "assistant",
|
|
717
753
|
content
|
|
@@ -721,24 +757,20 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
721
757
|
if (require_helpers.isToolCallResponse(response)) {
|
|
722
758
|
const journalEntry = addResponseEntry(200);
|
|
723
759
|
const functionToolCalls = preflightToolArguments(response, journalEntry);
|
|
724
|
-
const
|
|
760
|
+
const lifecycle = require_lifecycle.wsLifecycle(ws, fixture);
|
|
725
761
|
const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
|
|
726
762
|
const { recordedTimings } = fixture;
|
|
727
763
|
if (ws.isClosed) {
|
|
728
|
-
|
|
764
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
765
|
+
lifecycle.dispose();
|
|
729
766
|
return;
|
|
730
767
|
}
|
|
731
768
|
const tcDelay = require_sse_writer.calculateDelay(0, void 0, latency, recordedTimings, replaySpeed);
|
|
732
|
-
if (tcDelay > 0) await require_sse_writer.delay(tcDelay,
|
|
733
|
-
if (
|
|
769
|
+
if (tcDelay > 0) await require_sse_writer.delay(tcDelay, lifecycle.signal);
|
|
770
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
734
771
|
ws.destroy();
|
|
735
|
-
journalEntry
|
|
736
|
-
|
|
737
|
-
interruption?.cleanup();
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
if (ws.isClosed) {
|
|
741
|
-
interruption?.cleanup();
|
|
772
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
773
|
+
lifecycle.dispose();
|
|
742
774
|
return;
|
|
743
775
|
}
|
|
744
776
|
const resolvedToolCalls = functionToolCalls.map((tc) => ({
|
|
@@ -754,16 +786,19 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
754
786
|
};
|
|
755
787
|
});
|
|
756
788
|
ws.send(JSON.stringify({ toolCall: { functionCalls } }));
|
|
757
|
-
|
|
758
|
-
if (
|
|
789
|
+
lifecycle.tick();
|
|
790
|
+
if (lifecycle.signal.aborted || ws.isClosed) {
|
|
759
791
|
ws.destroy();
|
|
760
|
-
journalEntry
|
|
761
|
-
|
|
762
|
-
|
|
792
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
793
|
+
lifecycle.dispose();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
lifecycle.dispose();
|
|
797
|
+
ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
798
|
+
if (ws.isClosed) {
|
|
799
|
+
require_lifecycle.markWsTurnStopped(journalEntry, ws, lifecycle);
|
|
763
800
|
return;
|
|
764
801
|
}
|
|
765
|
-
interruption?.cleanup();
|
|
766
|
-
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
767
802
|
session.conversationHistory.push({
|
|
768
803
|
role: "assistant",
|
|
769
804
|
content: null,
|