@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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { renderError } from "./errors.js";
|
|
2
|
+
import { flattenHeaders } from "../helpers.js";
|
|
3
|
+
import { checkRawResponse } from "../raw-validation.js";
|
|
4
|
+
import { sseCodec, writeErrorResponse } from "../sse-writer.js";
|
|
5
|
+
import { recordMisbehaviorOutcome } from "../misbehavior.js";
|
|
6
|
+
import { rawFixtureContext } from "../fixture-loader.js";
|
|
7
|
+
import { deliverJson, deliverStream } from "./deliver.js";
|
|
8
|
+
import { encodeEventStreamMessage, eventStreamCodec } from "../aws-event-stream.js";
|
|
9
|
+
import { serializeOpenRouterError } from "../openrouter-chat.js";
|
|
10
|
+
import { ndjsonCodec } from "../ndjson-writer.js";
|
|
11
|
+
|
|
12
|
+
//#region src/response/raw.ts
|
|
13
|
+
/**
|
|
14
|
+
* The default headers of the typed stream on `wire`, read from the typed
|
|
15
|
+
* codec: SSE and NDJSON add `Cache-Control: no-cache` and
|
|
16
|
+
* `Connection: keep-alive`, the AWS event stream `Transfer-Encoding: chunked`.
|
|
17
|
+
*/
|
|
18
|
+
function rawStreamHeaders(wire) {
|
|
19
|
+
switch (wire) {
|
|
20
|
+
case "openai-responses":
|
|
21
|
+
case "openai-chat":
|
|
22
|
+
case "anthropic":
|
|
23
|
+
case "gemini":
|
|
24
|
+
case "gemini-interactions":
|
|
25
|
+
case "cohere": return sseCodec({ done: false }).headers;
|
|
26
|
+
case "ollama":
|
|
27
|
+
case "ollama-generate": return ndjsonCodec().headers;
|
|
28
|
+
case "bedrock-invoke":
|
|
29
|
+
case "bedrock-converse": return eventStreamCodec().headers;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The non-streaming JSON default, as `deliverJson` sends it. */
|
|
33
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
34
|
+
/**
|
|
35
|
+
* `defaults` with each `authored` header laid over it. Header names are
|
|
36
|
+
* case-insensitive: an authored header replaces the default of the same name
|
|
37
|
+
* in any letter case and takes the default's spelling, so a later spread over
|
|
38
|
+
* the same defaults (as `deliverJson` does) still has one key per name.
|
|
39
|
+
*/
|
|
40
|
+
function overrideHeaders(defaults, authored) {
|
|
41
|
+
const merged = { ...defaults };
|
|
42
|
+
const spelling = new Map(Object.keys(defaults).map((name) => [name.toLowerCase(), name]));
|
|
43
|
+
for (const [name, value] of Object.entries(authored ?? {})) merged[spelling.get(name.toLowerCase()) ?? name] = value;
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
/** One raw event, framed for `wire`. Events are checked before framing (raw-validation.ts). */
|
|
47
|
+
function encodeRawFrame(wire, event) {
|
|
48
|
+
const json = JSON.stringify(event);
|
|
49
|
+
switch (wire) {
|
|
50
|
+
case "openai-responses":
|
|
51
|
+
case "anthropic":
|
|
52
|
+
case "cohere": return `event: ${String(event.type)}\ndata: ${json}\n\n`;
|
|
53
|
+
case "openai-chat":
|
|
54
|
+
case "gemini":
|
|
55
|
+
case "gemini-interactions": return `data: ${json}\n\n`;
|
|
56
|
+
case "ollama":
|
|
57
|
+
case "ollama-generate": return `${json}\n`;
|
|
58
|
+
case "bedrock-invoke": return encodeEventStreamMessage("chunk", { bytes: Buffer.from(json, "utf8").toString("base64") });
|
|
59
|
+
case "bedrock-converse": {
|
|
60
|
+
const [eventType] = Object.keys(event);
|
|
61
|
+
const payload = event[eventType];
|
|
62
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error(`raw framing internal error: bedrock-converse event "${eventType}" has no object payload`);
|
|
63
|
+
return encodeEventStreamMessage(eventType, payload);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bytes written after the last frame, if any. Only `openai-chat` has a
|
|
69
|
+
* terminal (`data: [DONE]`), sent unless `raw.done === false` and never after
|
|
70
|
+
* an interrupted stream.
|
|
71
|
+
*/
|
|
72
|
+
function rawTrailer(wire, raw, aborted) {
|
|
73
|
+
if (wire === "openai-chat" && !aborted && raw.done !== false) return "data: [DONE]\n\n";
|
|
74
|
+
}
|
|
75
|
+
/** The error code for a request the raw fixture has no body for. */
|
|
76
|
+
const RAW_MODE_UNAVAILABLE = "aimock_raw_mode_unavailable";
|
|
77
|
+
/** The error code for a raw fixture that fails its checks or targets another wire. */
|
|
78
|
+
const RAW_FIXTURE_INVALID = "aimock_raw_fixture_invalid";
|
|
79
|
+
/**
|
|
80
|
+
* The raw events as a frame codec for the shared frame loop. Its headers are
|
|
81
|
+
* the typed stream's defaults for `wire` with `raw.headers` laid over them.
|
|
82
|
+
*/
|
|
83
|
+
function rawCodec(wire, raw) {
|
|
84
|
+
return {
|
|
85
|
+
headers: overrideHeaders(rawStreamHeaders(wire), raw.headers),
|
|
86
|
+
encode: (event) => encodeRawFrame(wire, event),
|
|
87
|
+
trailer: (aborted) => {
|
|
88
|
+
const trailer = rawTrailer(wire, raw, aborted);
|
|
89
|
+
return trailer === void 0 ? [] : [trailer];
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The error body for a raw 500, in the dialect of the handler's misbehavior
|
|
95
|
+
* errors. Dialects with no code slot (Gemini, Ollama, OpenRouter) carry the
|
|
96
|
+
* code as a message prefix.
|
|
97
|
+
*/
|
|
98
|
+
function renderRawError(ctx, code, message) {
|
|
99
|
+
if (ctx.openRouter) return { body: serializeOpenRouterError(500, `${code}: ${message}`) };
|
|
100
|
+
switch (ctx.errorWire) {
|
|
101
|
+
case "openai": return renderError("openai", "misbehavior", {
|
|
102
|
+
message,
|
|
103
|
+
type: "server_error",
|
|
104
|
+
...ctx.wire === "openai-chat" ? {
|
|
105
|
+
param: null,
|
|
106
|
+
includeParam: true
|
|
107
|
+
} : {},
|
|
108
|
+
code
|
|
109
|
+
});
|
|
110
|
+
case "messages": return renderError("messages", "misbehavior", {
|
|
111
|
+
type: "api_error",
|
|
112
|
+
code,
|
|
113
|
+
message
|
|
114
|
+
});
|
|
115
|
+
case "gemini": return renderError("gemini", "misbehavior", {
|
|
116
|
+
bodyCode: 500,
|
|
117
|
+
message: `${code}: ${message}`,
|
|
118
|
+
status: "INTERNAL"
|
|
119
|
+
});
|
|
120
|
+
case "bedrock":
|
|
121
|
+
case "bedrock-converse": return renderError(ctx.errorWire, "misbehavior", {
|
|
122
|
+
type: "InternalServerException",
|
|
123
|
+
code,
|
|
124
|
+
message
|
|
125
|
+
});
|
|
126
|
+
default: return renderError(ctx.errorWire, "misbehavior", {
|
|
127
|
+
code,
|
|
128
|
+
message
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serve a raw passthrough fixture on the handler's wire: check it, journal it,
|
|
134
|
+
* and send its events or JSON through the shared delivery layer. A fixture
|
|
135
|
+
* that fails a check, targets another wire, or has no body for the requested
|
|
136
|
+
* mode gets a 500 in the handler's misbehavior error dialect, and an error log
|
|
137
|
+
* line. A reply with status 400 or more is never delayed, streaming or not,
|
|
138
|
+
* and one with `json` answers in JSON even to a streaming request.
|
|
139
|
+
*/
|
|
140
|
+
async function serveRawResponse(ctx) {
|
|
141
|
+
const { wire, stream, req, res, fixture, journal, defaults, testId } = ctx;
|
|
142
|
+
const raw = ctx.response.raw;
|
|
143
|
+
const journalBase = {
|
|
144
|
+
method: req.method ?? "POST",
|
|
145
|
+
path: req.url ?? "/",
|
|
146
|
+
headers: flattenHeaders(req.headers),
|
|
147
|
+
body: ctx.body
|
|
148
|
+
};
|
|
149
|
+
const record = (entry) => recordMisbehaviorOutcome({
|
|
150
|
+
entry,
|
|
151
|
+
summary: ctx.misbehaviorSummary,
|
|
152
|
+
defaults,
|
|
153
|
+
testId
|
|
154
|
+
});
|
|
155
|
+
const status = raw.status ?? 200;
|
|
156
|
+
const errorReply = status >= 400;
|
|
157
|
+
const servesStream = stream && !(errorReply && raw.json !== void 0);
|
|
158
|
+
let failure;
|
|
159
|
+
const problems = checkRawResponse(raw, rawFixtureContext(fixture, ctx.response)).filter((finding) => finding.severity === "error");
|
|
160
|
+
if (problems.length > 0) failure = {
|
|
161
|
+
code: RAW_FIXTURE_INVALID,
|
|
162
|
+
message: `invalid raw fixture: ${problems.map((p) => p.message).join("; ")}`
|
|
163
|
+
};
|
|
164
|
+
else if (raw.wire !== wire) failure = {
|
|
165
|
+
code: RAW_FIXTURE_INVALID,
|
|
166
|
+
message: `raw fixture is for ${raw.wire}, not ${wire}`
|
|
167
|
+
};
|
|
168
|
+
else if (servesStream ? raw.events === void 0 : raw.json === void 0) failure = {
|
|
169
|
+
code: RAW_MODE_UNAVAILABLE,
|
|
170
|
+
message: `raw fixture has no "${servesStream ? "events" : "json"}" body for a ${servesStream ? "streaming" : "non-streaming"} request on ${wire}`
|
|
171
|
+
};
|
|
172
|
+
if (failure) {
|
|
173
|
+
defaults.logger.error(`${journalBase.method} ${journalBase.path}: ${failure.message}`);
|
|
174
|
+
const entry = journal.add({
|
|
175
|
+
...journalBase,
|
|
176
|
+
response: {
|
|
177
|
+
status: 500,
|
|
178
|
+
fixture
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
entry.response.error = failure.message;
|
|
182
|
+
record(entry);
|
|
183
|
+
const rendered = renderRawError(ctx, failure.code, failure.message);
|
|
184
|
+
for (const [name, value] of Object.entries(rendered.headers ?? {})) res.setHeader(name, value);
|
|
185
|
+
writeErrorResponse(res, 500, rendered.body);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const entry = journal.add({
|
|
189
|
+
...journalBase,
|
|
190
|
+
response: {
|
|
191
|
+
status,
|
|
192
|
+
fixture
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
record(entry);
|
|
196
|
+
if (servesStream) {
|
|
197
|
+
const events = raw.events;
|
|
198
|
+
if (events === void 0) throw new Error(`raw serve internal error: a streamed reply on ${wire} has no events`);
|
|
199
|
+
await deliverStream(res, entry, fixture, rawCodec(wire, raw), events, errorReply ? { latency: 0 } : {
|
|
200
|
+
latency: fixture.latency ?? defaults.latency,
|
|
201
|
+
streamingProfile: fixture.streamingProfile,
|
|
202
|
+
recordedTimings: fixture.recordedTimings,
|
|
203
|
+
replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed
|
|
204
|
+
}, { status: raw.status });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const json = raw.json;
|
|
208
|
+
if (json === void 0) throw new Error(`raw serve internal error: a JSON reply on ${wire} has no json`);
|
|
209
|
+
const servedLength = JSON.stringify(json).length;
|
|
210
|
+
await deliverJson(res, entry, errorReply ? {
|
|
211
|
+
...fixture,
|
|
212
|
+
nonStreamingTiming: false,
|
|
213
|
+
nonStreamingTimingAllProviders: false
|
|
214
|
+
} : fixture, () => json, {
|
|
215
|
+
surface: wire === "openai-chat" ? "openai-chat" : "other",
|
|
216
|
+
status: raw.status,
|
|
217
|
+
payloadLength: servedLength,
|
|
218
|
+
chunkSize: Math.max(1, fixture.chunkSize ?? defaults.chunkSize),
|
|
219
|
+
recordedTimings: fixture.recordedTimings,
|
|
220
|
+
replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,
|
|
221
|
+
headers: overrideHeaders(JSON_HEADERS, raw.headers)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
export { RAW_MODE_UNAVAILABLE, serveRawResponse };
|
|
227
|
+
//# sourceMappingURL=raw.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raw.js","names":[],"sources":["../../src/response/raw.ts"],"sourcesContent":["/**\n * @internal Raw passthrough fixtures (#549): per-wire framing and serving.\n *\n * A raw fixture supplies event objects; aimock adds only the named wire's\n * framing, using exactly the bytes the typed handlers write for that wire:\n *\n * - named SSE (`openai-responses`, `anthropic`, `cohere`):\n * `event: <type>\\ndata: <json>\\n\\n`\n * - data SSE (`openai-chat`, `gemini`, `gemini-interactions`): `data: <json>\\n\\n`;\n * `openai-chat` ends with `data: [DONE]\\n\\n` unless `raw.done === false`\n * - NDJSON (`ollama`, `ollama-generate`): `<json>\\n`\n * - AWS event stream: `bedrock-invoke` wraps each event as a `chunk` frame with\n * base64 `bytes`; `bedrock-converse` takes the event's single key as the\n * frame's event type and its value as the payload.\n *\n * The framing functions are pure. `serveRawResponse` checks and journals the\n * fixture. It writes one reply itself: the untimed 500 for a fixture that\n * fails a check, targets another wire or has no body for the request mode.\n * Every other reply goes through the shared delivery layer (`deliverStream`,\n * `deliverJson`), which owns the delay loop, writability checks and abort\n * handling. A reply with a status of 400 or more is never delayed.\n */\nimport type * as http from \"node:http\";\nimport { encodeEventStreamMessage, eventStreamCodec } from \"../aws-event-stream.js\";\nimport { rawFixtureContext } from \"../fixture-loader.js\";\nimport { flattenHeaders } from \"../helpers.js\";\nimport type { Journal } from \"../journal.js\";\nimport { recordMisbehaviorOutcome, type MisbehaviorSummary } from \"../misbehavior.js\";\nimport { serializeOpenRouterError } from \"../openrouter-chat.js\";\nimport { ndjsonCodec } from \"../ndjson-writer.js\";\nimport { checkRawResponse } from \"../raw-validation.js\";\nimport { sseCodec, writeErrorResponse } from \"../sse-writer.js\";\nimport type { Fixture, HandlerDefaults, JournalEntry, RawResponse, RawWireId } from \"../types.js\";\nimport { deliverJson, deliverStream } from \"./deliver.js\";\nimport { renderError } from \"./errors.js\";\nimport type { FrameCodec } from \"./frame-loop.js\";\nimport type { ErrorWire } from \"./profiles.js\";\n\n/** The streaming Content-Type each wire's typed handler sends. */\nexport function rawContentType(wire: RawWireId): string {\n switch (wire) {\n case \"openai-responses\":\n case \"openai-chat\":\n case \"anthropic\":\n case \"gemini\":\n case \"gemini-interactions\":\n case \"cohere\":\n return \"text/event-stream\";\n case \"ollama\":\n case \"ollama-generate\":\n return \"application/x-ndjson\";\n case \"bedrock-invoke\":\n case \"bedrock-converse\":\n return \"application/vnd.amazon.eventstream\";\n }\n}\n\n/**\n * The default headers of the typed stream on `wire`, read from the typed\n * codec: SSE and NDJSON add `Cache-Control: no-cache` and\n * `Connection: keep-alive`, the AWS event stream `Transfer-Encoding: chunked`.\n */\nfunction rawStreamHeaders(wire: RawWireId): Record<string, string> {\n switch (wire) {\n case \"openai-responses\":\n case \"openai-chat\":\n case \"anthropic\":\n case \"gemini\":\n case \"gemini-interactions\":\n case \"cohere\":\n return sseCodec({ done: false }).headers;\n case \"ollama\":\n case \"ollama-generate\":\n return ndjsonCodec().headers;\n case \"bedrock-invoke\":\n case \"bedrock-converse\":\n return eventStreamCodec().headers;\n }\n}\n\n/** The non-streaming JSON default, as `deliverJson` sends it. */\nconst JSON_HEADERS: Readonly<Record<string, string>> = { \"Content-Type\": \"application/json\" };\n\n/**\n * `defaults` with each `authored` header laid over it. Header names are\n * case-insensitive: an authored header replaces the default of the same name\n * in any letter case and takes the default's spelling, so a later spread over\n * the same defaults (as `deliverJson` does) still has one key per name.\n */\nfunction overrideHeaders(\n defaults: Readonly<Record<string, string>>,\n authored: Readonly<Record<string, string>> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = { ...defaults };\n const spelling = new Map(Object.keys(defaults).map((name) => [name.toLowerCase(), name]));\n for (const [name, value] of Object.entries(authored ?? {})) {\n merged[spelling.get(name.toLowerCase()) ?? name] = value;\n }\n return merged;\n}\n\n/** One raw event, framed for `wire`. Events are checked before framing (raw-validation.ts). */\nexport function encodeRawFrame(wire: RawWireId, event: Record<string, unknown>): string | Buffer {\n const json = JSON.stringify(event);\n switch (wire) {\n case \"openai-responses\":\n case \"anthropic\":\n case \"cohere\":\n return `event: ${String(event.type)}\\ndata: ${json}\\n\\n`;\n case \"openai-chat\":\n case \"gemini\":\n case \"gemini-interactions\":\n return `data: ${json}\\n\\n`;\n case \"ollama\":\n case \"ollama-generate\":\n return `${json}\\n`;\n case \"bedrock-invoke\":\n return encodeEventStreamMessage(\"chunk\", {\n bytes: Buffer.from(json, \"utf8\").toString(\"base64\"),\n });\n case \"bedrock-converse\": {\n // The checker guarantees exactly one key whose value is a JSON object.\n const [eventType] = Object.keys(event);\n const payload = event[eventType];\n if (payload === null || typeof payload !== \"object\" || Array.isArray(payload)) {\n throw new Error(\n `raw framing internal error: bedrock-converse event \"${eventType}\" has no object payload`,\n );\n }\n return encodeEventStreamMessage(eventType, payload);\n }\n }\n}\n\n/**\n * Bytes written after the last frame, if any. Only `openai-chat` has a\n * terminal (`data: [DONE]`), sent unless `raw.done === false` and never after\n * an interrupted stream.\n */\nexport function rawTrailer(\n wire: RawWireId,\n raw: RawResponse[\"raw\"],\n aborted: boolean,\n): string | undefined {\n if (wire === \"openai-chat\" && !aborted && raw.done !== false) return \"data: [DONE]\\n\\n\";\n return undefined;\n}\n\n// ─── Serving ──────────────────────────────────────────────────────────────────\n\n/** The error code for a request the raw fixture has no body for. */\nexport const RAW_MODE_UNAVAILABLE = \"aimock_raw_mode_unavailable\";\n/** The error code for a raw fixture that fails its checks or targets another wire. */\nexport const RAW_FIXTURE_INVALID = \"aimock_raw_fixture_invalid\";\n\n/** Everything {@link serveRawResponse} needs from the calling handler. */\nexport interface RawServeContext {\n wire: RawWireId;\n /** Whether the request asked for a stream, as the handler decides it. */\n stream: boolean;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n fixture: Fixture;\n response: RawResponse;\n /** The journaled request body, as the handler journals it. */\n body: JournalEntry[\"body\"];\n journal: Journal;\n defaults: HandlerDefaults;\n testId: string;\n misbehaviorSummary?: MisbehaviorSummary;\n /** The dialect of the handler's own `renderError(…, \"misbehavior\", …)` call. */\n errorWire: ErrorWire;\n /** OpenAI chat requests that arrived on the OpenRouter route. */\n openRouter?: boolean;\n}\n\n/**\n * The raw events as a frame codec for the shared frame loop. Its headers are\n * the typed stream's defaults for `wire` with `raw.headers` laid over them.\n */\nexport function rawCodec(\n wire: RawWireId,\n raw: RawResponse[\"raw\"],\n): FrameCodec<Record<string, unknown>> {\n return {\n headers: overrideHeaders(rawStreamHeaders(wire), raw.headers),\n encode: (event) => encodeRawFrame(wire, event),\n trailer: (aborted) => {\n const trailer = rawTrailer(wire, raw, aborted);\n return trailer === undefined ? [] : [trailer];\n },\n };\n}\n\n/**\n * The error body for a raw 500, in the dialect of the handler's misbehavior\n * errors. Dialects with no code slot (Gemini, Ollama, OpenRouter) carry the\n * code as a message prefix.\n */\nfunction renderRawError(\n ctx: RawServeContext,\n code: string,\n message: string,\n): { body: string; headers?: Record<string, string> } {\n if (ctx.openRouter) return { body: serializeOpenRouterError(500, `${code}: ${message}`) };\n switch (ctx.errorWire) {\n case \"openai\":\n return renderError(\"openai\", \"misbehavior\", {\n message,\n type: \"server_error\",\n ...(ctx.wire === \"openai-chat\" ? { param: null, includeParam: true as const } : {}),\n code,\n });\n case \"messages\":\n return renderError(\"messages\", \"misbehavior\", { type: \"api_error\", code, message });\n case \"gemini\":\n return renderError(\"gemini\", \"misbehavior\", {\n bodyCode: 500,\n message: `${code}: ${message}`,\n status: \"INTERNAL\",\n });\n case \"bedrock\":\n case \"bedrock-converse\":\n return renderError(ctx.errorWire, \"misbehavior\", {\n type: \"InternalServerException\",\n code,\n message,\n });\n default:\n return renderError(ctx.errorWire, \"misbehavior\", { code, message });\n }\n}\n\n/**\n * Serve a raw passthrough fixture on the handler's wire: check it, journal it,\n * and send its events or JSON through the shared delivery layer. A fixture\n * that fails a check, targets another wire, or has no body for the requested\n * mode gets a 500 in the handler's misbehavior error dialect, and an error log\n * line. A reply with status 400 or more is never delayed, streaming or not,\n * and one with `json` answers in JSON even to a streaming request.\n */\nexport async function serveRawResponse(ctx: RawServeContext): Promise<void> {\n const { wire, stream, req, res, fixture, journal, defaults, testId } = ctx;\n const raw = ctx.response.raw;\n const journalBase = {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: ctx.body,\n };\n const record = (entry: JournalEntry): void =>\n recordMisbehaviorOutcome({ entry, summary: ctx.misbehaviorSummary, defaults, testId });\n\n const status = raw.status ?? 200;\n // An error reply (status >= 400) is never delayed, streaming or not, as\n // typed error replies are not. Fixture interruptions still apply.\n const errorReply = status >= 400;\n // An error status with a JSON body answers in JSON even to a streaming\n // request, as provider APIs do; the JSON wins over any events.\n const servesStream = stream && !(errorReply && raw.json !== undefined);\n\n let failure: { code: string; message: string } | undefined;\n const problems = checkRawResponse(raw, rawFixtureContext(fixture, ctx.response)).filter(\n (finding) => finding.severity === \"error\",\n );\n if (problems.length > 0) {\n failure = {\n code: RAW_FIXTURE_INVALID,\n message: `invalid raw fixture: ${problems.map((p) => p.message).join(\"; \")}`,\n };\n } else if (raw.wire !== wire) {\n failure = { code: RAW_FIXTURE_INVALID, message: `raw fixture is for ${raw.wire}, not ${wire}` };\n } else if (servesStream ? raw.events === undefined : raw.json === undefined) {\n failure = {\n code: RAW_MODE_UNAVAILABLE,\n message: `raw fixture has no \"${servesStream ? \"events\" : \"json\"}\" body for a ${\n servesStream ? \"streaming\" : \"non-streaming\"\n } request on ${wire}`,\n };\n }\n if (failure) {\n defaults.logger.error(`${journalBase.method} ${journalBase.path}: ${failure.message}`);\n const entry = journal.add({ ...journalBase, response: { status: 500, fixture } });\n entry.response.error = failure.message;\n record(entry);\n const rendered = renderRawError(ctx, failure.code, failure.message);\n for (const [name, value] of Object.entries(rendered.headers ?? {})) res.setHeader(name, value);\n writeErrorResponse(res, 500, rendered.body);\n return;\n }\n\n const entry = journal.add({ ...journalBase, response: { status, fixture } });\n record(entry);\n\n if (servesStream) {\n const events = raw.events;\n if (events === undefined) {\n throw new Error(`raw serve internal error: a streamed reply on ${wire} has no events`);\n }\n await deliverStream(\n res,\n entry,\n fixture,\n rawCodec(wire, raw),\n events,\n errorReply\n ? { latency: 0 }\n : {\n latency: fixture.latency ?? defaults.latency,\n streamingProfile: fixture.streamingProfile,\n recordedTimings: fixture.recordedTimings,\n replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,\n },\n // The codec already carries raw.headers over its defaults.\n { status: raw.status },\n );\n return;\n }\n\n // The wait is sized from the served JSON, as typed replies size theirs from\n // the served content, in chunks of the fixture's or server's chunk size.\n const json = raw.json;\n if (json === undefined) {\n throw new Error(`raw serve internal error: a JSON reply on ${wire} has no json`);\n }\n const servedLength = JSON.stringify(json).length;\n // deliverJson reads the timing opt-ins from the fixture. With them off, an\n // error reply still gets its writability check and journaling, but no wait.\n await deliverJson(\n res,\n entry,\n errorReply\n ? { ...fixture, nonStreamingTiming: false, nonStreamingTimingAllProviders: false }\n : fixture,\n () => json,\n {\n surface: wire === \"openai-chat\" ? \"openai-chat\" : \"other\",\n status: raw.status,\n payloadLength: servedLength,\n chunkSize: Math.max(1, fixture.chunkSize ?? defaults.chunkSize),\n recordedTimings: fixture.recordedTimings,\n replaySpeed: fixture.replaySpeed ?? defaults.replaySpeed,\n headers: overrideHeaders(JSON_HEADERS, raw.headers),\n },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8DA,SAAS,iBAAiB,MAAyC;AACjE,SAAQ,MAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC;EACnC,KAAK;EACL,KAAK,kBACH,QAAO,aAAa,CAAC;EACvB,KAAK;EACL,KAAK,mBACH,QAAO,kBAAkB,CAAC;;;;AAKhC,MAAM,eAAiD,EAAE,gBAAgB,oBAAoB;;;;;;;AAQ7F,SAAS,gBACP,UACA,UACwB;CACxB,MAAM,SAAiC,EAAE,GAAG,UAAU;CACtD,MAAM,WAAW,IAAI,IAAI,OAAO,KAAK,SAAS,CAAC,KAAK,SAAS,CAAC,KAAK,aAAa,EAAE,KAAK,CAAC,CAAC;AACzF,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,YAAY,EAAE,CAAC,CACxD,QAAO,SAAS,IAAI,KAAK,aAAa,CAAC,IAAI,QAAQ;AAErD,QAAO;;;AAIT,SAAgB,eAAe,MAAiB,OAAiD;CAC/F,MAAM,OAAO,KAAK,UAAU,MAAM;AAClC,SAAQ,MAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK,SACH,QAAO,UAAU,OAAO,MAAM,KAAK,CAAC,UAAU,KAAK;EACrD,KAAK;EACL,KAAK;EACL,KAAK,sBACH,QAAO,SAAS,KAAK;EACvB,KAAK;EACL,KAAK,kBACH,QAAO,GAAG,KAAK;EACjB,KAAK,iBACH,QAAO,yBAAyB,SAAS,EACvC,OAAO,OAAO,KAAK,MAAM,OAAO,CAAC,SAAS,SAAS,EACpD,CAAC;EACJ,KAAK,oBAAoB;GAEvB,MAAM,CAAC,aAAa,OAAO,KAAK,MAAM;GACtC,MAAM,UAAU,MAAM;AACtB,OAAI,YAAY,QAAQ,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,CAC3E,OAAM,IAAI,MACR,uDAAuD,UAAU,yBAClE;AAEH,UAAO,yBAAyB,WAAW,QAAQ;;;;;;;;;AAUzD,SAAgB,WACd,MACA,KACA,SACoB;AACpB,KAAI,SAAS,iBAAiB,CAAC,WAAW,IAAI,SAAS,MAAO,QAAO;;;AAOvE,MAAa,uBAAuB;;AAEpC,MAAa,sBAAsB;;;;;AA2BnC,SAAgB,SACd,MACA,KACqC;AACrC,QAAO;EACL,SAAS,gBAAgB,iBAAiB,KAAK,EAAE,IAAI,QAAQ;EAC7D,SAAS,UAAU,eAAe,MAAM,MAAM;EAC9C,UAAU,YAAY;GACpB,MAAM,UAAU,WAAW,MAAM,KAAK,QAAQ;AAC9C,UAAO,YAAY,SAAY,EAAE,GAAG,CAAC,QAAQ;;EAEhD;;;;;;;AAQH,SAAS,eACP,KACA,MACA,SACoD;AACpD,KAAI,IAAI,WAAY,QAAO,EAAE,MAAM,yBAAyB,KAAK,GAAG,KAAK,IAAI,UAAU,EAAE;AACzF,SAAQ,IAAI,WAAZ;EACE,KAAK,SACH,QAAO,YAAY,UAAU,eAAe;GAC1C;GACA,MAAM;GACN,GAAI,IAAI,SAAS,gBAAgB;IAAE,OAAO;IAAM,cAAc;IAAe,GAAG,EAAE;GAClF;GACD,CAAC;EACJ,KAAK,WACH,QAAO,YAAY,YAAY,eAAe;GAAE,MAAM;GAAa;GAAM;GAAS,CAAC;EACrF,KAAK,SACH,QAAO,YAAY,UAAU,eAAe;GAC1C,UAAU;GACV,SAAS,GAAG,KAAK,IAAI;GACrB,QAAQ;GACT,CAAC;EACJ,KAAK;EACL,KAAK,mBACH,QAAO,YAAY,IAAI,WAAW,eAAe;GAC/C,MAAM;GACN;GACA;GACD,CAAC;EACJ,QACE,QAAO,YAAY,IAAI,WAAW,eAAe;GAAE;GAAM;GAAS,CAAC;;;;;;;;;;;AAYzE,eAAsB,iBAAiB,KAAqC;CAC1E,MAAM,EAAE,MAAM,QAAQ,KAAK,KAAK,SAAS,SAAS,UAAU,WAAW;CACvE,MAAM,MAAM,IAAI,SAAS;CACzB,MAAM,cAAc;EAClB,QAAQ,IAAI,UAAU;EACtB,MAAM,IAAI,OAAO;EACjB,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM,IAAI;EACX;CACD,MAAM,UAAU,UACd,yBAAyB;EAAE;EAAO,SAAS,IAAI;EAAoB;EAAU;EAAQ,CAAC;CAExF,MAAM,SAAS,IAAI,UAAU;CAG7B,MAAM,aAAa,UAAU;CAG7B,MAAM,eAAe,UAAU,EAAE,cAAc,IAAI,SAAS;CAE5D,IAAI;CACJ,MAAM,WAAW,iBAAiB,KAAK,kBAAkB,SAAS,IAAI,SAAS,CAAC,CAAC,QAC9E,YAAY,QAAQ,aAAa,QACnC;AACD,KAAI,SAAS,SAAS,EACpB,WAAU;EACR,MAAM;EACN,SAAS,wBAAwB,SAAS,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK;EAC3E;UACQ,IAAI,SAAS,KACtB,WAAU;EAAE,MAAM;EAAqB,SAAS,sBAAsB,IAAI,KAAK,QAAQ;EAAQ;UACtF,eAAe,IAAI,WAAW,SAAY,IAAI,SAAS,OAChE,WAAU;EACR,MAAM;EACN,SAAS,uBAAuB,eAAe,WAAW,OAAO,eAC/D,eAAe,cAAc,gBAC9B,cAAc;EAChB;AAEH,KAAI,SAAS;AACX,WAAS,OAAO,MAAM,GAAG,YAAY,OAAO,GAAG,YAAY,KAAK,IAAI,QAAQ,UAAU;EACtF,MAAM,QAAQ,QAAQ,IAAI;GAAE,GAAG;GAAa,UAAU;IAAE,QAAQ;IAAK;IAAS;GAAE,CAAC;AACjF,QAAM,SAAS,QAAQ,QAAQ;AAC/B,SAAO,MAAM;EACb,MAAM,WAAW,eAAe,KAAK,QAAQ,MAAM,QAAQ,QAAQ;AACnE,OAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,SAAS,WAAW,EAAE,CAAC,CAAE,KAAI,UAAU,MAAM,MAAM;AAC9F,qBAAmB,KAAK,KAAK,SAAS,KAAK;AAC3C;;CAGF,MAAM,QAAQ,QAAQ,IAAI;EAAE,GAAG;EAAa,UAAU;GAAE;GAAQ;GAAS;EAAE,CAAC;AAC5E,QAAO,MAAM;AAEb,KAAI,cAAc;EAChB,MAAM,SAAS,IAAI;AACnB,MAAI,WAAW,OACb,OAAM,IAAI,MAAM,iDAAiD,KAAK,gBAAgB;AAExF,QAAM,cACJ,KACA,OACA,SACA,SAAS,MAAM,IAAI,EACnB,QACA,aACI,EAAE,SAAS,GAAG,GACd;GACE,SAAS,QAAQ,WAAW,SAAS;GACrC,kBAAkB,QAAQ;GAC1B,iBAAiB,QAAQ;GACzB,aAAa,QAAQ,eAAe,SAAS;GAC9C,EAEL,EAAE,QAAQ,IAAI,QAAQ,CACvB;AACD;;CAKF,MAAM,OAAO,IAAI;AACjB,KAAI,SAAS,OACX,OAAM,IAAI,MAAM,6CAA6C,KAAK,cAAc;CAElF,MAAM,eAAe,KAAK,UAAU,KAAK,CAAC;AAG1C,OAAM,YACJ,KACA,OACA,aACI;EAAE,GAAG;EAAS,oBAAoB;EAAO,gCAAgC;EAAO,GAChF,eACE,MACN;EACE,SAAS,SAAS,gBAAgB,gBAAgB;EAClD,QAAQ,IAAI;EACZ,eAAe;EACf,WAAW,KAAK,IAAI,GAAG,QAAQ,aAAa,SAAS,UAAU;EAC/D,iBAAiB,QAAQ;EACzB,aAAa,QAAQ,eAAe,SAAS;EAC7C,SAAS,gBAAgB,cAAc,IAAI,QAAQ;EACpD,CACF"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const require_helpers = require('../helpers.cjs');
|
|
2
|
+
const require_sse_writer = require('../sse-writer.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/response/timing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a non-streaming (`stream: false`) reply on `surface` waits. It is
|
|
7
|
+
* opt-in per fixture: `nonStreamingTimingAllProviders: true` on every
|
|
8
|
+
* surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat
|
|
9
|
+
* Completions only. Either one also needs an explicit `streamingProfile` or
|
|
10
|
+
* `recordedTimings`. Only the boolean `true` counts.
|
|
11
|
+
*/
|
|
12
|
+
function nonStreamingTimingEnabled(fixture, surface) {
|
|
13
|
+
return (fixture.nonStreamingTimingAllProviders === true || fixture.nonStreamingTiming === true && surface === "openai-chat") && !!(fixture.streamingProfile || fixture.recordedTimings);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The recorded timings a non-streaming wait on `surface` is sized from. `t` is
|
|
17
|
+
* returned as given when it is `undefined`, when its `interChunkDelaysMs` is
|
|
18
|
+
* an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.
|
|
19
|
+
* Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is
|
|
20
|
+
* returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps
|
|
21
|
+
* and the reply is still served.
|
|
22
|
+
*
|
|
23
|
+
* The `nonStreamingTiming: true` OpenAI Chat Completions case is passed
|
|
24
|
+
* through unchanged so that the shipped option keeps its behavior: with no
|
|
25
|
+
* `streamingProfile`, sizing the wait for a reply of more than one chunk reads
|
|
26
|
+
* the missing `interChunkDelaysMs` and throws. With `deliverJson`'s
|
|
27
|
+
* `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any
|
|
28
|
+
* case, because {@link extendRecordedTimings} reads the list first.
|
|
29
|
+
*/
|
|
30
|
+
function nonStreamingRecordedTimings(fixture, surface, t) {
|
|
31
|
+
if (!t || Array.isArray(t.interChunkDelaysMs)) return t;
|
|
32
|
+
if (fixture.nonStreamingTiming === true && surface === "openai-chat") return t;
|
|
33
|
+
return {
|
|
34
|
+
...t,
|
|
35
|
+
interChunkDelaysMs: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The total wait before a non-streaming reply: `calculateTotalDelay` over the
|
|
40
|
+
* content chunks `payloadLength` makes at `chunkSize`. There is no latency
|
|
41
|
+
* input, so bare `latency` never delays a non-streaming reply.
|
|
42
|
+
*/
|
|
43
|
+
function nonStreamingDelayMs(fixture, payloadLength, chunkSize, recordedTimings, replaySpeed) {
|
|
44
|
+
return require_sse_writer.calculateTotalDelay(payloadLength, chunkSize, fixture.streamingProfile, recordedTimings, replaySpeed);
|
|
45
|
+
}
|
|
46
|
+
function toolCallsLength(calls) {
|
|
47
|
+
return calls.map((tc) => tc.name + tc.arguments).join("").length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The payload a reply serves, as OpenAI Chat Completions sizes its
|
|
51
|
+
* non-streaming wait: reasoning, then text content, then each tool call's
|
|
52
|
+
* name and arguments. Non-empty `blocks` are authoritative and resolve
|
|
53
|
+
* through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,
|
|
54
|
+
* so the length is the same on every provider.
|
|
55
|
+
*/
|
|
56
|
+
function servedPayloadLength(response, reasoning) {
|
|
57
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
58
|
+
if (require_helpers.isCombinedFixtureResponse(response)) {
|
|
59
|
+
if (response.blocks && response.blocks.length > 0) {
|
|
60
|
+
const outcome = require_helpers.resolveFixtureBlockOutcome(response.blocks);
|
|
61
|
+
return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);
|
|
62
|
+
}
|
|
63
|
+
return reasoningLength + (response.content ?? "").length + toolCallsLength(response.toolCalls ?? []);
|
|
64
|
+
}
|
|
65
|
+
if (require_helpers.isTextResponse(response)) return reasoningLength + response.content.length;
|
|
66
|
+
if (require_helpers.isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);
|
|
67
|
+
return reasoningLength;
|
|
68
|
+
}
|
|
69
|
+
/** Each served call's name plus its function `arguments` or custom `input`. */
|
|
70
|
+
function servedCallsLength(calls) {
|
|
71
|
+
return calls.map((tc) => tc.name + (tc.type === "custom" ? tc.input : tc.arguments)).join("").length;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The payload the OpenAI Responses API serves, sized as
|
|
75
|
+
* {@link servedPayloadLength} sizes it but from the Responses carriers: a
|
|
76
|
+
* non-empty `responsesBlocks` (else a non-empty `blocks`) through
|
|
77
|
+
* `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through
|
|
78
|
+
* `servedToolCalls`. A custom call counts its name and `input`; its stream
|
|
79
|
+
* sends the input in `chunkSize` deltas, as a function call's arguments.
|
|
80
|
+
*/
|
|
81
|
+
function responsesPayloadLength(response, reasoning) {
|
|
82
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
83
|
+
if (require_helpers.isCombinedFixtureResponse(response)) {
|
|
84
|
+
const outcome = require_helpers.resolveServedBlockOutcome(response);
|
|
85
|
+
if (outcome) return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);
|
|
86
|
+
return reasoningLength + (response.content ?? "").length + servedCallsLength(require_helpers.servedToolCalls(response));
|
|
87
|
+
}
|
|
88
|
+
if (require_helpers.isTextResponse(response)) return reasoningLength + response.content.length;
|
|
89
|
+
if (require_helpers.isToolCallResponse(response)) return reasoningLength + servedCallsLength(require_helpers.servedToolCalls(response));
|
|
90
|
+
return reasoningLength;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The stream frames of one `image_generation_call` item: `output_item.added`,
|
|
94
|
+
* the `in_progress`, `generating` and `completed` events, and
|
|
95
|
+
* `output_item.done`.
|
|
96
|
+
*/
|
|
97
|
+
const IMAGE_GENERATION_FRAMES = 5;
|
|
98
|
+
/**
|
|
99
|
+
* The frames the OpenAI Responses stream sends for a fixture's `citations`
|
|
100
|
+
* and generated `files`, which carry no text for {@link responsesPayloadLength}
|
|
101
|
+
* to count: one `output_text.annotation.added` per citation on a turn with
|
|
102
|
+
* text, and {@link IMAGE_GENERATION_FRAMES} per file (its
|
|
103
|
+
* `image_generation_call` item has no deltas; these frames are its whole
|
|
104
|
+
* stream). The non-streaming wait counts each as one chunk, as the stream
|
|
105
|
+
* counts each as one frame, so the JSON wait is not undersized for them. A
|
|
106
|
+
* malformed value counts as none: this never throws, and the builder rejects
|
|
107
|
+
* it with its coded error before any byte is written.
|
|
108
|
+
*/
|
|
109
|
+
function responsesOutputFrames(response) {
|
|
110
|
+
const textTurn = require_helpers.isCombinedFixtureResponse(response) || require_helpers.isTextResponse(response);
|
|
111
|
+
if (!textTurn && !require_helpers.isToolCallResponse(response)) return 0;
|
|
112
|
+
return (Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0) + (textTurn && Array.isArray(response.citations) ? response.citations.length : 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The payload length for a misbehavior-applied reply. For the three fault
|
|
116
|
+
* terminals on a text response (refusal, content filter, reasoning-only) it is
|
|
117
|
+
* OpenAI's selection: the refusal text, nothing, or the reasoning. In every
|
|
118
|
+
* other case, including a stop plan on a tool-call, combined or blocks
|
|
119
|
+
* response, it is `servedLength`: {@link servedPayloadLength} by default, or
|
|
120
|
+
* {@link responsesPayloadLength} on the OpenAI Responses API.
|
|
121
|
+
*/
|
|
122
|
+
function misbehaviorPayloadLength(plan, effReasoning, servedLength = servedPayloadLength) {
|
|
123
|
+
if (require_helpers.isTextResponse(plan.response) && (plan.stop === "refusal" || plan.stop === "content_filter" || plan.reasoning !== void 0)) {
|
|
124
|
+
if (plan.stop === "refusal") return (plan.refusal ?? "").length;
|
|
125
|
+
if (plan.stop === "content_filter") return 0;
|
|
126
|
+
return (plan.reasoning ?? "").length;
|
|
127
|
+
}
|
|
128
|
+
return servedLength(plan.response, effReasoning);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so
|
|
132
|
+
* with `duplicate` true the recorded gaps grow to at least `frameCount - 1`
|
|
133
|
+
* entries by repeating the last gap (0 when the list is empty). A longer
|
|
134
|
+
* list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):
|
|
135
|
+
* the list is then truncated to empty. The wait is 0 either way, because a
|
|
136
|
+
* `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no
|
|
137
|
+
* `t`, `t` is returned unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function extendRecordedTimings(t, frameCount, duplicate) {
|
|
140
|
+
if (!duplicate || !t) return t;
|
|
141
|
+
const gaps = t.interChunkDelaysMs;
|
|
142
|
+
return {
|
|
143
|
+
...t,
|
|
144
|
+
interChunkDelaysMs: Array.from({ length: Math.max(gaps.length, frameCount - 1) }, (_, index) => gaps[index] ?? gaps.at(-1) ?? 0)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
exports.extendRecordedTimings = extendRecordedTimings;
|
|
150
|
+
exports.misbehaviorPayloadLength = misbehaviorPayloadLength;
|
|
151
|
+
exports.nonStreamingDelayMs = nonStreamingDelayMs;
|
|
152
|
+
exports.nonStreamingRecordedTimings = nonStreamingRecordedTimings;
|
|
153
|
+
exports.nonStreamingTimingEnabled = nonStreamingTimingEnabled;
|
|
154
|
+
exports.responsesOutputFrames = responsesOutputFrames;
|
|
155
|
+
exports.responsesPayloadLength = responsesPayloadLength;
|
|
156
|
+
exports.servedPayloadLength = servedPayloadLength;
|
|
157
|
+
//# sourceMappingURL=timing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.cjs","names":["calculateTotalDelay","isCombinedFixtureResponse","resolveFixtureBlockOutcome","isTextResponse","isToolCallResponse","resolveServedBlockOutcome","servedToolCalls"],"sources":["../../src/response/timing.ts"],"sourcesContent":["import type {\n Fixture,\n FixtureResponse,\n FixtureToolCall,\n RecordedTimings,\n ToolCall,\n} from \"../types.js\";\nimport type { MisbehaviorCandidate } from \"../misbehavior.js\";\nimport { calculateTotalDelay } from \"../sse-writer.js\";\nimport {\n isCombinedFixtureResponse,\n isTextResponse,\n isToolCallResponse,\n resolveFixtureBlockOutcome,\n resolveServedBlockOutcome,\n servedToolCalls,\n} from \"../helpers.js\";\n\n/** Which non-streaming surface asks: OpenAI Chat Completions, or any other provider. */\nexport type NonStreamingSurface = \"openai-chat\" | \"other\";\n\n/**\n * Whether a non-streaming (`stream: false`) reply on `surface` waits. It is\n * opt-in per fixture: `nonStreamingTimingAllProviders: true` on every\n * surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat\n * Completions only. Either one also needs an explicit `streamingProfile` or\n * `recordedTimings`. Only the boolean `true` counts.\n */\nexport function nonStreamingTimingEnabled(fixture: Fixture, surface: NonStreamingSurface): boolean {\n const optedIn =\n fixture.nonStreamingTimingAllProviders === true ||\n (fixture.nonStreamingTiming === true && surface === \"openai-chat\");\n return optedIn && !!(fixture.streamingProfile || fixture.recordedTimings);\n}\n\n/**\n * The recorded timings a non-streaming wait on `surface` is sized from. `t` is\n * returned as given when it is `undefined`, when its `interChunkDelaysMs` is\n * an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.\n * Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is\n * returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps\n * and the reply is still served.\n *\n * The `nonStreamingTiming: true` OpenAI Chat Completions case is passed\n * through unchanged so that the shipped option keeps its behavior: with no\n * `streamingProfile`, sizing the wait for a reply of more than one chunk reads\n * the missing `interChunkDelaysMs` and throws. With `deliverJson`'s\n * `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any\n * case, because {@link extendRecordedTimings} reads the list first.\n */\nexport function nonStreamingRecordedTimings(\n fixture: Fixture,\n surface: NonStreamingSurface,\n t: RecordedTimings | undefined,\n): RecordedTimings | undefined {\n if (!t || Array.isArray(t.interChunkDelaysMs)) return t;\n if (fixture.nonStreamingTiming === true && surface === \"openai-chat\") return t;\n return { ...t, interChunkDelaysMs: [] };\n}\n\n/**\n * The total wait before a non-streaming reply: `calculateTotalDelay` over the\n * content chunks `payloadLength` makes at `chunkSize`. There is no latency\n * input, so bare `latency` never delays a non-streaming reply.\n */\nexport function nonStreamingDelayMs(\n fixture: Fixture,\n payloadLength: number,\n chunkSize: number,\n recordedTimings: RecordedTimings | undefined,\n replaySpeed: number | undefined,\n): number {\n return calculateTotalDelay(\n payloadLength,\n chunkSize,\n fixture.streamingProfile,\n recordedTimings,\n replaySpeed,\n );\n}\n\nfunction toolCallsLength(calls: ToolCall[]): number {\n return calls.map((tc) => tc.name + tc.arguments).join(\"\").length;\n}\n\n/**\n * The payload a reply serves, as OpenAI Chat Completions sizes its\n * non-streaming wait: reasoning, then text content, then each tool call's\n * name and arguments. Non-empty `blocks` are authoritative and resolve\n * through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,\n * so the length is the same on every provider.\n */\nexport function servedPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n if (response.blocks && response.blocks.length > 0) {\n const outcome = resolveFixtureBlockOutcome(response.blocks);\n return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength + (response.content ?? \"\").length + toolCallsLength(response.toolCalls ?? [])\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);\n return reasoningLength;\n}\n\n/** Each served call's name plus its function `arguments` or custom `input`. */\nfunction servedCallsLength(calls: readonly FixtureToolCall[]): number {\n return calls.map((tc) => tc.name + (tc.type === \"custom\" ? tc.input : tc.arguments)).join(\"\")\n .length;\n}\n\n/**\n * The payload the OpenAI Responses API serves, sized as\n * {@link servedPayloadLength} sizes it but from the Responses carriers: a\n * non-empty `responsesBlocks` (else a non-empty `blocks`) through\n * `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through\n * `servedToolCalls`. A custom call counts its name and `input`; its stream\n * sends the input in `chunkSize` deltas, as a function call's arguments.\n */\nexport function responsesPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n const outcome = resolveServedBlockOutcome(response);\n if (outcome) {\n return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength +\n (response.content ?? \"\").length +\n servedCallsLength(servedToolCalls(response))\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) {\n return reasoningLength + servedCallsLength(servedToolCalls(response));\n }\n return reasoningLength;\n}\n\n/**\n * The stream frames of one `image_generation_call` item: `output_item.added`,\n * the `in_progress`, `generating` and `completed` events, and\n * `output_item.done`.\n */\nexport const IMAGE_GENERATION_FRAMES = 5;\n\n/**\n * The frames the OpenAI Responses stream sends for a fixture's `citations`\n * and generated `files`, which carry no text for {@link responsesPayloadLength}\n * to count: one `output_text.annotation.added` per citation on a turn with\n * text, and {@link IMAGE_GENERATION_FRAMES} per file (its\n * `image_generation_call` item has no deltas; these frames are its whole\n * stream). The non-streaming wait counts each as one chunk, as the stream\n * counts each as one frame, so the JSON wait is not undersized for them. A\n * malformed value counts as none: this never throws, and the builder rejects\n * it with its coded error before any byte is written.\n */\nexport function responsesOutputFrames(response: FixtureResponse): number {\n const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);\n if (!textTurn && !isToolCallResponse(response)) return 0;\n const files = Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0;\n // A tool-only turn has no text to annotate: its builder rejects citations.\n const citations = textTurn && Array.isArray(response.citations) ? response.citations.length : 0;\n return files + citations;\n}\n\n/**\n * The payload length for a misbehavior-applied reply. For the three fault\n * terminals on a text response (refusal, content filter, reasoning-only) it is\n * OpenAI's selection: the refusal text, nothing, or the reasoning. In every\n * other case, including a stop plan on a tool-call, combined or blocks\n * response, it is `servedLength`: {@link servedPayloadLength} by default, or\n * {@link responsesPayloadLength} on the OpenAI Responses API.\n */\nexport function misbehaviorPayloadLength(\n plan: Pick<MisbehaviorCandidate, \"response\" | \"stop\" | \"refusal\" | \"reasoning\">,\n effReasoning: string | undefined,\n servedLength: (response: FixtureResponse, reasoning?: string) => number = servedPayloadLength,\n): number {\n if (\n isTextResponse(plan.response) &&\n (plan.stop === \"refusal\" || plan.stop === \"content_filter\" || plan.reasoning !== undefined)\n ) {\n if (plan.stop === \"refusal\") return (plan.refusal ?? \"\").length;\n if (plan.stop === \"content_filter\") return 0;\n return (plan.reasoning ?? \"\").length;\n }\n return servedLength(plan.response, effReasoning);\n}\n\n/**\n * The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so\n * with `duplicate` true the recorded gaps grow to at least `frameCount - 1`\n * entries by repeating the last gap (0 when the list is empty). A longer\n * list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):\n * the list is then truncated to empty. The wait is 0 either way, because a\n * `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no\n * `t`, `t` is returned unchanged.\n */\nexport function extendRecordedTimings(\n t: RecordedTimings | undefined,\n frameCount: number,\n duplicate: boolean,\n): RecordedTimings | undefined {\n if (!duplicate || !t) return t;\n const gaps = t.interChunkDelaysMs;\n return {\n ...t,\n interChunkDelaysMs: Array.from(\n { length: Math.max(gaps.length, frameCount - 1) },\n (_, index) => gaps[index] ?? gaps.at(-1) ?? 0,\n ),\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { isCombinedFixtureResponse, isTextResponse, isToolCallResponse, resolveFixtureBlockOutcome, resolveServedBlockOutcome, servedToolCalls } from "../helpers.js";
|
|
2
|
+
import { calculateTotalDelay } from "../sse-writer.js";
|
|
3
|
+
|
|
4
|
+
//#region src/response/timing.ts
|
|
5
|
+
/**
|
|
6
|
+
* Whether a non-streaming (`stream: false`) reply on `surface` waits. It is
|
|
7
|
+
* opt-in per fixture: `nonStreamingTimingAllProviders: true` on every
|
|
8
|
+
* surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat
|
|
9
|
+
* Completions only. Either one also needs an explicit `streamingProfile` or
|
|
10
|
+
* `recordedTimings`. Only the boolean `true` counts.
|
|
11
|
+
*/
|
|
12
|
+
function nonStreamingTimingEnabled(fixture, surface) {
|
|
13
|
+
return (fixture.nonStreamingTimingAllProviders === true || fixture.nonStreamingTiming === true && surface === "openai-chat") && !!(fixture.streamingProfile || fixture.recordedTimings);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The recorded timings a non-streaming wait on `surface` is sized from. `t` is
|
|
17
|
+
* returned as given when it is `undefined`, when its `interChunkDelaysMs` is
|
|
18
|
+
* an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.
|
|
19
|
+
* Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is
|
|
20
|
+
* returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps
|
|
21
|
+
* and the reply is still served.
|
|
22
|
+
*
|
|
23
|
+
* The `nonStreamingTiming: true` OpenAI Chat Completions case is passed
|
|
24
|
+
* through unchanged so that the shipped option keeps its behavior: with no
|
|
25
|
+
* `streamingProfile`, sizing the wait for a reply of more than one chunk reads
|
|
26
|
+
* the missing `interChunkDelaysMs` and throws. With `deliverJson`'s
|
|
27
|
+
* `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any
|
|
28
|
+
* case, because {@link extendRecordedTimings} reads the list first.
|
|
29
|
+
*/
|
|
30
|
+
function nonStreamingRecordedTimings(fixture, surface, t) {
|
|
31
|
+
if (!t || Array.isArray(t.interChunkDelaysMs)) return t;
|
|
32
|
+
if (fixture.nonStreamingTiming === true && surface === "openai-chat") return t;
|
|
33
|
+
return {
|
|
34
|
+
...t,
|
|
35
|
+
interChunkDelaysMs: []
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The total wait before a non-streaming reply: `calculateTotalDelay` over the
|
|
40
|
+
* content chunks `payloadLength` makes at `chunkSize`. There is no latency
|
|
41
|
+
* input, so bare `latency` never delays a non-streaming reply.
|
|
42
|
+
*/
|
|
43
|
+
function nonStreamingDelayMs(fixture, payloadLength, chunkSize, recordedTimings, replaySpeed) {
|
|
44
|
+
return calculateTotalDelay(payloadLength, chunkSize, fixture.streamingProfile, recordedTimings, replaySpeed);
|
|
45
|
+
}
|
|
46
|
+
function toolCallsLength(calls) {
|
|
47
|
+
return calls.map((tc) => tc.name + tc.arguments).join("").length;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The payload a reply serves, as OpenAI Chat Completions sizes its
|
|
51
|
+
* non-streaming wait: reasoning, then text content, then each tool call's
|
|
52
|
+
* name and arguments. Non-empty `blocks` are authoritative and resolve
|
|
53
|
+
* through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,
|
|
54
|
+
* so the length is the same on every provider.
|
|
55
|
+
*/
|
|
56
|
+
function servedPayloadLength(response, reasoning) {
|
|
57
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
58
|
+
if (isCombinedFixtureResponse(response)) {
|
|
59
|
+
if (response.blocks && response.blocks.length > 0) {
|
|
60
|
+
const outcome = resolveFixtureBlockOutcome(response.blocks);
|
|
61
|
+
return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);
|
|
62
|
+
}
|
|
63
|
+
return reasoningLength + (response.content ?? "").length + toolCallsLength(response.toolCalls ?? []);
|
|
64
|
+
}
|
|
65
|
+
if (isTextResponse(response)) return reasoningLength + response.content.length;
|
|
66
|
+
if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);
|
|
67
|
+
return reasoningLength;
|
|
68
|
+
}
|
|
69
|
+
/** Each served call's name plus its function `arguments` or custom `input`. */
|
|
70
|
+
function servedCallsLength(calls) {
|
|
71
|
+
return calls.map((tc) => tc.name + (tc.type === "custom" ? tc.input : tc.arguments)).join("").length;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The payload the OpenAI Responses API serves, sized as
|
|
75
|
+
* {@link servedPayloadLength} sizes it but from the Responses carriers: a
|
|
76
|
+
* non-empty `responsesBlocks` (else a non-empty `blocks`) through
|
|
77
|
+
* `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through
|
|
78
|
+
* `servedToolCalls`. A custom call counts its name and `input`; its stream
|
|
79
|
+
* sends the input in `chunkSize` deltas, as a function call's arguments.
|
|
80
|
+
*/
|
|
81
|
+
function responsesPayloadLength(response, reasoning) {
|
|
82
|
+
const reasoningLength = reasoning?.length ?? 0;
|
|
83
|
+
if (isCombinedFixtureResponse(response)) {
|
|
84
|
+
const outcome = resolveServedBlockOutcome(response);
|
|
85
|
+
if (outcome) return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);
|
|
86
|
+
return reasoningLength + (response.content ?? "").length + servedCallsLength(servedToolCalls(response));
|
|
87
|
+
}
|
|
88
|
+
if (isTextResponse(response)) return reasoningLength + response.content.length;
|
|
89
|
+
if (isToolCallResponse(response)) return reasoningLength + servedCallsLength(servedToolCalls(response));
|
|
90
|
+
return reasoningLength;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The stream frames of one `image_generation_call` item: `output_item.added`,
|
|
94
|
+
* the `in_progress`, `generating` and `completed` events, and
|
|
95
|
+
* `output_item.done`.
|
|
96
|
+
*/
|
|
97
|
+
const IMAGE_GENERATION_FRAMES = 5;
|
|
98
|
+
/**
|
|
99
|
+
* The frames the OpenAI Responses stream sends for a fixture's `citations`
|
|
100
|
+
* and generated `files`, which carry no text for {@link responsesPayloadLength}
|
|
101
|
+
* to count: one `output_text.annotation.added` per citation on a turn with
|
|
102
|
+
* text, and {@link IMAGE_GENERATION_FRAMES} per file (its
|
|
103
|
+
* `image_generation_call` item has no deltas; these frames are its whole
|
|
104
|
+
* stream). The non-streaming wait counts each as one chunk, as the stream
|
|
105
|
+
* counts each as one frame, so the JSON wait is not undersized for them. A
|
|
106
|
+
* malformed value counts as none: this never throws, and the builder rejects
|
|
107
|
+
* it with its coded error before any byte is written.
|
|
108
|
+
*/
|
|
109
|
+
function responsesOutputFrames(response) {
|
|
110
|
+
const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);
|
|
111
|
+
if (!textTurn && !isToolCallResponse(response)) return 0;
|
|
112
|
+
return (Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0) + (textTurn && Array.isArray(response.citations) ? response.citations.length : 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The payload length for a misbehavior-applied reply. For the three fault
|
|
116
|
+
* terminals on a text response (refusal, content filter, reasoning-only) it is
|
|
117
|
+
* OpenAI's selection: the refusal text, nothing, or the reasoning. In every
|
|
118
|
+
* other case, including a stop plan on a tool-call, combined or blocks
|
|
119
|
+
* response, it is `servedLength`: {@link servedPayloadLength} by default, or
|
|
120
|
+
* {@link responsesPayloadLength} on the OpenAI Responses API.
|
|
121
|
+
*/
|
|
122
|
+
function misbehaviorPayloadLength(plan, effReasoning, servedLength = servedPayloadLength) {
|
|
123
|
+
if (isTextResponse(plan.response) && (plan.stop === "refusal" || plan.stop === "content_filter" || plan.reasoning !== void 0)) {
|
|
124
|
+
if (plan.stop === "refusal") return (plan.refusal ?? "").length;
|
|
125
|
+
if (plan.stop === "content_filter") return 0;
|
|
126
|
+
return (plan.reasoning ?? "").length;
|
|
127
|
+
}
|
|
128
|
+
return servedLength(plan.response, effReasoning);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so
|
|
132
|
+
* with `duplicate` true the recorded gaps grow to at least `frameCount - 1`
|
|
133
|
+
* entries by repeating the last gap (0 when the list is empty). A longer
|
|
134
|
+
* list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):
|
|
135
|
+
* the list is then truncated to empty. The wait is 0 either way, because a
|
|
136
|
+
* `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no
|
|
137
|
+
* `t`, `t` is returned unchanged.
|
|
138
|
+
*/
|
|
139
|
+
function extendRecordedTimings(t, frameCount, duplicate) {
|
|
140
|
+
if (!duplicate || !t) return t;
|
|
141
|
+
const gaps = t.interChunkDelaysMs;
|
|
142
|
+
return {
|
|
143
|
+
...t,
|
|
144
|
+
interChunkDelaysMs: Array.from({ length: Math.max(gaps.length, frameCount - 1) }, (_, index) => gaps[index] ?? gaps.at(-1) ?? 0)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//#endregion
|
|
149
|
+
export { extendRecordedTimings, misbehaviorPayloadLength, nonStreamingDelayMs, nonStreamingRecordedTimings, nonStreamingTimingEnabled, responsesOutputFrames, responsesPayloadLength, servedPayloadLength };
|
|
150
|
+
//# sourceMappingURL=timing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.js","names":[],"sources":["../../src/response/timing.ts"],"sourcesContent":["import type {\n Fixture,\n FixtureResponse,\n FixtureToolCall,\n RecordedTimings,\n ToolCall,\n} from \"../types.js\";\nimport type { MisbehaviorCandidate } from \"../misbehavior.js\";\nimport { calculateTotalDelay } from \"../sse-writer.js\";\nimport {\n isCombinedFixtureResponse,\n isTextResponse,\n isToolCallResponse,\n resolveFixtureBlockOutcome,\n resolveServedBlockOutcome,\n servedToolCalls,\n} from \"../helpers.js\";\n\n/** Which non-streaming surface asks: OpenAI Chat Completions, or any other provider. */\nexport type NonStreamingSurface = \"openai-chat\" | \"other\";\n\n/**\n * Whether a non-streaming (`stream: false`) reply on `surface` waits. It is\n * opt-in per fixture: `nonStreamingTimingAllProviders: true` on every\n * surface that calls this, or the shipped `nonStreamingTiming: true` on OpenAI Chat\n * Completions only. Either one also needs an explicit `streamingProfile` or\n * `recordedTimings`. Only the boolean `true` counts.\n */\nexport function nonStreamingTimingEnabled(fixture: Fixture, surface: NonStreamingSurface): boolean {\n const optedIn =\n fixture.nonStreamingTimingAllProviders === true ||\n (fixture.nonStreamingTiming === true && surface === \"openai-chat\");\n return optedIn && !!(fixture.streamingProfile || fixture.recordedTimings);\n}\n\n/**\n * The recorded timings a non-streaming wait on `surface` is sized from. `t` is\n * returned as given when it is `undefined`, when its `interChunkDelaysMs` is\n * an array, or for `nonStreamingTiming: true` on OpenAI Chat Completions.\n * Otherwise (a fixture built in code can omit `interChunkDelaysMs`) it is\n * returned with `interChunkDelaysMs: []`, so the wait counts no recorded gaps\n * and the reply is still served.\n *\n * The `nonStreamingTiming: true` OpenAI Chat Completions case is passed\n * through unchanged so that the shipped option keeps its behavior: with no\n * `streamingProfile`, sizing the wait for a reply of more than one chunk reads\n * the missing `interChunkDelaysMs` and throws. With `deliverJson`'s\n * `duplicateId` set, that path throws on a missing `interChunkDelaysMs` in any\n * case, because {@link extendRecordedTimings} reads the list first.\n */\nexport function nonStreamingRecordedTimings(\n fixture: Fixture,\n surface: NonStreamingSurface,\n t: RecordedTimings | undefined,\n): RecordedTimings | undefined {\n if (!t || Array.isArray(t.interChunkDelaysMs)) return t;\n if (fixture.nonStreamingTiming === true && surface === \"openai-chat\") return t;\n return { ...t, interChunkDelaysMs: [] };\n}\n\n/**\n * The total wait before a non-streaming reply: `calculateTotalDelay` over the\n * content chunks `payloadLength` makes at `chunkSize`. There is no latency\n * input, so bare `latency` never delays a non-streaming reply.\n */\nexport function nonStreamingDelayMs(\n fixture: Fixture,\n payloadLength: number,\n chunkSize: number,\n recordedTimings: RecordedTimings | undefined,\n replaySpeed: number | undefined,\n): number {\n return calculateTotalDelay(\n payloadLength,\n chunkSize,\n fixture.streamingProfile,\n recordedTimings,\n replaySpeed,\n );\n}\n\nfunction toolCallsLength(calls: ToolCall[]): number {\n return calls.map((tc) => tc.name + tc.arguments).join(\"\").length;\n}\n\n/**\n * The payload a reply serves, as OpenAI Chat Completions sizes its\n * non-streaming wait: reasoning, then text content, then each tool call's\n * name and arguments. Non-empty `blocks` are authoritative and resolve\n * through `resolveFixtureBlockOutcome`. The inputs are fixture-level values,\n * so the length is the same on every provider.\n */\nexport function servedPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n if (response.blocks && response.blocks.length > 0) {\n const outcome = resolveFixtureBlockOutcome(response.blocks);\n return reasoningLength + outcome.content.length + toolCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength + (response.content ?? \"\").length + toolCallsLength(response.toolCalls ?? [])\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) return reasoningLength + toolCallsLength(response.toolCalls);\n return reasoningLength;\n}\n\n/** Each served call's name plus its function `arguments` or custom `input`. */\nfunction servedCallsLength(calls: readonly FixtureToolCall[]): number {\n return calls.map((tc) => tc.name + (tc.type === \"custom\" ? tc.input : tc.arguments)).join(\"\")\n .length;\n}\n\n/**\n * The payload the OpenAI Responses API serves, sized as\n * {@link servedPayloadLength} sizes it but from the Responses carriers: a\n * non-empty `responsesBlocks` (else a non-empty `blocks`) through\n * `resolveServedBlockOutcome`, and `toolCalls` plus `customToolCalls` through\n * `servedToolCalls`. A custom call counts its name and `input`; its stream\n * sends the input in `chunkSize` deltas, as a function call's arguments.\n */\nexport function responsesPayloadLength(response: FixtureResponse, reasoning?: string): number {\n const reasoningLength = reasoning?.length ?? 0;\n if (isCombinedFixtureResponse(response)) {\n const outcome = resolveServedBlockOutcome(response);\n if (outcome) {\n return reasoningLength + outcome.content.length + servedCallsLength(outcome.toolCalls);\n }\n return (\n reasoningLength +\n (response.content ?? \"\").length +\n servedCallsLength(servedToolCalls(response))\n );\n }\n if (isTextResponse(response)) return reasoningLength + response.content.length;\n if (isToolCallResponse(response)) {\n return reasoningLength + servedCallsLength(servedToolCalls(response));\n }\n return reasoningLength;\n}\n\n/**\n * The stream frames of one `image_generation_call` item: `output_item.added`,\n * the `in_progress`, `generating` and `completed` events, and\n * `output_item.done`.\n */\nexport const IMAGE_GENERATION_FRAMES = 5;\n\n/**\n * The frames the OpenAI Responses stream sends for a fixture's `citations`\n * and generated `files`, which carry no text for {@link responsesPayloadLength}\n * to count: one `output_text.annotation.added` per citation on a turn with\n * text, and {@link IMAGE_GENERATION_FRAMES} per file (its\n * `image_generation_call` item has no deltas; these frames are its whole\n * stream). The non-streaming wait counts each as one chunk, as the stream\n * counts each as one frame, so the JSON wait is not undersized for them. A\n * malformed value counts as none: this never throws, and the builder rejects\n * it with its coded error before any byte is written.\n */\nexport function responsesOutputFrames(response: FixtureResponse): number {\n const textTurn = isCombinedFixtureResponse(response) || isTextResponse(response);\n if (!textTurn && !isToolCallResponse(response)) return 0;\n const files = Array.isArray(response.files) ? response.files.length * IMAGE_GENERATION_FRAMES : 0;\n // A tool-only turn has no text to annotate: its builder rejects citations.\n const citations = textTurn && Array.isArray(response.citations) ? response.citations.length : 0;\n return files + citations;\n}\n\n/**\n * The payload length for a misbehavior-applied reply. For the three fault\n * terminals on a text response (refusal, content filter, reasoning-only) it is\n * OpenAI's selection: the refusal text, nothing, or the reasoning. In every\n * other case, including a stop plan on a tool-call, combined or blocks\n * response, it is `servedLength`: {@link servedPayloadLength} by default, or\n * {@link responsesPayloadLength} on the OpenAI Responses API.\n */\nexport function misbehaviorPayloadLength(\n plan: Pick<MisbehaviorCandidate, \"response\" | \"stop\" | \"refusal\" | \"reasoning\">,\n effReasoning: string | undefined,\n servedLength: (response: FixtureResponse, reasoning?: string) => number = servedPayloadLength,\n): number {\n if (\n isTextResponse(plan.response) &&\n (plan.stop === \"refusal\" || plan.stop === \"content_filter\" || plan.reasoning !== undefined)\n ) {\n if (plan.stop === \"refusal\") return (plan.refusal ?? \"\").length;\n if (plan.stop === \"content_filter\") return 0;\n return (plan.reasoning ?? \"\").length;\n }\n return servedLength(plan.response, effReasoning);\n}\n\n/**\n * The K4 duplicate-id gap extension: a duplicate-id plan can add frames, so\n * with `duplicate` true the recorded gaps grow to at least `frameCount - 1`\n * entries by repeating the last gap (0 when the list is empty). A longer\n * list is kept whole, except when `frameCount` is `NaN` (a `NaN` chunk size):\n * the list is then truncated to empty. The wait is 0 either way, because a\n * `NaN` chunk size gives no chunks to wait for. With `duplicate` false or no\n * `t`, `t` is returned unchanged.\n */\nexport function extendRecordedTimings(\n t: RecordedTimings | undefined,\n frameCount: number,\n duplicate: boolean,\n): RecordedTimings | undefined {\n if (!duplicate || !t) return t;\n const gaps = t.interChunkDelaysMs;\n return {\n ...t,\n interChunkDelaysMs: Array.from(\n { length: Math.max(gaps.length, frameCount - 1) },\n (_, index) => gaps[index] ?? gaps.at(-1) ?? 0,\n ),\n };\n}\n"],"mappings":"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"}
|