@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
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { MisbehaviorSummary } from \"./misbehavior.js\";\nimport type * as http from \"node:http\";\nimport type * as net from \"node:net\";\nimport type { Journal } from \"./journal.js\";\nimport type { LiveFixtureResponse, LiveOptions } from \"./live-types.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type { McpFakeAddOrigin, McpFakeAddResult } from \"./mcp-fakes.js\";\n\n// aimock type definitions — shared across all provider adapters and the fixture router.\n\nexport interface Mountable {\n handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean>;\n handleUpgrade?(socket: net.Socket, head: Buffer, pathname: string): Promise<boolean>;\n health?(): { status: string; [key: string]: unknown };\n setJournal?(journal: Journal): void;\n setBaseUrl?(url: string): void;\n setRegistry?(registry: MetricsRegistry): void;\n /**\n * MCP fakes. A mount \"is an MCPMock\" for fakes if and only if it\n * implements `addMcpFakes`. Always appends. `origin` picks the entry-id\n * source and its counter: `file` keeps each block's `source` (`@<k>` load\n * counter); `code` / `control-api` is one run-time addition (`code#<n>` /\n * `control-api#<n>`). Returns the shadowed-entry warnings with the real\n * entry ids. When any block is bad, or the input holds no blocks, throws one\n * `FixtureLoadError` (an `McpFakesAddError` carrying every error found and\n * the warnings) and adds nothing.\n */\n addMcpFakes?(blocks: McpFakeSource[], origin: McpFakeAddOrigin): McpFakeAddResult;\n /** Unload every fake on this mount. The entry-id counters are kept. */\n clearMcpFakes?(): void;\n /** Reset fake consumption state for one test id, or for all when omitted. */\n resetScenarioState?(testId?: string): void;\n /** Hands an MCP mount a `Logger`. Nothing in the server calls it. */\n setLogger?(logger: Logger): void;\n}\n\nexport interface ContentPart {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\nexport interface ChatMessage {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n content: string | ContentPart[] | null;\n name?: string;\n tool_calls?: ToolCallMessage[];\n /**\n * OpenAI Responses `custom_tool_call` history items. Set only when the\n * server runs with `responsesTools: \"extended\"`; the default ignores\n * custom tool call history, as earlier releases did.\n */\n custom_tool_calls?: CustomToolCallMessage[];\n tool_call_id?: string;\n}\n\nexport interface ToolCallMessage {\n id: string;\n type: \"function\";\n /**\n * OpenAI Responses namespace of the called tool. Set only with\n * `responsesTools: \"extended\"`, when the history item had one.\n */\n namespace?: string;\n function: { name: string; arguments: string };\n}\n\n/**\n * An OpenAI Responses `custom_tool_call` history item, normalized for fixture\n * matching. Produced only with `responsesTools: \"extended\"`.\n */\nexport interface CustomToolCallMessage {\n id: string;\n type: \"custom\";\n name: string;\n /** Free-text input of the call (\"\" when the history item had none). */\n input: string;\n namespace?: string;\n}\n\nexport interface ChatCompletionRequest {\n model: string;\n messages: ChatMessage[];\n stream?: boolean;\n /**\n * OpenRouter fallback model list. When present (and the request arrived on\n * the OpenRouter `/api/v1/...` base), the candidate list `[model, ...models]`\n * is attempted in order. A NON-error fixture match wins; an ERROR fixture\n * candidate falls through to the next candidate by default — UNLESS it is\n * terminal, in which case that error is served with no failover. An error is\n * terminal when its fixture sets `fallthrough: false` (per-error-class gate)\n * or when the request sets `provider.allow_fallbacks: false` (request-level\n * gate). A winning (non-error) slug is echoed back as the response `model`;\n * when the chain terminates on an error, the response is the OpenRouter error\n * envelope (`{ error: { code, message, metadata? } }`), which carries no\n * `model` field. Purely additive: a request without `models` behaves as a\n * single-model match. Every other\n * OpenRouter request extension (`route`, `reasoning`, `plugins`,\n * `prediction`, `usage`, and any unknown key) is accepted and journaled via\n * the index signature below — never required, never rejected. aimock does not\n * model those sub-shapes; unknown body keys pass straight through.\n */\n models?: string[];\n /**\n * OpenRouter provider-routing preferences. Accepted loosely and journaled\n * verbatim; the ONLY sub-field aimock reads behaviorally is\n * `allow_fallbacks`. When `allow_fallbacks === false` on an OpenRouter\n * request, the `models[]` fallback loop is suppressed — only the primary is\n * tried, and a primary error fixture is served as terminal instead of\n * falling through (the fail-CLOSED half of the feature). Absent/`true` keeps\n * the default fall-through behavior. Every other provider-prefs key\n * (`order`, `only`, `ignore`, `sort`, …) passes straight through unread.\n */\n provider?: { allow_fallbacks?: boolean; [key: string]: unknown };\n stream_options?: { include_usage?: boolean; [key: string]: unknown };\n temperature?: number;\n max_tokens?: number;\n tools?: ToolDefinition[];\n /**\n * OpenAI Responses `custom` tools offered by the request (top level, inside\n * a `namespace` tool, or in an `additional_tools` / `tool_search_output`\n * input item). Set only with `responsesTools: \"extended\"`; `toolName`\n * matches these as well as `tools`.\n */\n customTools?: CustomToolDefinition[];\n tool_choice?: string | object;\n response_format?: { type: string; [key: string]: unknown };\n /** Embedding input text, set by the embeddings handler for fixture matching. */\n embeddingInput?: string;\n /** Endpoint type, set by handlers for fixture endpoint filtering. */\n _endpointType?: string;\n /** Context identifier, set by handlers for fixture context routing. */\n _context?: string;\n /**\n * Video-provider discriminator, set by the async video handlers for journal\n * and dispatch clarity only. NOT a fixture match key — `buildFixtureMatch`\n * derives the endpoint from `_endpointType` and never reads this field; the\n * model string is the provider disambiguator (veo-* / grok-imagine-* /\n * openrouter ids do not overlap).\n */\n _videoProvider?: \"openrouter\" | \"veo\" | \"grok\" | \"byteplus\";\n [key: string]: unknown;\n}\n\n/**\n * aimock's normalized request function tool. The OpenAI Responses adapter\n * keeps only top-level `function` tools here by default. With\n * `responsesTools: \"extended\"` it also adds the function tools inside a\n * `namespace` tool (with `namespace` set) and those from `additional_tools` /\n * `tool_search_output` input items.\n */\nexport interface ToolDefinition {\n type: \"function\";\n function: { name: string; description?: string; parameters?: object };\n /** OpenAI Responses namespace the tool was offered in (extended mode only). */\n namespace?: string;\n}\n\n/**\n * An OpenAI Responses `custom` (freeform) tool offered by a request. See\n * {@link ChatCompletionRequest.customTools}.\n */\nexport interface CustomToolDefinition {\n type: \"custom\";\n name: string;\n description?: string;\n /** OpenAI Responses namespace the tool was offered in, when inside a `namespace` tool. */\n namespace?: string;\n /** Custom-tool input format (grammar or text), carried verbatim. */\n format?: unknown;\n}\n\n// Fixture matching\n\nexport interface FixtureMatch {\n userMessage?: string | RegExp;\n /**\n * Substring, regexp, or array of substrings matched against the concatenated\n * text content of every `system` role message in the request. Gates fixture\n * activation on values the host plumbs in via system messages (agent\n * context, persona, dynamic config) instead of the user-typed prompt — so\n * changing context state in the calling app causes stale fixtures to fall\n * through to a real upstream instead of silently returning a baked response\n * that no longer reflects reality.\n *\n * When given an array of strings, ALL substrings must be present (AND\n * semantics). Useful when the gate must combine multiple non-adjacent\n * tokens — e.g., a default name AND a default activity list whose JSON\n * positions vary across requests.\n */\n systemMessage?: string | string[] | RegExp;\n inputText?: string | RegExp;\n toolCallId?: string;\n /**\n * Substring matched against the text content of the LAST message when that\n * message is a `tool` result (same last-message rule as `toolCallId`).\n * Discriminates fixtures whose requests differ ONLY inside the tool-result\n * payload — e.g. a human-in-the-loop tool where approve and cancel resume\n * with the same toolCallId but different result JSON.\n */\n toolResultContains?: string;\n toolName?: string;\n /**\n * Exact OpenAI Responses tool namespace. Alone, it matches when any offered\n * tool sits in this namespace; with `toolName`, a single offered tool must\n * carry both the name and the namespace. Applies only when the server runs\n * with `responsesTools: \"extended\"`; otherwise it is ignored, as in 1.44.0.\n */\n toolNamespace?: string;\n model?: string | RegExp;\n responseFormat?: string;\n predicate?: (req: ChatCompletionRequest) => boolean;\n /** Which occurrence of this match to respond to (0-indexed). Undefined means match any. */\n sequenceIndex?: number;\n turnIndex?: number;\n /**\n * Turn-scoped tool-result presence gate: matches on whether the CURRENT turn\n * (the messages after the last `role: \"user\"` message) contains a `role: \"tool\"`\n * message. Scoping to the current turn — rather than the whole conversation —\n * lets a leg-1 (`false`) / leg-2 (`true`) fixture pair keep discriminating on\n * later turns whose history still carries earlier turns' tool results. If the\n * request has no user message, it falls back to scanning the whole conversation.\n */\n hasToolResult?: boolean;\n endpoint?:\n | \"chat\"\n | \"image\"\n | \"speech\"\n | \"transcription\"\n | \"translation\"\n | \"video\"\n | \"embedding\"\n | \"audio-gen\"\n | \"elevenlabs-tts\"\n | \"elevenlabs-voice-design\"\n | \"elevenlabs-voice\"\n | \"elevenlabs-voice-get\"\n | \"elevenlabs-voice-delete\"\n | \"fal-audio\"\n | \"fal\"\n | \"realtime\"\n | \"realtime-transcription\"\n | \"realtime-translation\"\n | \"openai-live\";\n context?: string;\n}\n\n// Fixture response types\n\n/**\n * Fields that override auto-generated envelope values in the built response.\n * Scalar fields (finishReason, role) use OpenAI-canonical values — provider\n * handlers translate automatically. For usage, provide field names native to\n * your target provider (OpenAI Chat: prompt_tokens, completion_tokens;\n * Responses API: input_tokens, output_tokens; Anthropic: input_tokens,\n * output_tokens; Gemini: promptTokenCount, candidatesTokenCount).\n *\n * When total_tokens (or provider equivalent) is omitted, it is auto-computed\n * from the component fields.\n *\n * Provider support: seven fields are common (id, created, model, usage,\n * systemFingerprint, finishReason, role); provider and nativeFinishReason are\n * OpenRouter-only and ignored elsewhere. OpenAI Chat honors the seven common\n * fields; OpenRouter additionally honors provider/nativeFinishReason; Responses\n * API (5: no role, systemFingerprint); Claude (5: no created,\n * systemFingerprint); Gemini (2: only finishReason, usage).\n */\nexport interface ResponseOverrides {\n id?: string;\n created?: number;\n model?: string;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n totalTokenCount?: number;\n /**\n * OpenRouter per-request cost (USD/credits). Fixture-scriptable — powers\n * budget-guard demos (\"assert the guard trips at $5 without spending $5\").\n * Emitted on OpenRouter-shaped responses only, and ONLY when supplied —\n * `cost`/`cost_details` are nullable and omitted by default (never 0).\n */\n cost?: number;\n /**\n * OpenRouter cost breakdown. When the response emits `cost_details` at all\n * it MUST carry all three upstream fields (the canonical `@openrouter/sdk`\n * throws on a partial object); a fixture may supply any subset and the rest\n * default to 0. Only emitted when the fixture supplies a `cost`.\n */\n cost_details?: {\n upstream_inference_cost?: number;\n upstream_inference_prompt_cost?: number;\n upstream_inference_completions_cost?: number;\n [key: string]: unknown;\n };\n /**\n * OpenRouter prompt-token breakdown. Emitted ONLY when overridden — the\n * default response never asserts a fake cache state.\n */\n prompt_tokens_details?: {\n cached_tokens?: number;\n cache_write_tokens?: number;\n audio_tokens?: number;\n [key: string]: unknown;\n };\n /**\n * OpenRouter completion-token breakdown. Emitted ONLY when overridden.\n */\n completion_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };\n /**\n * OpenRouter bring-your-own-key flag on the usage object. Emitted ONLY when\n * overridden (avoids asserting a fake BYOK state by default).\n */\n is_byok?: boolean;\n /**\n * Forward-compat escape hatch for provider usage fields aimock does not\n * model (OpenRouter `native_tokens_prompt` / `native_tokens_completion`,\n * future breakdowns, …). The recorder persists such fields when an upstream\n * usage frame carries them, and OpenRouter-shaped replays pass any extra key\n * through verbatim onto the emitted `usage` object. The load-time validator\n * still requires extra scalars to be numbers (see fixture-loader.ts), so a\n * typo'd field is caught rather than silently emitted as a string.\n */\n [key: string]: unknown;\n };\n systemFingerprint?: string;\n finishReason?: string;\n role?: string;\n /**\n * OpenRouter top-level `provider` override. When omitted, OpenRouter-shaped\n * responses default it to the author segment of the winning model slug\n * (`openai/gpt-4o` → `\"openai\"`). Ignored for OpenAI (`/v1/...`) responses.\n */\n provider?: string;\n /**\n * OpenRouter `native_finish_reason` override (the raw upstream finish reason).\n * When omitted, OpenRouter-shaped responses mirror `finish_reason`. Ignored\n * for OpenAI responses.\n */\n nativeFinishReason?: string;\n}\n\nexport interface TextResponse extends ResponseOverrides {\n content: string;\n reasoning?: string;\n /**\n * The real cryptographic `signature` captured from a recorded Anthropic\n * thinking turn. When present it is emitted on the replayed thinking block;\n * otherwise replay falls back to aimock's round-trip-safe placeholder.\n * Persisted only alongside a non-empty `reasoning`: a signature captured\n * without plaintext reasoning (e.g. whitespace-only thinking) is intentionally\n * discarded at the persistence layer, since replay attaches signatures only to\n * plaintext thinking blocks.\n */\n reasoningSignature?: string;\n /**\n * Opaque `data` payload(s) of any Anthropic `redacted_thinking` blocks\n * captured from a recorded thinking turn, in stream order. When present they\n * are replayed as faithful `redacted_thinking` content blocks so the encrypted\n * reasoning round-trips; absent for non-Anthropic providers and turns without\n * redacted thinking. Recorded redacted blocks replay as a leading group: the\n * original interleaving of `thinking` and `redacted_thinking` blocks is not\n * preserved (see the CHANGELOG fidelity caveat).\n */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ToolCall {\n name: string;\n arguments: string;\n id?: string;\n /**\n * OpenAI Responses API only: emitted as `function_call.namespace` when the\n * server runs with `responsesTools: \"extended\"`. Ignored otherwise, and by\n * every other wire.\n */\n namespace?: string;\n}\n\n/**\n * An OpenAI Responses custom (freeform) tool call, emitted as a\n * `custom_tool_call` item. Fixtures carry it in `customToolCalls` (or as a\n * `customToolCall` block in `responsesBlocks`). Only the Responses API (HTTP\n * and WebSocket) can serve it; every other wire rejects it with\n * `UnsupportedToolCallError`.\n */\nexport interface CustomToolCall {\n type: \"custom\";\n name: string;\n /** Free-text input. Never parsed, never stringified. */\n input: string;\n id?: string;\n /** OpenAI Responses API only: emitted as `custom_tool_call.namespace`. */\n namespace?: string;\n}\n\n/**\n * A tool call the OpenAI Responses API serves: a function call (from\n * `toolCalls`) or a custom tool call (from `customToolCalls`).\n */\nexport type FixtureToolCall = (ToolCall & { type?: undefined }) | CustomToolCall;\n\n/**\n * A single ordered streaming block for a {@link ContentWithToolCallsResponse}.\n *\n * When a combined content+toolCalls fixture sets the optional `blocks` field,\n * builders stream the blocks in array order — enabling tool-call-before-text\n * and interleaved orderings that the legacy `{ content, toolCalls }` shape\n * (always text-first) cannot express. A `text` block carries a text segment; a\n * `toolCall` block mirrors {@link ToolCall} (`name` + JSON-string `arguments`,\n * optional `id`).\n */\nexport type FixtureBlock =\n | { type: \"text\"; text: string }\n | { type: \"toolCall\"; name: string; arguments: string; id?: string; namespace?: string };\n\n/**\n * An ordered block for {@link ContentWithToolCallsResponse.responsesBlocks}:\n * a {@link FixtureBlock}, or a `customToolCall` block that mirrors\n * {@link CustomToolCall} (`name` + free-text `input`, optional `id` and\n * `namespace`). Only the OpenAI Responses API serves `responsesBlocks`.\n */\nexport type ResponsesFixtureBlock =\n | FixtureBlock\n | { type: \"customToolCall\"; name: string; input: string; id?: string; namespace?: string };\n\nexport interface ToolCallResponse extends ResponseOverrides {\n toolCalls: ToolCall[];\n /**\n * OpenAI Responses custom tool calls, emitted after `toolCalls`. A turn with\n * only custom calls sets `toolCalls: []`. Every other wire rejects a fixture\n * with a non-empty `customToolCalls` (`UnsupportedToolCallError`). Applies\n * only with `responsesTools: \"extended\"`; otherwise it is ignored.\n */\n customToolCalls?: CustomToolCall[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ContentWithToolCallsResponse extends ResponseOverrides {\n /**\n * Combined-turn text. Required for the legacy `{ content, toolCalls }` shape\n * and the combined `{ content, toolCalls, blocks }` shape. OPTIONAL only for a\n * BLOCKS-ONLY fixture (`{ blocks: [...] }` with no `content`/`toolCalls`),\n * where the ordered `blocks` array is the sole source of streamed output and\n * the legacy text-first builder branch is never taken. The runtime guard\n * `isContentWithToolCallsResponse` enforces this: it matches when BOTH\n * `content` + `toolCalls` are present (legacy/combined) OR when a non-empty\n * `blocks` array is present (blocks-only).\n */\n content?: string;\n /** See {@link ContentWithToolCallsResponse.content} — optional only for the blocks-only shape. */\n toolCalls?: ToolCall[];\n /** OpenAI Responses custom tool calls; see {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: CustomToolCall[];\n /**\n * Optional ordered streaming blocks. When present, builders stream these in\n * array order (tool-first / interleaved); when absent, the legacy\n * `{ content, toolCalls }` text-first path runs unchanged. Purely additive.\n * A non-empty `blocks` array also makes this a FIRST-CLASS blocks-only\n * response even without `content`/`toolCalls` (see those fields above).\n */\n blocks?: FixtureBlock[];\n /**\n * OpenAI Responses only: ordered blocks that may include `customToolCall`\n * blocks. On the Responses API they replace `blocks`; a fixture may not set\n * both. Every other wire rejects a `customToolCall` block here\n * (`UnsupportedToolCallError`) and otherwise ignores this field. Applies\n * only with `responsesTools: \"extended\"`; otherwise it is ignored.\n */\n responsesBlocks?: ResponsesFixtureBlock[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ErrorResponse {\n error: {\n message: string;\n type?: string;\n param?: string | null;\n code?: string;\n /**\n * OpenRouter error metadata. Emitted verbatim inside the OpenRouter error\n * envelope (`{ error: { code, message, metadata } }`) when the request\n * arrived on the OpenRouter base. Moderation blocks carry\n * `{ reasons, flagged_input, provider_name, model_slug }` on a 403.\n * Ignored for OpenAI-shaped error responses.\n */\n metadata?: Record<string, unknown>;\n };\n status?: number;\n /** Override the Retry-After header value on 429 responses. Default: 1. */\n retryAfter?: number;\n /**\n * OpenRouter `models[]` failover eligibility for THIS error class. Real\n * OpenRouter fails over INCONSISTENTLY by error class — a `403 budget-exceeded`\n * or a generic \"provider returned error\" is served as terminal (it does NOT\n * advance to the next `models[]` candidate), while 429/503 usually do fail\n * over (openclaw #60191). This flag lets a fixture reproduce that:\n * - absent / `true` (default) — the error candidate FALLS THROUGH to the next\n * `models[]` candidate (backward-compatible; existing fixtures unchanged).\n * - `false` — the error is TERMINAL: the fallback loop stops and serves this\n * error with no failover, even if a later candidate would match.\n *\n * Composes with the request-level `provider.allow_fallbacks`: fall-through\n * happens only when BOTH allow it (if EITHER says don't, don't). Only\n * consulted for OpenRouter `models[]` fallback; ignored elsewhere.\n */\n fallthrough?: boolean;\n}\n\nexport interface EmbeddingResponse {\n embedding: number[];\n}\n\nexport interface ImageItem {\n url?: string;\n b64Json?: string;\n revisedPrompt?: string;\n}\n\nexport interface ImageResponse {\n image?: ImageItem;\n images?: ImageItem[];\n}\n\n// ORDERING CONTRACT: audio fixtures MUST be discriminated by `isAudioResponse`\n// BEFORE the `isContentWithToolCallsResponse` / `isToolCallResponse` / text\n// guards, because the optional companion fields below make these shapes\n// structurally overlap (an AudioResponse with `toolCalls`/`content` would also\n// satisfy those guards otherwise).\nexport interface VoiceDesignPreview {\n generated_voice_id: string;\n // Optional like its siblings below: `voiceDesignToJson` substitutes `\"\"`\n // when it is absent, so requiring it here made that documented fallback\n // unreachable from TypeScript.\n audio_base_64?: string;\n media_type?: string;\n duration_secs?: number;\n language?: string | null;\n}\n\n/**\n * Convenience shape for `LLMock.onElevenLabsVoiceDesign`. Stored as a\n * {@link RawJSONResponse} wrapping `{ previews, text }` — the ElevenLabs\n * `/v1/text-to-voice/design` envelope.\n */\nexport interface VoiceDesignResponse {\n previews: VoiceDesignPreview[];\n text?: string;\n}\n\nexport interface AudioResponse {\n audio: string | { b64Json: string; contentType?: string };\n format?: string;\n /**\n * Companion modalities that can accompany streamed audio. A single Gemini turn\n * may interleave inlineData audio with a functionCall and/or text/thought\n * parts; the recorder preserves them here so the tool call / content / reasoning\n * are not silently discarded when audio is also present.\n */\n toolCalls?: ToolCall[];\n content?: string;\n reasoning?: string;\n}\n\nexport interface TranscriptionResponse {\n transcription: {\n text: string;\n language?: string;\n languages?: Array<{ code: string }>;\n duration?: number;\n usage?: Record<string, unknown>;\n words?: Array<{ word: string; start: number; end: number }>;\n segments?: Array<{ id: number; text: string; start: number; end: number }>;\n };\n}\n\nexport interface VideoResponse {\n video: {\n id: string;\n status: \"processing\" | \"completed\" | \"failed\";\n url?: string;\n /** Failure message surfaced by async video jobs (e.g. OpenRouter `error`). */\n error?: string;\n /** Base64-encoded video bytes served by content-download endpoints. */\n b64?: string;\n /** Generation cost surfaced in usage envelopes (e.g. OpenRouter `usage.cost`). */\n cost?: number;\n /** Clip duration in seconds surfaced by some providers (e.g. Grok `video.duration`). */\n duration?: number;\n };\n}\n\n/**\n * Pass-through JSON response. Used by handlers (e.g. fal.ai) that record\n * arbitrary upstream JSON payloads and replay them verbatim, without the\n * provider-specific shape coercion the other response types impose.\n */\nexport interface RawJSONResponse extends ResponseOverrides {\n json: unknown;\n status?: number;\n /**\n * Billed quantity surfaced on the completed fal `queue-result` response via\n * the `x-fal-billable-units` header (emitted alongside `x-fal-request-id` on\n * the completed result). Real fal sets this header on its queue responses;\n * recent versions of `@tanstack/ai-fal` read it to populate a consumer-side\n * billed-units field (e.g. `usage.unitsBilled`). Omit it to preserve the\n * header-less default — present only to let a fixture opt into exercising a\n * consumer's cost/billing accounting path on replay.\n */\n billableUnits?: number;\n}\n\nexport type FixtureResponse =\n | TextResponse\n | ToolCallResponse\n | ContentWithToolCallsResponse\n | ErrorResponse\n | EmbeddingResponse\n | ImageResponse\n | AudioResponse\n | TranscriptionResponse\n | VideoResponse\n | RawJSONResponse\n | LiveFixtureResponse;\n\n// GA Realtime session types\n\nexport type RealtimePhase = \"final_answer\" | \"commentary\";\n\nexport interface GASessionAudioConfig {\n voice: string | null;\n input_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;\n output_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;\n input_audio_noise_reduction: { type: string } | null;\n input_audio_transcription: { model: string } | null;\n}\n\nexport interface GASessionConfig {\n model: string;\n modalities: string[];\n instructions: string;\n tools: unknown[];\n temperature: number;\n max_response_output_tokens: number | \"inf\";\n audio: GASessionAudioConfig;\n turn_detection: unknown | null;\n input_audio_transcription: { model: string } | null;\n type: \"conversation\" | \"transcription\" | \"translation\";\n reasoning: { effort: string } | null;\n}\n\n// Streaming physics\n\nexport interface StreamingProfile {\n ttft?: number; // Time to first token (ms)\n tps?: number; // Tokens per second\n jitter?: number; // Random variance factor (0-1), default 0\n}\n\n/**\n * Per-frame arrival timestamps captured during proxy recording.\n * Used during replay to reproduce real-world streaming timing instead of\n * the synthetic model (StreamingProfile / flat latency).\n */\nexport interface RecordedTimings {\n ttftMs: number;\n interChunkDelaysMs: number[];\n totalDurationMs: number;\n}\n\n/**\n * Probabilistic chaos injection rates.\n *\n * Rates are evaluated sequentially per request — drop → malformed → rateLimit\n * → disconnect — as four separate draws in that order, and the first draw that\n * hits wins and short-circuits the rest. Each rate is therefore the probability\n * that its OWN draw hits GIVEN that no earlier rate fired, not the share of\n * requests that end in that fault; only `dropRate`, first in the chain, is\n * unconditional. A config of `{ dropRate: 0.5, malformedRate: 0.5 }` yields a\n * ~25 % effective malformed rate, not 50 %, and the same compounding applies to\n * `{ dropRate: 0.5, rateLimitRate: 0.5 }` (~25 % rate limits).\n */\n/**\n * Probabilistic (and one deterministic) fault injection knobs.\n *\n * VALIDATION — ONE policy, whatever the source. Each field is a finite number\n * in its range: the rates in [0, 1], `latencyMs` in [0, 30000]. A value outside\n * it, or one that is not a number at all (`\"0.5abc\"`, `\"Infinity\"`, `NaN`), is\n * REJECTED, never clamped — silent clamping substitutes a fault rate nobody\n * asked for and hides the misconfiguration that produced it.\n *\n * Rejection is reported in the loudest way each source allows, and the three\n * agree: `POST /__aimock/chaos` answers 400, the `--chaos-*` CLI flags refuse\n * to start, and a per-request header or a fixture/server value is refused with\n * a `[chaos]` warning and left unset so the next level of precedence (header >\n * fixture > server) applies.\n */\nexport interface ChaosConfig {\n dropRate?: number;\n malformedRate?: number;\n disconnectRate?: number;\n /**\n * Deterministic delay (ms) injected before the request is handled.\n * Applied when > 0 — header > fixture > server precedence, same as rates.\n * Unlike rates it is not probabilistic: a set latency always fires so\n * timeout/retry suites get a stable signal. Must be in [0, 30000]; a value\n * outside that range is rejected, not clamped (see above).\n */\n latencyMs?: number;\n /**\n * Chance of a 429 rate-limit rejection with `Retry-After`, CONDITIONAL on\n * neither `dropRate` nor `malformedRate` having fired first: it is the third\n * draw in the drop → malformed → rateLimit → disconnect order, and the first\n * hit wins. Paired with a 0.5 `dropRate` a 0.5 `rateLimitRate` produces ~25 %\n * rate limits, not 50 % (see the interface docstring above).\n */\n rateLimitRate?: number;\n}\n\n/**\n * Server-level chaos as the handlers see it: a baseline plus per-`X-Test-Id`\n * overrides installed at runtime via `POST /__aimock/chaos`.\n *\n * Chaos is the LAST link in the precedence chain (request headers > fixture >\n * server), and this makes that last link test-scoped, the way every other\n * mutable axis in aimock is (fixture match-counts, video job maps). Under the\n * shipped topology — one shared `aimock` process serving a parallel suite — a\n * purely global switch would mean one test's chaos failing every other test.\n *\n * Scoping is by testId on BOTH sides: the traffic being evaluated and the\n * control request that installed the override. Both resolve the tag the way\n * `getTestId` does — the `X-Test-Id` header, else `?testId=` in the query\n * string — so a harness that can only tag one of those two ways still lands in\n * the same scope on both sides, and the two can never silently disagree.\n */\nexport interface ChaosScope {\n /** Server-wide baseline: the construction config, or the untagged override. */\n base?: ChaosConfig;\n /**\n * Per-testId overrides, consulted INSTEAD OF `base` — never merged over it.\n *\n * An installed override REPLACES the baseline wholesale for that testId: a\n * server started with `--chaos-latency 500` whose test `t1` installs\n * `{ dropRate: 1 }` gives `t1` traffic a drop and NO latency, because\n * `latencyMs` is not restated. To keep a baseline field, restate it in the\n * override body.\n *\n * Replacement — not a field-wise merge — is deliberate, and it is what makes\n * `POST /__aimock/chaos {}` (\"explicitly no chaos for my test\") different\n * from `DELETE /__aimock/chaos` (\"forget I said anything, fall back to the\n * baseline\"). Under a merge, `POST {}` would be a no-op and that distinction\n * would collapse. It also matches the untagged case, where an override\n * likewise replaces the construction-time config rather than layering on it.\n *\n * The two chain links ABOVE this one — fixture chaos over server chaos, and\n * request headers over both — DO merge field-wise (see `resolveChaosConfig`).\n * Only the scope lookup replaces, because it selects a config rather than\n * refining one. `GET /__aimock/chaos` with the same testId always reports the\n * config actually in effect for that scope, so the replacement is auditable.\n */\n byTestId?: ReadonlyMap<string, ChaosConfig>;\n}\n\n/**\n * What `evaluateChaos` / `applyChaos` accept as server defaults. A plain\n * `ChaosConfig` still works (it is what external callers pass); the server\n * itself passes a `ChaosScope`.\n */\nexport type ChaosDefaults = ChaosConfig | ChaosScope;\n\nexport type ChaosAction = \"drop\" | \"malformed\" | \"rateLimit\" | \"disconnect\";\n\n// Response factory — allows dynamic fixture responses based on the incoming request\n\nexport type ResponseFactory = (\n req: ChatCompletionRequest,\n) => FixtureResponse | Promise<FixtureResponse>;\n\n// Fixture\n\n/** Provider wire identifiers, independent of endpoint response families. */\nexport type WireId =\n | \"openai-chat\"\n | \"openai-responses\"\n | \"openai-realtime\"\n | \"anthropic\"\n | \"bedrock-invoke\"\n | \"bedrock-converse\"\n | \"gemini\"\n | \"gemini-live\"\n | \"gemini-interactions\"\n | \"cohere\"\n | \"ollama\";\n\ninterface MisbehaviorCommon {\n rate?: number;\n times?: number;\n tool?: string;\n providers?: WireId[];\n}\n\nexport type MisbehaviorFault = MisbehaviorCommon &\n (\n | { fault: \"tool-args-invalid-json\"; style?: \"truncated\" | \"trailing-comma\" | \"single-quotes\" }\n | {\n fault: \"tool-args-schema-violation\";\n violation?:\n | \"missing-required\"\n | \"wrong-type\"\n | \"extra-property\"\n | \"enum-mismatch\"\n | \"not-object\";\n property?: string;\n }\n | { fault: \"tool-unknown-name\"; name?: string }\n | { fault: \"tool-call-id-duplicate\" }\n | { fault: \"stop-length-mid-tool\"; at?: number }\n | { fault: \"empty-response\" }\n | { fault: \"refusal\"; message?: string; category?: string | null }\n | { fault: \"content-filter\" }\n | { fault: \"reasoning-only\"; reasoning?: string }\n );\n\nexport type MisbehaviorFaultId = MisbehaviorFault[\"fault\"];\nexport interface MisbehaviorConfig {\n seed?: number | \"random\";\n faults: MisbehaviorFault[];\n}\n\nexport interface MisbehaviorCounterKey {\n testId: string;\n sourceKey: string;\n entryIndex: number;\n}\nexport interface MisbehaviorCounters {\n getFiringCount(key: MisbehaviorCounterKey): number;\n nextOrdinal(key: MisbehaviorCounterKey): number;\n recordFiring(key: MisbehaviorCounterKey): void;\n clearMisbehaviorCounters(testId?: string): void;\n}\nexport interface MisbehaviorScope {\n baseline?: MisbehaviorConfig;\n byTestId: Map<string, MisbehaviorConfig>;\n}\n\nexport interface Fixture {\n match: FixtureMatch;\n response: FixtureResponse | ResponseFactory;\n latency?: number;\n chunkSize?: number;\n truncateAfterChunks?: number;\n disconnectAfterMs?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /**\n * Opt into OpenRouter's `: OPENROUTER PROCESSING` SSE keepalive comment\n * lines on this fixture's streamed OpenRouter responses (default OFF).\n * A faithful client skips `:`-prefixed lines before JSON-parsing; enabling\n * this exercises that path. No effect on non-streaming or OpenAI responses.\n */\n openRouterProcessing?: boolean;\n metadata?: {\n systemHash?: string;\n toolsHash?: string;\n };\n}\n\nexport type FixtureOpts = Omit<Fixture, \"match\" | \"response\">;\nexport type EmbeddingFixtureOpts = Pick<FixtureOpts, \"latency\" | \"chaos\">;\n/**\n * Options for the fal queue/run fixture builders. Adds `billableUnits`, which\n * is emitted as the `x-fal-billable-units` header on the completed\n * `queue-result` response (see {@link RawJSONResponse.billableUnits}).\n */\nexport type FalQueueOpts = FixtureOpts & { billableUnits?: number };\n\n// Fixture file format (JSON on disk)\n//\n// File-entry types are intentionally relaxed compared to their runtime\n// counterparts so that fixture authors can write JSON objects where the\n// API ultimately expects a JSON *string*. The fixture loader auto-\n// stringifies these before building the runtime Fixture.\n\nexport interface FixtureFileToolCall {\n name: string;\n /** Accepts a JSON object or array for convenience — the loader will JSON.stringify it. */\n arguments: string | Record<string, unknown> | unknown[];\n id?: string;\n /** OpenAI Responses namespace; see {@link ToolCall.namespace}. */\n namespace?: string;\n}\n\n/** On-disk counterpart of {@link CustomToolCall} (`customToolCalls` entries). */\nexport interface FixtureFileCustomToolCall {\n /** Optional in a file: every `customToolCalls` entry is a custom call. */\n type?: \"custom\";\n name: string;\n /** Free-text input, never parsed or stringified. */\n input: string;\n id?: string;\n namespace?: string;\n}\n\n/**\n * On-disk counterpart of {@link FixtureBlock}. A `toolCall` block's\n * `arguments` is relaxed exactly like {@link FixtureFileToolCall} so authors\n * may write a JSON object/array; the loader JSON.stringifies it into the\n * runtime string form. Normalizes to a {@link FixtureBlock}.\n */\nexport type FixtureFileBlock =\n | { type: \"text\"; text: string }\n | {\n type: \"toolCall\";\n name: string;\n /** Accepts a JSON object or array for convenience — the loader will JSON.stringify it. */\n arguments: string | Record<string, unknown> | unknown[];\n id?: string;\n namespace?: string;\n };\n\n/** On-disk counterpart of {@link ResponsesFixtureBlock} (`responsesBlocks` entries). */\nexport type FixtureFileResponsesBlock =\n | FixtureFileBlock\n | { type: \"customToolCall\"; name: string; input: string; id?: string; namespace?: string };\n\nexport interface FixtureFileToolCallResponse extends ResponseOverrides {\n toolCalls: FixtureFileToolCall[];\n /** See {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: FixtureFileCustomToolCall[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface FixtureFileTextResponse extends ResponseOverrides {\n /** Accepts a JSON object or array (structured output) — the loader will JSON.stringify it. */\n content: string | Record<string, unknown> | unknown[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface FixtureFileContentWithToolCallsResponse extends ResponseOverrides {\n /**\n * Accepts a JSON object or array (structured output) — the loader will\n * JSON.stringify it. OPTIONAL only for a BLOCKS-ONLY fixture (mirrors\n * {@link ContentWithToolCallsResponse.content}); required for the\n * legacy/combined shapes.\n */\n content?: string | Record<string, unknown> | unknown[];\n /** See {@link FixtureFileContentWithToolCallsResponse.content} — optional only for the blocks-only shape. */\n toolCalls?: FixtureFileToolCall[];\n /** See {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: FixtureFileCustomToolCall[];\n /**\n * Optional ordered streaming blocks (mirrors the in-memory\n * {@link ContentWithToolCallsResponse.blocks}). When present, builders stream\n * these in array order (tool-first / interleaved); a `toolCall` block's\n * object `arguments` is auto-stringified just like `toolCalls[].arguments`.\n * Absent → legacy text-first path runs unchanged. Purely additive. Uses the\n * on-disk {@link FixtureFileBlock} shape with relaxed `arguments`. A non-empty\n * `blocks` array alone makes this a first-class blocks-only fixture.\n */\n blocks?: FixtureFileBlock[];\n /** See {@link ContentWithToolCallsResponse.responsesBlocks}. */\n responsesBlocks?: FixtureFileResponsesBlock[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport type FixtureFileResponse =\n | FixtureFileTextResponse\n | FixtureFileToolCallResponse\n | FixtureFileContentWithToolCallsResponse\n | ErrorResponse\n | EmbeddingResponse\n | ImageResponse\n | AudioResponse\n | TranscriptionResponse\n | VideoResponse\n | RawJSONResponse\n | LiveFixtureResponse;\n\nexport interface FixtureFile {\n fixtures: FixtureFileEntry[];\n}\n\nexport interface FixtureFileEntry {\n match: {\n userMessage?: string;\n /**\n * String (single substring) or array of strings (all must be present).\n * Mirrors the runtime FixtureMatch.systemMessage but without the RegExp\n * form, which JSON cannot express.\n */\n systemMessage?: string | string[];\n inputText?: string;\n toolCallId?: string;\n /**\n * Substring matched against the last tool-result message's text content.\n * Mirrors the runtime FixtureMatch.toolResultContains.\n */\n toolResultContains?: string;\n toolName?: string;\n /** Mirrors the runtime FixtureMatch.toolNamespace. */\n toolNamespace?: string;\n model?: string;\n responseFormat?: string;\n sequenceIndex?: number;\n turnIndex?: number;\n /**\n * Turn-scoped tool-result presence gate: matches on whether the CURRENT turn\n * (the messages after the last `role: \"user\"` message) contains a `role: \"tool\"`\n * message, not the whole conversation. Falls back to scanning the whole\n * conversation when the request has no user message. Mirrors the runtime\n * `FixtureMatch.hasToolResult`.\n */\n hasToolResult?: boolean;\n endpoint?:\n | \"chat\"\n | \"image\"\n | \"speech\"\n | \"transcription\"\n | \"translation\"\n | \"video\"\n | \"embedding\"\n | \"audio-gen\"\n | \"elevenlabs-tts\"\n | \"elevenlabs-voice-design\"\n | \"elevenlabs-voice\"\n | \"elevenlabs-voice-get\"\n | \"elevenlabs-voice-delete\"\n | \"fal-audio\"\n | \"fal\"\n | \"realtime\"\n | \"realtime-transcription\"\n | \"realtime-translation\"\n | \"openai-live\";\n context?: string;\n // predicate not supported in JSON files\n };\n response: FixtureFileResponse;\n latency?: number;\n chunkSize?: number;\n truncateAfterChunks?: number;\n disconnectAfterMs?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /** See {@link Fixture.openRouterProcessing}. */\n openRouterProcessing?: boolean;\n metadata?: {\n systemHash?: string;\n toolsHash?: string;\n };\n}\n\n// Request journal\n\n/**\n * A recorded request body.\n *\n * Most journal entries carry a chat-completion request, but every service\n * journals through one entry type and several of them record a body that is\n * not one: the fine-tuning create payload is recorded verbatim so the journal\n * reports what the caller actually sent, and the journal's own size cap\n * substitutes a truncation marker for an oversized body. Neither shape has a\n * `model`/`messages` pair, so typing the field as `ChatCompletionRequest`\n * alone forces those writers through a cast that claims fields that are not\n * there — the second member exists so they do not have to lie.\n *\n * TypeScript collapses the two for ASSIGNMENT (`ChatCompletionRequest` has an\n * index signature, so it is itself a `Record<string, unknown>`); the union is\n * kept two-membered because it is what the field means, not because it narrows\n * anything. For READING, treat the body as opaque JSON — that is already how\n * every consumer uses it (the journal cap re-serializes it, `GET\n * /__aimock/journal` hands it back) — and property-check before reaching for a\n * chat-request field.\n */\nexport type JournalBody = ChatCompletionRequest | Record<string, unknown>;\n\nexport interface JournalEntry {\n id: string;\n timestamp: number;\n method: string;\n path: string;\n headers: Record<string, string>;\n body: JournalBody | null;\n service?: string;\n /**\n * Resolved test id of an MCP request (`\"__default__\"` when none), for an\n * MCP mount to set. Journal test-id filtering does not read it: it\n * re-derives the id from the headers and path (`journalEntryTestId`).\n */\n testId?: string;\n /** Resolved context of an MCP request, `null` when none; for an MCP mount to set. */\n context?: string | null;\n response: {\n status: number;\n fixture: Fixture | null;\n /** For an MCP mount to set on `tools/call` entries a fake answered or failed. */\n mcpFake?: { id: string | null; outcome: McpFakeOutcome };\n /**\n * What was going to serve this request. \"fixture\" = a fixture matched (or\n * would have, before chaos intervened). \"proxy\" = no fixture matched and\n * proxy was configured. \"internal\" = entries where the request was served\n * by aimock's own synthetic logic — neither a matched fixture nor a\n * configured proxy: chaos-path entries (e.g. chaos on the OpenRouter\n * video lifecycle endpoints; in REPLAY mode their normal 200/400/401/404\n * entries omit source, while record-mode 200s on those endpoints carry\n * source:\"proxy\"), the OpenRouter video models listing synthesized as the\n * fallback after a FAILED proxy attempt, and the services whose responses\n * are synthesized outright — every fine-tuning entry carries it, success\n * and error alike, since that store is aimock's own and no fixture or\n * proxy ever serves it. Absent when the distinction doesn't apply\n * (e.g. 404/503 fallback where nothing was going to serve).\n */\n source?: \"fixture\" | \"proxy\" | \"internal\";\n interrupted?: boolean;\n interruptReason?: string;\n /**\n * The error message of a failed request, set in two cases:\n * - The request failed before any content was sent, and `status` is 500:\n * a fixture tool-call error (HTTP or WebSocket), or any builder error on\n * the WebSocket Responses transport.\n * - The handler crashed AFTER the response completed. The client received\n * `status` in full, so this is not an interruption.\n */\n error?: string;\n chaosAction?: ChaosAction;\n /** Full prepared fault output; interrupted delivery is recorded separately above. */\n misbehavior?: MisbehaviorSummary;\n /** When the X-AIMock-Strict header overrode the server default. */\n strictOverride?: boolean;\n /** MR8 / MR11 / MR14: why a forwarded MCP record-mode message was not recorded. */\n recordSkipped?: string;\n };\n}\n\n// SSE chunk types (OpenAI format)\n\n/**\n * OpenRouter-only additions layered onto a `usage` object when a response is\n * shaped for the OpenRouter `/api/v1/...` base. `cost`/`cost_details` are\n * NULLABLE and emitted only when a fixture supplies a cost (default: omitted).\n * When `cost_details` is emitted it carries all three upstream fields (partial\n * objects break the canonical SDK). The detail breakdowns and `is_byok` appear\n * only when a fixture overrides them; an emitted `prompt_tokens_details` always\n * carries `cached_tokens` and an emitted `completion_tokens_details` always\n * carries `reasoning_tokens` (required-if-present in the AI SDK provider).\n */\nexport interface OpenRouterUsageExtras {\n cost?: number;\n cost_details?: {\n upstream_inference_cost: number;\n upstream_inference_prompt_cost: number;\n upstream_inference_completions_cost: number;\n [key: string]: unknown;\n };\n prompt_tokens_details?: {\n cached_tokens: number;\n cache_write_tokens?: number;\n audio_tokens?: number;\n [key: string]: unknown;\n };\n completion_tokens_details?: { reasoning_tokens: number; [key: string]: unknown };\n is_byok?: boolean;\n /**\n * Provider usage fields aimock does not model, passed through verbatim from a\n * fixture's `response.usage` (typically written by the recorder from a real\n * upstream usage frame — e.g. OpenRouter `native_tokens_prompt`).\n */\n [key: string]: unknown;\n}\n\nexport interface SSEChunk {\n id: string;\n object: \"chat.completion.chunk\";\n created: number;\n model: string;\n choices: SSEChoice[];\n /** Nullable: OpenRouter shaping emits this on every chunk (null when unset). */\n system_fingerprint?: string | null;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n } & OpenRouterUsageExtras;\n /** OpenRouter top-level provider that served the chunk. OpenRouter shaping only. */\n provider?: string;\n /** OpenRouter emits `service_tier: null` on the final (usage-bearing) chunk. Shaping only. */\n service_tier?: string | null;\n}\n\nexport interface SSEChoice {\n index: number;\n delta: SSEDelta;\n logprobs: null;\n finish_reason: string | null;\n /** OpenRouter raw upstream finish reason, alongside the normalized one. */\n native_finish_reason?: string | null;\n}\n\n/** OpenRouter text reasoning detail, paired with the same logical reasoning text. */\nexport interface ReasoningTextDetail {\n type: \"reasoning.text\";\n text: string;\n format: \"unknown\";\n index: number;\n}\n\nexport interface SSEDelta {\n role?: string;\n content?: string | null;\n reasoning?: string;\n reasoning_details?: ReasoningTextDetail[];\n reasoning_content?: string;\n tool_calls?: SSEToolCallDelta[];\n}\n\nexport interface SSEToolCallDelta {\n index: number;\n id?: string;\n type?: \"function\";\n function?: { name?: string; arguments?: string };\n}\n\n// Non-streaming completion response types (OpenAI format)\n\nexport interface ChatCompletion {\n id: string;\n object: \"chat.completion\";\n created: number;\n model: string;\n choices: ChatCompletionChoice[];\n usage: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n } & OpenRouterUsageExtras;\n /**\n * Nullable: OpenRouter shaping always emits this on the non-streaming\n * response (null when no override) because the canonical `@openrouter/sdk`\n * requires the field. OpenAI responses omit it unless overridden.\n */\n system_fingerprint?: string | null;\n /** OpenRouter top-level provider that served the request. OpenRouter shaping only. */\n provider?: string;\n /** OpenRouter emits `service_tier: null` on the non-streaming response. Shaping only. */\n service_tier?: string | null;\n}\n\nexport interface ChatCompletionChoice {\n index: number;\n message: ChatCompletionMessage;\n logprobs: null;\n finish_reason: string;\n /** OpenRouter raw upstream finish reason, alongside the normalized one. */\n native_finish_reason?: string;\n}\n\nexport interface ChatCompletionMessage {\n role: string;\n content: string | null;\n refusal: string | null;\n reasoning_details?: ReasoningTextDetail[];\n reasoning_content?: string;\n /**\n * OpenRouter emits `reasoning` (null when absent) on the response message.\n * OpenRouter shaping mirrors `reasoning_content` here (or null). Omitted for\n * OpenAI responses.\n */\n reasoning?: string | null;\n tool_calls?: ToolCallMessage[];\n}\n\n// Server options\n\nexport type RecordProviderKey =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"gemini-interactions\"\n | \"vertexai\"\n | \"bedrock\"\n | \"azure\"\n | \"ollama\"\n | \"cohere\"\n | \"elevenlabs\"\n | \"fal\"\n | \"openrouter\"\n | \"veo\"\n | \"grok\"\n | \"byteplus\";\n\nexport interface RecordConfig {\n providers: Partial<Record<RecordProviderKey, string>>;\n /**\n * aimock's own built-in upstream API keys, keyed by provider. Opt-in and\n * backward-compatible: when set for a provider, aimock injects the key on a\n * fixture-miss passthrough IF the caller sent no credential or a dummy\n * placeholder (see `applyProviderAuth`); a real caller key always overrides.\n * Only static-key providers are eligible: OpenAI/OpenRouter/Cohere/Grok/BytePlus/Ollama\n * (bearer), Anthropic (x-api-key), Gemini/Gemini-Interactions/Veo\n * (x-goog-api-key), Azure OpenAI (api-key), ElevenLabs (xi-api-key), and fal\n * (Authorization: Key). Signed/OAuth providers (Bedrock SigV4, Vertex AI\n * OAuth) are never rewritten. Sourced from AIMOCK_PROVIDER_*_KEY env vars by\n * the CLI (secrets stay out of `ps`).\n */\n providerKeys?: Partial<Record<RecordProviderKey, string>>;\n fixturePath?: string;\n /** Proxy unmatched requests without saving fixtures or caching in memory. */\n proxyOnly?: boolean;\n /**\n * When true, record the exact model version string returned by the provider\n * (e.g. \"gpt-4o-2024-08-06\") instead of stripping the date suffix to a\n * canonical alias (e.g. \"gpt-4o\"). Default: false.\n */\n recordFullModelVersion?: boolean;\n /**\n * fal-specific recording knobs for the queue-walk recorder. During recording\n * the queue handler POSTs submit, polls `status_url` until COMPLETED, then\n * GETs `response_url` for the final job body — saved as the fixture. Tune\n * the poll cadence and timeout here if upstream is unusually slow or fast.\n */\n fal?: FalRecordConfig;\n /**\n * OpenRouter-video-specific recording knobs for the live lifecycle proxy on\n * `/api/v1/videos` (submit/poll proxied 1:1; completed jobs are captured\n * eagerly as fixtures).\n */\n openRouterVideo?: OpenRouterVideoRecordConfig;\n /**\n * Connection idle timeout (ms) on the upstream request socket — fires if the\n * socket is inactive for this duration at any point before the response body\n * begins. Default: 30_000 (30s). Increase for upstreams with slow initial\n * responses (reasoning models, queue-backed providers).\n *\n * Nuance on the OpenRouter video surface: the small-JSON lifecycle fetches\n * (submit, status poll, models listing) apply this value as a TOTAL\n * deadline on the whole fetch — indistinguishable from an idle timeout for\n * envelope-sized bodies. The byte-bearing content fetches (eager capture,\n * proxy-only content relay) gate only the response HEADERS on this value;\n * their body progress is governed by `bodyTimeoutMs` idle semantics.\n *\n * Ceiling note: the fetch-based OpenRouter proxy paths run on Node's\n * built-in undici dispatcher, which carries its own ~300s\n * headers/body-timeout defaults — values above that ceiling do not take\n * effect on those paths without installing a custom undici dispatcher\n * (out of aimock's scope).\n */\n upstreamTimeoutMs?: number;\n /**\n * Idle timeout (ms) on the upstream response body — fires if the upstream\n * goes silent (no bytes) for this long after the response has started.\n * Default: 30_000 (30s). Reasoning models under concurrent load can leave\n * 30s+ gaps between streaming chunks while the model is thinking; lift this\n * to e.g. 180_000 in those setups. The OpenRouter video content fetches use\n * the same idle semantics: the timer re-arms on every chunk, so a steadily\n * downloading multi-minute video never times out — only a silent stall does.\n */\n bodyTimeoutMs?: number;\n /**\n * Maximum number of bytes aimock will accumulate in memory from a single\n * proxied upstream response on the record/proxy path. The full response is\n * still relayed to the client byte-for-byte; this cap only bounds the\n * in-memory buffer used to collapse/journal the response. Once the cap is\n * exceeded aimock stops appending to the buffer, marks the response as\n * truncated, and skips collapse/recording — preventing both unbounded heap\n * growth and the `RangeError: Invalid string length` that a >512MB string\n * would otherwise throw. Default: 64 MiB.\n */\n maxProxyBufferBytes?: number;\n /**\n * Maximum number of SSE/NDJSON/EventStream frames whose per-frame state\n * (`frameTimestamps`, parse buffers) aimock retains for a single proxied\n * response. Frame state is count-indexed, not byte-sized, so a long-lived or\n * never-ending stream accumulates it unbounded even when `maxProxyBufferBytes`\n * is generous. Tripping truncation on EITHER bytes OR frame count bounds both;\n * on the trip the accumulated frame state is freed and collapse/recording is\n * skipped (the full response is still relayed to the client byte-for-byte).\n * Default: 5,000,000 frames.\n */\n maxProxyBufferFrames?: number;\n}\n\nexport interface OpenRouterVideoRecordConfig {\n /**\n * Maximum decoded video size (bytes) embedded as `b64` in a recorded\n * fixture. Captures larger than the cap are persisted WITHOUT `b64` (with a\n * `_warning` in the fixture file) so a multi-hundred-MB video cannot bloat\n * the fixture directory. The cap is also a memory guard: an upstream\n * response that DECLARES an over-cap Content-Length is skipped without\n * downloading, and a response with no declared length is streamed with the\n * byte count enforced during the read — on exceed the download is aborted\n * and nothing oversized is retained in memory. In both cases the\n * same-session job serves the placeholder MP4. Default: 33554432 (32 MB\n * decoded). Set 0 for unlimited. Negative or non-integer values are treated\n * as the default (createServer warns at startup).\n */\n maxContentBytes?: number;\n}\n\nexport interface FalRecordConfig {\n /** Interval between status polls upstream during recording. Default: 1000ms. */\n pollIntervalMs?: number;\n /** Total budget for an upstream queue walk before aborting. Default: 900000ms (15 min) to accommodate video generation. */\n timeoutMs?: number;\n}\n\n/** See {@link MockServerOptions.responsesTools}. */\nexport type ResponsesToolsMode = \"legacy\" | \"extended\";\n\nexport interface MockServerOptions {\n live?: LiveOptions;\n /** Optional inbound test-client access keys. Omit to preserve permissive behavior. */\n auth?: ApiKeyAuthConfig;\n port?: number;\n host?: string;\n latency?: number;\n chunkSize?: number;\n replaySpeed?: number;\n /** Log verbosity. CLI default is \"info\"; programmatic default (when omitted) is \"silent\". */\n logLevel?: \"silent\" | \"warn\" | \"info\" | \"debug\";\n /**\n * Opt in to model misbehavior. Without it, the `misbehavior` option, a\n * fixture's `misbehavior` key and the `X-AIMock-Misbehavior` header are\n * ignored, as in 1.44.0. CLI: `--misbehavior`.\n *\n * When on, addFixture, addFixtures and prependFixture store a per-addition\n * copy, so getFixtures()[i] is the copy. findByFixture, the match-count APIs\n * and journal entry.response.fixture still name the caller's object. A field\n * assigned on that object later (for example `fixture.response = ...`) has no\n * effect; add the fixture again. When off, the caller's object is stored.\n */\n enableMisbehavior?: boolean;\n /** Server-wide misbehavior baseline; read only with `enableMisbehavior: true`. */\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /** Enable Prometheus-compatible /metrics endpoint. */\n metrics?: boolean;\n /** Strict mode: return 503 instead of 404 when no fixture matches. */\n strict?: boolean;\n /**\n * Reject fixture tool calls whose `arguments` are not valid JSON instead of\n * serving them as `{}`. Off by default (the 1.44.0 behavior). When on, wires\n * that carry arguments as an object (Anthropic and Bedrock non-streaming,\n * Converse non-streaming, Gemini, Vertex AI, Gemini Interactions\n * non-streaming, Gemini Live, Ollama) answer 500 `aimock_invalid_tool_arguments`\n * (Gemini Live: an error frame), and wires that carry them as a string\n * (Cohere and the streaming Anthropic, Bedrock, Converse and Gemini\n * Interactions paths) serve the authored text unchanged.\n */\n strictToolArguments?: boolean;\n /**\n * OpenAI Responses tool handling. `\"legacy\"` (the default) matches,\n * counts and journals Responses requests exactly as earlier releases did:\n * only top-level function tools are visible, custom tool call history is not\n * counted, and a `namespace` on a `toolCalls` entry or `toolCall` block is\n * not emitted. `\"extended\"` makes namespaced, custom and `additional_tools`\n * / `tool_search_output` tools visible to `toolName` and predicates, counts\n * custom tool call rounds for `turnIndex` / `hasToolResult` / `toolCallId`,\n * emits fixture namespaces, applies `match.toolNamespace`,\n * `customToolCalls` and `responsesBlocks` (which `\"legacy\"` ignores), and\n * makes the recorder keep namespaces and custom tool calls. The mode is\n * fixed when the server starts; changing `responsesTools` on the options\n * object after `createServer` or `LLMock.start()` is unsupported.\n */\n responsesTools?: ResponsesToolsMode;\n /** Record-and-replay: proxy unmatched requests to upstream and save fixtures. */\n record?: RecordConfig;\n /**\n * Maximum number of request/response entries to retain in the in-memory\n * journal. Oldest entries are dropped FIFO when the cap is exceeded.\n * Set to 0 (or omit) for unbounded retention. Negative values are\n * rejected at the CLI parse layer; programmatically they are treated\n * as 0 (unbounded) for back-compat.\n *\n * Default: 1000 (applied by `createServer` when omitted). The CLI passes\n * through its own default. Short-lived test harnesses that want every\n * request recorded can opt in to unbounded retention by passing 0.\n */\n journalMaxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the journal's fixture\n * match-count map. Oldest testIds are dropped FIFO when the cap is\n * exceeded. Set to 0 (or omit) for unbounded retention. Negative values\n * are rejected at the CLI parse layer; programmatically they are treated\n * as 0 (unbounded) for back-compat.\n *\n * Default: 500 (applied by `createServer` when omitted). Without a cap\n * this map can grow over time in long-running servers that see many\n * unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n /**\n * Normalize requests before matching and recording. Useful for stripping\n * dynamic data (timestamps, UUIDs, session IDs) that would cause fixture\n * mismatches on replay.\n *\n * When set, string matching for `userMessage` and `inputText` uses exact\n * equality (`===`) instead of substring (`includes`) to prevent false\n * positives from shortened keys.\n */\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n /**\n * Configure fal.ai queue polling progression. By default a job completes\n * on submit (preserves the legacy `COMPLETED`-on-submit shape). Opt into a\n * realistic `IN_QUEUE → IN_PROGRESS → COMPLETED` progression by setting\n * positive poll thresholds — useful for exercising client code that polls\n * `/status` and reacts to intermediate states.\n *\n * Applies to the general fal handler (`x-fal-target-host`-routed); the\n * legacy `/fal/queue/...` audio handler is unaffected.\n */\n falQueue?: FalQueueConfig;\n /**\n * Configure OpenRouter async video job polling progression\n * (`pending → in_progress → completed | failed` on `GET /api/v1/videos/{id}`).\n * Same threshold semantics as `falQueue`. By default (0/0) the job is\n * seeded terminal at submit — content is downloadable with zero polls, and\n * the first status poll merely reports the already-terminal status.\n */\n openRouterVideo?: FalQueueConfig;\n /**\n * Configure Google Veo async video job polling progression\n * (`done:false → done:true` on `GET /v1beta/operations/{name}`). Same\n * threshold semantics as `openRouterVideo`; the internal\n * `pending → in_progress → completed | failed` model is serialized to the\n * two-state Veo wire.\n */\n veoVideo?: FalQueueConfig;\n /**\n * Configure xAI Grok Imagine async video job polling progression\n * (`pending → done | failed` on `GET /v1/videos/{request_id}`). Same\n * threshold semantics as `openRouterVideo`; progress is synthesized from the\n * poll count.\n */\n grokVideo?: FalQueueConfig;\n /**\n * Configure BytePlus Ark (Seedance) async video task polling progression\n * (`queued → running → <recorded terminal status>` on\n * `GET /api/v3/contents/generations/tasks/{id}`). Same threshold semantics as\n * `grokVideo`; unlike the others the TERMINAL status is not synthesized — it\n * is whatever the recorded/authored envelope carries.\n */\n bytePlusVideo?: FalQueueConfig;\n}\n\nexport interface ApiKeyAuthConfig {\n apiKeys: readonly string[];\n}\n\n/**\n * Poll-progression thresholds, documented below in fal queue terms; when used\n * as `openRouterVideo` the states map to `pending` / `in_progress` /\n * `completed | failed`.\n */\nexport interface FalQueueConfig {\n /**\n * Status polls before transitioning `IN_QUEUE → IN_PROGRESS`. Unset and an\n * explicit `0` differ: when BOTH fields are unset the job completes\n * synchronously on submit (no IN_QUEUE / IN_PROGRESS polls emitted), but\n * explicitly setting this field — even to `0` — enables progression when\n * `pollsBeforeCompleted` is unset, with `pollsBeforeCompleted` defaulting\n * to `pollsBeforeInProgress + 1` so the job passes through IN_PROGRESS\n * (an explicit `pollsBeforeCompleted: 0` completes on submit only when\n * `pollsBeforeInProgress` is `0` or unset — otherwise the clamp below\n * lifts it to `pollsBeforeInProgress`).\n */\n pollsBeforeInProgress?: number;\n /**\n * Status polls before transitioning to `COMPLETED`. Default: 0 when\n * `pollsBeforeInProgress` is also unset (no progression), otherwise\n * `pollsBeforeInProgress + 1` so the job spends one poll in IN_PROGRESS.\n * An explicit value lower than `pollsBeforeInProgress` is clamped up so\n * IN_PROGRESS is never skipped. When equal to a nonzero\n * `pollsBeforeInProgress` — or when `pollsBeforeInProgress` is unset or 0\n * and this field is 1 (the IN_PROGRESS branch consumes the first poll) —\n * the job still spends one poll in IN_PROGRESS, so the terminal status\n * lands one poll later than configured.\n */\n pollsBeforeCompleted?: number;\n}\n\n// Handler defaults — the common shape passed from server.ts to every handler\n\nexport interface HandlerDefaults {\n /** Present only when misbehavior is enabled; without it no fault is planned. */\n misbehavior?: MisbehaviorScope;\n misbehaviorCounters?: MisbehaviorCounters;\n latency: number;\n chunkSize: number;\n replaySpeed: number;\n logger: Logger;\n /**\n * Reads resolve to a `ChaosScope` on a real server (baseline + per-testId\n * overrides); assigning `undefined` drops every runtime override.\n */\n chaos?: ChaosDefaults;\n /**\n * Live per-testId chaos overrides, owned by the server instance so the\n * control API can install one without reaching through the getter.\n */\n chaosByTestId?: Map<string, ChaosConfig>;\n registry?: MetricsRegistry;\n record?: RecordConfig;\n strict?: boolean;\n /** See {@link MockServerOptions.strictToolArguments}. */\n strictToolArguments?: boolean;\n /** See {@link MockServerOptions.responsesTools}. Absent means `\"legacy\"`. */\n responsesTools?: ResponsesToolsMode;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n falQueue?: FalQueueConfig;\n openRouterVideo?: FalQueueConfig;\n veoVideo?: FalQueueConfig;\n grokVideo?: FalQueueConfig;\n /**\n * Configure BytePlus Ark (Seedance) async video task polling progression\n * (`queued → running → <recorded terminal status>` on\n * `GET /api/v3/contents/generations/tasks/{id}`). Same threshold semantics as\n * `grokVideo`; unlike the others the TERMINAL status is not synthesized — it\n * is whatever the recorded/authored envelope carries.\n */\n bytePlusVideo?: FalQueueConfig;\n}\n\n// MCP fakes — scenario-scoped MCP tool fakes in fixture files (`mcpFakes`)\n\n/**\n * Block scope: `\"shared\"`, or exact test id and/or context (at least one;\n * `{}` is not a scope). Readonly: validated scopes are frozen.\n */\nexport type McpFakeScope =\n | \"shared\"\n | Readonly<{ testId: string; context?: string }>\n | Readonly<{ testId?: string; context: string }>;\n\n/**\n * `T` with every nested property and array read-only: the type of a value the\n * MCP fakes store deep-freezes before it hands it out.\n */\nexport type McpFakeDeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends object\n ? { readonly [K in keyof T]: McpFakeDeepReadonly<T[K]> }\n : T;\n\n/** MCP `Annotations` on a content block. */\nexport interface McpFakeAnnotations {\n audience?: Array<\"user\" | \"assistant\">;\n /** From 0 (least) to 1 (most important). */\n priority?: number;\n /** RFC 3339 date-time with seconds and a `Z` or `±hh:mm` offset. */\n lastModified?: string;\n}\n\n/** MCP `Icon` on a `resource_link` content block. */\nexport interface McpFakeIcon {\n src: string;\n mimeType?: string;\n sizes?: string[];\n theme?: \"light\" | \"dark\";\n}\n\n/** MCP `TextResourceContents` / `BlobResourceContents`: exactly one of `text` / `blob`. */\nexport type McpFakeResourceContents = {\n uri: string;\n mimeType?: string;\n _meta?: Record<string, unknown>;\n} & ({ text: string; blob?: never } | { blob: string; text?: never });\n\ntype McpFakeContentCommon = {\n annotations?: McpFakeAnnotations;\n _meta?: Record<string, unknown>;\n};\n\n/** One MCP content block of a tool result (`ContentBlock`); `data` and `blob` are base64. */\nexport type McpFakeContentBlock = McpFakeContentCommon &\n (\n | { type: \"text\"; text: string }\n | { type: \"image\"; data: string; mimeType: string }\n | { type: \"audio\"; data: string; mimeType: string }\n | {\n type: \"resource_link\";\n uri: string;\n name: string;\n title?: string;\n description?: string;\n mimeType?: string;\n size?: number;\n icons?: McpFakeIcon[];\n }\n | { type: \"resource\"; resource: McpFakeResourceContents }\n );\n\n/** A full MCP `CallToolResult`, served unchanged. No other keys. */\nexport interface McpFakeFullResult {\n content: McpFakeContentBlock[];\n isError?: boolean;\n structuredContent?: Record<string, unknown>;\n _meta?: Record<string, unknown>;\n}\n\n/**\n * Structured data: any JSON object without the `CallToolResult`\n * keys, served as text plus `structuredContent`.\n */\nexport type McpFakeStructuredResult = Record<string, unknown> & {\n content?: never;\n isError?: never;\n structuredContent?: never;\n _meta?: never;\n};\n\n/**\n * A call entry's `result` value. Validation also checks what the type cannot\n * say: base64 `data` / `blob`, `priority` from 0 to 1, the `lastModified`\n * format, and plain JSON throughout. Unknown keys inside a content item are\n * allowed and served as given.\n */\nexport type McpFakeResult =\n | string\n | McpFakeContentBlock[]\n | McpFakeFullResult\n | McpFakeStructuredResult;\n\n/** How a call entry matches: exactly one of `args` / `anyArgs`. */\nexport type McpFakeCallMatch =\n | { args: Record<string, unknown>; anyArgs?: never }\n | { anyArgs: true; args?: never };\n\n/** What a call entry answers: exactly one of `result` / `error`. */\nexport type McpFakeCallAnswer =\n | { result: McpFakeResult; error?: never }\n | { error: string; result?: never };\n\n/** One ordered answer for a tool. */\nexport type McpFakeCall = McpFakeCallMatch &\n McpFakeCallAnswer & {\n id?: string;\n /** FA3: the notifications the upstream sent on the call's stream before the response. */\n notifications?: McpFakeNotification[];\n /** FA4: the recorded response time in ms (a number >= 0). */\n durationMs?: number;\n };\n\n/** One faked tool in a block. */\nexport interface McpFakeTool {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n calls: McpFakeCall[];\n}\n\n/** One `mcpFakes` block. */\nexport interface McpFakeBlock {\n scope: McpFakeScope;\n mount?: string;\n undeclaredTools?: McpFakeUndeclaredPolicy;\n tools: McpFakeTool[];\n /** FA1: the recorded `tools/list` (MCP `Tool` objects, kept verbatim). */\n list?: Record<string, unknown>[];\n /** FA2: provenance of a recording. Never used for matching. */\n recorded?: McpFakeRecorded;\n /** FA5: `\"recorded\"` (default) plays FA3/FA4 timing; `\"immediate\"` ignores it. */\n timing?: \"recorded\" | \"immediate\";\n}\n\n/** FA2 `recorded`: where and when a block was recorded. */\nexport interface McpFakeRecorded {\n /** The upstream origin only. */\n upstream: string;\n protocolVersion: string;\n serverInfo?: Record<string, unknown>;\n aimockVersion: string;\n /** ISO 8601 date-time with seconds and an offset. */\n at: string;\n}\n\n/** MR1 `MCPMock.enableRecording` options. */\nexport interface McpRecordConfig {\n upstream: string;\n fixturePath?: string;\n proxyOnly?: boolean;\n maxRecordBufferBytes?: number;\n /** MR14 `\"Name: value\"`; env AIMOCK_MCP_UPSTREAM_AUTH (AM6). */\n upstreamAuth?: string;\n /** S2 (d); env AIMOCK_RECORD_SECRET_VALUES (newline-separated). */\n secretValues?: string[];\n /** MR5 (b): the server's strict default; X-AIMock-Strict still overrides per request. */\n strict?: boolean;\n}\n\n/** FA3: one recorded notification of a call entry. */\nexport interface McpFakeNotification {\n atMs: number;\n method: string;\n params: Record<string, unknown>;\n}\n\nexport type McpFakeUndeclaredPolicy = \"allow\" | \"deny\";\n\n/**\n * The resolved identity of one MCP request: test id, context and undeclared\n * override, each from the request or else from the session.\n * `null` means \"not supplied\". The identity shape the fake store's matching\n * methods take (`applicable`, `selectTier`, `claim`, `policy`, `declaredTools`,\n * `listTools`).\n */\nexport interface McpFakeIdentity {\n testId: string | null;\n context: string | null;\n undeclared: McpFakeUndeclaredPolicy | null;\n}\n\n/**\n * One raw block as a loader hands it off, before validation. `source` is the\n * entry-id source string; `blockIndex` is `null` when `mcpFakes` is a single object.\n */\nexport interface McpFakeSource {\n source: string;\n blockIndex: number | null;\n raw: unknown;\n}\n\n/**\n * `response.mcpFake.outcome` on a `tools/call` journal entry. `evicted`:\n * the request's test id lost its consumption state to the FIFO test-id cap.\n */\nexport type McpFakeOutcome = \"answered\" | \"mismatch\" | \"exhausted\" | \"not_declared\" | \"evicted\";\n\n/**\n * JSON-RPC error codes of the four fake failures, keyed by\n * `error.data.aimock.code`. `-31010` and `-31011` sit outside the JSON-RPC\n * reserved range. `MCP_FAKE_EVICTED` is for a lookup of a declared tool by a\n * test id whose state the FIFO test-id cap evicted, until it is reset.\n */\nexport const MCP_FAKE_ERROR_CODES = {\n MCP_FAKE_NOT_DECLARED: -32602,\n MCP_FAKE_MISMATCH: -32602,\n MCP_FAKE_EXHAUSTED: -31010,\n MCP_FAKE_EVICTED: -31011,\n} as const;\n\nexport type McpFakeErrorCode = keyof typeof MCP_FAKE_ERROR_CODES;\n"],"mappings":";;;;;;;AAi1DA,MAAa,uBAAuB;CAClC,uBAAuB;CACvB,mBAAmB;CACnB,oBAAoB;CACpB,kBAAkB;CACnB"}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../src/types.ts"],"sourcesContent":["import type { MisbehaviorSummary } from \"./misbehavior.js\";\nimport type * as http from \"node:http\";\nimport type * as net from \"node:net\";\nimport type { Journal } from \"./journal.js\";\nimport type { LiveFixtureResponse, LiveOptions } from \"./live-types.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type { McpFakeAddOrigin, McpFakeAddResult } from \"./mcp-fakes.js\";\n\n// aimock type definitions — shared across all provider adapters and the fixture router.\n\nexport interface Mountable {\n handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean>;\n handleUpgrade?(socket: net.Socket, head: Buffer, pathname: string): Promise<boolean>;\n health?(): { status: string; [key: string]: unknown };\n setJournal?(journal: Journal): void;\n setBaseUrl?(url: string): void;\n setRegistry?(registry: MetricsRegistry): void;\n /**\n * MCP fakes. A mount \"is an MCPMock\" for fakes if and only if it\n * implements `addMcpFakes`. Always appends. `origin` picks the entry-id\n * source and its counter: `file` keeps each block's `source` (`@<k>` load\n * counter); `code` / `control-api` is one run-time addition (`code#<n>` /\n * `control-api#<n>`). Returns the shadowed-entry warnings with the real\n * entry ids. When any block is bad, or the input holds no blocks, throws one\n * `FixtureLoadError` (an `McpFakesAddError` carrying every error found and\n * the warnings) and adds nothing.\n */\n addMcpFakes?(blocks: McpFakeSource[], origin: McpFakeAddOrigin): McpFakeAddResult;\n /** Unload every fake on this mount. The entry-id counters are kept. */\n clearMcpFakes?(): void;\n /** Reset fake consumption state for one test id, or for all when omitted. */\n resetScenarioState?(testId?: string): void;\n /** Hands an MCP mount a `Logger`. Nothing in the server calls it. */\n setLogger?(logger: Logger): void;\n}\n\nexport interface ContentPart {\n type: string;\n text?: string;\n [key: string]: unknown;\n}\n\nexport interface ChatMessage {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n content: string | ContentPart[] | null;\n name?: string;\n tool_calls?: ToolCallMessage[];\n /**\n * OpenAI Responses `custom_tool_call` history items. Set only when the\n * server runs with `responsesTools: \"extended\"`; the default ignores\n * custom tool call history, as earlier releases did.\n */\n custom_tool_calls?: CustomToolCallMessage[];\n tool_call_id?: string;\n}\n\nexport interface ToolCallMessage {\n id: string;\n type: \"function\";\n /**\n * OpenAI Responses namespace of the called tool. Set only with\n * `responsesTools: \"extended\"`, when the history item had one.\n */\n namespace?: string;\n function: { name: string; arguments: string };\n}\n\n/**\n * An OpenAI Responses `custom_tool_call` history item, normalized for fixture\n * matching. Produced only with `responsesTools: \"extended\"`.\n */\nexport interface CustomToolCallMessage {\n id: string;\n type: \"custom\";\n name: string;\n /** Free-text input of the call (\"\" when the history item had none). */\n input: string;\n namespace?: string;\n}\n\nexport interface ChatCompletionRequest {\n model: string;\n messages: ChatMessage[];\n stream?: boolean;\n /**\n * OpenRouter fallback model list. When present (and the request arrived on\n * the OpenRouter `/api/v1/...` base), the candidate list `[model, ...models]`\n * is attempted in order. A NON-error fixture match wins; an ERROR fixture\n * candidate falls through to the next candidate by default — UNLESS it is\n * terminal, in which case that error is served with no failover. An error is\n * terminal when its fixture sets `fallthrough: false` (per-error-class gate)\n * or when the request sets `provider.allow_fallbacks: false` (request-level\n * gate). A winning (non-error) slug is echoed back as the response `model`;\n * when the chain terminates on an error, the response is the OpenRouter error\n * envelope (`{ error: { code, message, metadata? } }`), which carries no\n * `model` field. Purely additive: a request without `models` behaves as a\n * single-model match. Every other\n * OpenRouter request extension (`route`, `reasoning`, `plugins`,\n * `prediction`, `usage`, and any unknown key) is accepted and journaled via\n * the index signature below — never required, never rejected. aimock does not\n * model those sub-shapes; unknown body keys pass straight through.\n */\n models?: string[];\n /**\n * OpenRouter provider-routing preferences. Accepted loosely and journaled\n * verbatim; the ONLY sub-field aimock reads behaviorally is\n * `allow_fallbacks`. When `allow_fallbacks === false` on an OpenRouter\n * request, the `models[]` fallback loop is suppressed — only the primary is\n * tried, and a primary error fixture is served as terminal instead of\n * falling through (the fail-CLOSED half of the feature). Absent/`true` keeps\n * the default fall-through behavior. Every other provider-prefs key\n * (`order`, `only`, `ignore`, `sort`, …) passes straight through unread.\n */\n provider?: { allow_fallbacks?: boolean; [key: string]: unknown };\n stream_options?: { include_usage?: boolean; [key: string]: unknown };\n temperature?: number;\n max_tokens?: number;\n tools?: ToolDefinition[];\n /**\n * OpenAI Responses `custom` tools offered by the request (top level, inside\n * a `namespace` tool, or in an `additional_tools` / `tool_search_output`\n * input item). Set only with `responsesTools: \"extended\"`; `toolName`\n * matches these as well as `tools`.\n */\n customTools?: CustomToolDefinition[];\n tool_choice?: string | object;\n response_format?: { type: string; [key: string]: unknown };\n /** Embedding input text, set by the embeddings handler for fixture matching. */\n embeddingInput?: string;\n /** Endpoint type, set by handlers for fixture endpoint filtering. */\n _endpointType?: string;\n /** Context identifier, set by handlers for fixture context routing. */\n _context?: string;\n /**\n * Video-provider discriminator, set by the async video handlers for journal\n * and dispatch clarity only. NOT a fixture match key — `buildFixtureMatch`\n * derives the endpoint from `_endpointType` and never reads this field; the\n * model string is the provider disambiguator (veo-* / grok-imagine-* /\n * openrouter ids do not overlap).\n */\n _videoProvider?: \"openrouter\" | \"veo\" | \"grok\" | \"byteplus\";\n [key: string]: unknown;\n}\n\n/**\n * aimock's normalized request function tool. The OpenAI Responses adapter\n * keeps only top-level `function` tools here by default. With\n * `responsesTools: \"extended\"` it also adds the function tools inside a\n * `namespace` tool (with `namespace` set) and those from `additional_tools` /\n * `tool_search_output` input items.\n */\nexport interface ToolDefinition {\n type: \"function\";\n function: { name: string; description?: string; parameters?: object };\n /** OpenAI Responses namespace the tool was offered in (extended mode only). */\n namespace?: string;\n}\n\n/**\n * An OpenAI Responses `custom` (freeform) tool offered by a request. See\n * {@link ChatCompletionRequest.customTools}.\n */\nexport interface CustomToolDefinition {\n type: \"custom\";\n name: string;\n description?: string;\n /** OpenAI Responses namespace the tool was offered in, when inside a `namespace` tool. */\n namespace?: string;\n /** Custom-tool input format (grammar or text), carried verbatim. */\n format?: unknown;\n}\n\n// Fixture matching\n\nexport interface FixtureMatch {\n userMessage?: string | RegExp;\n /**\n * Substring, regexp, or array of substrings matched against the concatenated\n * text content of every `system` role message in the request. Gates fixture\n * activation on values the host plumbs in via system messages (agent\n * context, persona, dynamic config) instead of the user-typed prompt — so\n * changing context state in the calling app causes stale fixtures to fall\n * through to a real upstream instead of silently returning a baked response\n * that no longer reflects reality.\n *\n * When given an array of strings, ALL substrings must be present (AND\n * semantics). Useful when the gate must combine multiple non-adjacent\n * tokens — e.g., a default name AND a default activity list whose JSON\n * positions vary across requests.\n */\n systemMessage?: string | string[] | RegExp;\n inputText?: string | RegExp;\n /**\n * Exact embedding operation, input shape and requested dimensions. Recordings\n * set this to keep batches and widths distinct across Ollama and OpenAI wire\n * formats. Omit this field for broad fixtures. Values preserve the wire JSON,\n * including invalid arguments when recording an upstream error response.\n */\n embeddingRequest?: {\n operation: \"/api/embed\" | \"/api/embeddings\" | \"/v1/embeddings\";\n input: unknown;\n dimensions?: unknown;\n };\n toolCallId?: string;\n /**\n * Substring matched against the text content of the LAST message when that\n * message is a `tool` result (same last-message rule as `toolCallId`).\n * Discriminates fixtures whose requests differ ONLY inside the tool-result\n * payload — e.g. a human-in-the-loop tool where approve and cancel resume\n * with the same toolCallId but different result JSON.\n */\n toolResultContains?: string;\n toolName?: string;\n /**\n * Exact OpenAI Responses tool namespace. Alone, it matches when any offered\n * tool sits in this namespace; with `toolName`, a single offered tool must\n * carry both the name and the namespace. Applies only when the server runs\n * with `responsesTools: \"extended\"`; otherwise it is ignored, as in 1.44.0.\n */\n toolNamespace?: string;\n model?: string | RegExp;\n responseFormat?: string;\n predicate?: (req: ChatCompletionRequest) => boolean;\n /** Which occurrence of this match to respond to (0-indexed). Undefined means match any. */\n sequenceIndex?: number;\n turnIndex?: number;\n /**\n * Turn-scoped tool-result presence gate: matches on whether the CURRENT turn\n * (the messages after the last `role: \"user\"` message) contains a `role: \"tool\"`\n * message. Scoping to the current turn — rather than the whole conversation —\n * lets a leg-1 (`false`) / leg-2 (`true`) fixture pair keep discriminating on\n * later turns whose history still carries earlier turns' tool results. If the\n * request has no user message, it falls back to scanning the whole conversation.\n */\n hasToolResult?: boolean;\n endpoint?:\n | \"chat\"\n | \"image\"\n | \"speech\"\n | \"transcription\"\n | \"translation\"\n | \"video\"\n | \"embedding\"\n | \"audio-gen\"\n | \"elevenlabs-tts\"\n | \"elevenlabs-voice-design\"\n | \"elevenlabs-voice\"\n | \"elevenlabs-voice-get\"\n | \"elevenlabs-voice-delete\"\n | \"fal-audio\"\n | \"fal\"\n | \"realtime\"\n | \"realtime-transcription\"\n | \"realtime-translation\"\n | \"openai-live\";\n context?: string;\n}\n\n// Fixture response types\n\n/**\n * A web source citation attached to a text response. Served as `url_citation`\n * annotations on OpenAI Responses (`output_text.annotations`) and Chat\n * Completions (`message.annotations` / `delta.annotations`), as\n * `web_search_result_location` citations on Anthropic text blocks, and as\n * Gemini `groundingMetadata` (`groundingChunks` + `groundingSupports`).\n */\nexport interface FixtureUrlCitation {\n /**\n * Optional discriminator; a citation without `type` is a URL citation. A\n * citation that also sets file citation fields (`fileId`, `filename`,\n * `index`) is rejected: set `type: \"file\"` or remove the stray fields.\n */\n type?: \"url\";\n url: string;\n title?: string;\n /**\n * Offsets of the cited span, as UTF-16 string indices (JavaScript string\n * positions) into the joined text of all the response's text blocks. Chat\n * Completions, Anthropic, Gemini and Responses read them the same way;\n * Gemini converts them to the UTF-8 byte offsets its wire carries. Default\n * to the whole text.\n */\n startIndex?: number;\n endIndex?: number;\n}\n\n/**\n * A citation of an uploaded file, served as a `file_citation` annotation on\n * OpenAI Responses (`output_text.annotations`, streamed through\n * `response.output_text.annotation.added`). Chat Completions, Anthropic\n * Messages and Gemini have no file-citation wire shape, so they skip it and\n * serve only the fixture's URL citations.\n */\nexport interface FixtureFileCitation {\n type: \"file\";\n /** Wire `file_id`, e.g. `file-abc123`. */\n fileId: string;\n /** Wire `filename` of the cited file. */\n filename: string;\n /**\n * Character position cited: a UTF-16 string index into the joined text of\n * all the response's text blocks; defaults to the end of the text. The wire\n * `index` is not this value: the annotation goes on the `output_text` whose\n * text holds the position, and its `index` counts from the start of that\n * `output_text`.\n */\n index?: number;\n}\n\n/** A source citation attached to a text response: a web page or an uploaded file. */\nexport type FixtureCitation = FixtureUrlCitation | FixtureFileCitation;\n\n/**\n * A model-generated file (e.g. an inline image). Served on Gemini as\n * `inlineData` parts and on the OpenAI Responses API as\n * `image_generation_call` output items (`image/*` files only; another\n * `mimeType` is rejected). Chat Completions (OpenAI, Azure OpenAI, OpenRouter\n * and BytePlus ModelArk) and Anthropic Messages cannot carry one and reject\n * the request. Every rejection is a coded 500,\n * `aimock_unsupported_generated_file`, sent before any content. Every other\n * wire (Bedrock, Cohere, Ollama, Gemini Interactions and the rest) drops\n * `files` without an error.\n */\nexport interface FixtureGeneratedFile {\n mimeType: string;\n /** Base64-encoded file bytes. */\n data: string;\n}\n\n/**\n * Fields that override auto-generated envelope values in the built response.\n * Scalar fields (finishReason, role) use OpenAI-canonical values — provider\n * handlers translate automatically. For usage, provide field names native to\n * your target provider (OpenAI Chat: prompt_tokens, completion_tokens;\n * Responses API: input_tokens, output_tokens; Anthropic: input_tokens,\n * output_tokens; Gemini: promptTokenCount, candidatesTokenCount).\n *\n * When total_tokens (or provider equivalent) is omitted, it is auto-computed\n * from the component fields.\n *\n * Provider support: seven envelope fields are common (id, created, model,\n * usage, systemFingerprint, finishReason, role); provider and\n * nativeFinishReason are OpenRouter-only and ignored elsewhere; `citations`\n * and `files` are content fields (see their docs). OpenAI Chat honors the\n * seven common fields plus `citations` (it rejects `files`); OpenRouter\n * additionally honors provider/nativeFinishReason; Responses API honors five\n * common fields (no role, systemFingerprint) plus optional phase, `citations`\n * and `files`; Claude honors five (no created, systemFingerprint) plus\n * `citations` (it rejects `files`); Gemini honors two (only finishReason,\n * usage) plus `citations` and `files`.\n */\nexport interface ResponseOverrides {\n /**\n * Model-generated files emitted before the response text and tool calls.\n * Served on Gemini and the OpenAI Responses API (`image/*` only); Chat\n * Completions and Anthropic Messages reject them; every other wire drops\n * them without an error (see {@link FixtureGeneratedFile}).\n */\n files?: FixtureGeneratedFile[];\n /**\n * Source citations for the response text (OpenAI Responses, Chat Completions, Anthropic, Gemini).\n * File citations are served on OpenAI Responses only; the other three skip them.\n * Every other wire ignores `citations`.\n */\n citations?: FixtureCitation[];\n id?: string;\n created?: number;\n model?: string;\n usage?: {\n prompt_tokens?: number;\n completion_tokens?: number;\n total_tokens?: number;\n input_tokens?: number;\n output_tokens?: number;\n promptTokenCount?: number;\n candidatesTokenCount?: number;\n totalTokenCount?: number;\n /**\n * OpenRouter per-request cost (USD/credits). Fixture-scriptable — powers\n * budget-guard demos (\"assert the guard trips at $5 without spending $5\").\n * Emitted on OpenRouter-shaped responses only, and ONLY when supplied —\n * `cost`/`cost_details` are nullable and omitted by default (never 0).\n */\n cost?: number;\n /**\n * OpenRouter cost breakdown. When the response emits `cost_details` at all\n * it MUST carry all three upstream fields (the canonical `@openrouter/sdk`\n * throws on a partial object); a fixture may supply any subset and the rest\n * default to 0. Only emitted when the fixture supplies a `cost`.\n */\n cost_details?: {\n upstream_inference_cost?: number;\n upstream_inference_prompt_cost?: number;\n upstream_inference_completions_cost?: number;\n [key: string]: unknown;\n };\n /**\n * OpenRouter prompt-token breakdown. Emitted ONLY when overridden — the\n * default response never asserts a fake cache state.\n */\n prompt_tokens_details?: {\n cached_tokens?: number;\n cache_write_tokens?: number;\n audio_tokens?: number;\n [key: string]: unknown;\n };\n /**\n * OpenRouter completion-token breakdown. Emitted ONLY when overridden.\n */\n completion_tokens_details?: { reasoning_tokens?: number; [key: string]: unknown };\n /**\n * OpenRouter bring-your-own-key flag on the usage object. Emitted ONLY when\n * overridden (avoids asserting a fake BYOK state by default).\n */\n is_byok?: boolean;\n /**\n * Forward-compat escape hatch for provider usage fields aimock does not\n * model (OpenRouter `native_tokens_prompt` / `native_tokens_completion`,\n * future breakdowns, …). The recorder persists such fields when an upstream\n * usage frame carries them, and OpenRouter-shaped replays pass any extra key\n * through verbatim onto the emitted `usage` object. The load-time validator\n * still requires extra scalars to be numbers (see fixture-loader.ts), so a\n * typo'd field is caught rather than silently emitted as a string.\n *\n * Anthropic Messages and Bedrock invoke accept cache_read_input_tokens and\n * cache_creation_input_tokens here. cache_creation accepts a number\n * (unrendered) or an object with ephemeral_5m_input_tokens and\n * ephemeral_1h_input_tokens. Missing object members render as zero in final\n * and message_start usage; message_delta emits only the flat cache fields.\n * Record mode preserves Anthropic token counts and cache usage for replay.\n */\n [key: string]: unknown;\n };\n systemFingerprint?: string;\n finishReason?: string;\n role?: string;\n /**\n * OpenAI Responses message phase (HTTP and WebSocket). Applied to each\n * emitted assistant message, never to reasoning or tool-call items. Omitted\n * unless supplied; aimock does not infer a phase from content or stop reason.\n * Ignored by other providers.\n */\n phase?: \"commentary\" | \"final_answer\";\n /**\n * OpenRouter top-level `provider` override. When omitted, OpenRouter-shaped\n * responses default it to the author segment of the winning model slug\n * (`openai/gpt-4o` → `\"openai\"`). Ignored for OpenAI (`/v1/...`) responses.\n */\n provider?: string;\n /**\n * OpenRouter `native_finish_reason` override (the raw upstream finish reason).\n * When omitted, OpenRouter-shaped responses mirror `finish_reason`. Ignored\n * for OpenAI responses.\n */\n nativeFinishReason?: string;\n}\n\nexport interface TextResponse extends ResponseOverrides {\n content: string;\n reasoning?: string;\n /**\n * The real cryptographic `signature` captured from a recorded Anthropic\n * thinking turn. When present it is emitted on the replayed thinking block;\n * otherwise replay falls back to aimock's round-trip-safe placeholder.\n * Persisted only alongside a non-empty `reasoning`: a signature captured\n * without plaintext reasoning (e.g. whitespace-only thinking) is intentionally\n * discarded at the persistence layer, since replay attaches signatures only to\n * plaintext thinking blocks.\n */\n reasoningSignature?: string;\n /**\n * Opaque `data` payload(s) of any Anthropic `redacted_thinking` blocks\n * captured from a recorded thinking turn, in stream order. When present they\n * are replayed as faithful `redacted_thinking` content blocks so the encrypted\n * reasoning round-trips; absent for non-Anthropic providers and turns without\n * redacted thinking. Recorded redacted blocks replay as a leading group: the\n * original interleaving of `thinking` and `redacted_thinking` blocks is not\n * preserved (see the CHANGELOG fidelity caveat).\n */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ToolCall {\n name: string;\n arguments: string;\n id?: string;\n /**\n * OpenAI Responses API only: emitted as `function_call.namespace` when the\n * server runs with `responsesTools: \"extended\"`. Ignored otherwise, and by\n * every other wire.\n */\n namespace?: string;\n}\n\n/**\n * An OpenAI Responses custom (freeform) tool call, emitted as a\n * `custom_tool_call` item. Fixtures carry it in `customToolCalls` (or as a\n * `customToolCall` block in `responsesBlocks`). Only the Responses API (HTTP\n * and WebSocket) can serve it; every other wire rejects it with\n * `UnsupportedToolCallError`.\n */\nexport interface CustomToolCall {\n type: \"custom\";\n name: string;\n /** Free-text input. Never parsed, never stringified. */\n input: string;\n id?: string;\n /** OpenAI Responses API only: emitted as `custom_tool_call.namespace`. */\n namespace?: string;\n}\n\n/**\n * A tool call the OpenAI Responses API serves: a function call (from\n * `toolCalls`) or a custom tool call (from `customToolCalls`).\n */\nexport type FixtureToolCall = (ToolCall & { type?: undefined }) | CustomToolCall;\n\n/**\n * A single ordered streaming block for a {@link ContentWithToolCallsResponse}.\n *\n * When a combined content+toolCalls fixture sets the optional `blocks` field,\n * builders stream the blocks in array order — enabling tool-call-before-text\n * and interleaved orderings that the legacy `{ content, toolCalls }` shape\n * (always text-first) cannot express. A `text` block carries a text segment; a\n * `toolCall` block mirrors {@link ToolCall} (`name` + JSON-string `arguments`,\n * optional `id`).\n */\nexport type FixtureBlock =\n | { type: \"text\"; text: string }\n | { type: \"toolCall\"; name: string; arguments: string; id?: string; namespace?: string };\n\n/**\n * An ordered block for {@link ContentWithToolCallsResponse.responsesBlocks}:\n * a {@link FixtureBlock}, or a `customToolCall` block that mirrors\n * {@link CustomToolCall} (`name` + free-text `input`, optional `id` and\n * `namespace`). Only the OpenAI Responses API serves `responsesBlocks`.\n */\nexport type ResponsesFixtureBlock =\n | FixtureBlock\n | { type: \"customToolCall\"; name: string; input: string; id?: string; namespace?: string };\n\nexport interface ToolCallResponse extends ResponseOverrides {\n toolCalls: ToolCall[];\n /**\n * OpenAI Responses custom tool calls, emitted after `toolCalls`. A turn with\n * only custom calls sets `toolCalls: []`. Every other wire rejects a fixture\n * with a non-empty `customToolCalls` (`UnsupportedToolCallError`). Applies\n * only with `responsesTools: \"extended\"`; otherwise it is ignored.\n */\n customToolCalls?: CustomToolCall[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ContentWithToolCallsResponse extends ResponseOverrides {\n /**\n * Combined-turn text. Required for the legacy `{ content, toolCalls }` shape\n * and the combined `{ content, toolCalls, blocks }` shape. OPTIONAL only for a\n * BLOCKS-ONLY fixture (`{ blocks: [...] }` with no `content`/`toolCalls`),\n * where the ordered `blocks` array is the sole source of streamed output and\n * the legacy text-first builder branch is never taken. The runtime guard\n * `isContentWithToolCallsResponse` enforces this: it matches when BOTH\n * `content` + `toolCalls` are present (legacy/combined) OR when a non-empty\n * `blocks` array is present (blocks-only).\n */\n content?: string;\n /** See {@link ContentWithToolCallsResponse.content} — optional only for the blocks-only shape. */\n toolCalls?: ToolCall[];\n /** OpenAI Responses custom tool calls; see {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: CustomToolCall[];\n /**\n * Optional ordered streaming blocks. When present, builders stream these in\n * array order (tool-first / interleaved); when absent, the legacy\n * `{ content, toolCalls }` text-first path runs unchanged. Purely additive.\n * A non-empty `blocks` array also makes this a FIRST-CLASS blocks-only\n * response even without `content`/`toolCalls` (see those fields above).\n */\n blocks?: FixtureBlock[];\n /**\n * OpenAI Responses only: ordered blocks that may include `customToolCall`\n * blocks. On the Responses API they replace `blocks`; a fixture may not set\n * both. Every other wire rejects a `customToolCall` block here\n * (`UnsupportedToolCallError`) and otherwise ignores this field. Applies\n * only with `responsesTools: \"extended\"`; otherwise it is ignored.\n */\n responsesBlocks?: ResponsesFixtureBlock[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface ErrorResponse {\n error: {\n message: string;\n type?: string;\n param?: string | null;\n code?: string;\n /**\n * OpenRouter error metadata. Emitted verbatim inside the OpenRouter error\n * envelope (`{ error: { code, message, metadata } }`) when the request\n * arrived on the OpenRouter base. Moderation blocks carry\n * `{ reasons, flagged_input, provider_name, model_slug }` on a 403.\n * Ignored for OpenAI-shaped error responses.\n */\n metadata?: Record<string, unknown>;\n };\n status?: number;\n /** Override the Retry-After header value on 429 responses. Default: 1. */\n retryAfter?: number;\n /**\n * OpenRouter `models[]` failover eligibility for THIS error class. Real\n * OpenRouter fails over INCONSISTENTLY by error class — a `403 budget-exceeded`\n * or a generic \"provider returned error\" is served as terminal (it does NOT\n * advance to the next `models[]` candidate), while 429/503 usually do fail\n * over (openclaw #60191). This flag lets a fixture reproduce that:\n * - absent / `true` (default) — the error candidate FALLS THROUGH to the next\n * `models[]` candidate (backward-compatible; existing fixtures unchanged).\n * - `false` — the error is TERMINAL: the fallback loop stops and serves this\n * error with no failover, even if a later candidate would match.\n *\n * Composes with the request-level `provider.allow_fallbacks`: fall-through\n * happens only when BOTH allow it (if EITHER says don't, don't). Only\n * consulted for OpenRouter `models[]` fallback; ignored elsewhere.\n */\n fallthrough?: boolean;\n}\n\nexport interface EmbeddingResponse {\n embedding: number[];\n}\n\nexport interface ImageItem {\n url?: string;\n b64Json?: string;\n revisedPrompt?: string;\n}\n\nexport interface ImageResponse {\n image?: ImageItem;\n images?: ImageItem[];\n}\n\n// ORDERING CONTRACT: audio fixtures MUST be discriminated by `isAudioResponse`\n// BEFORE the `isContentWithToolCallsResponse` / `isToolCallResponse` / text\n// guards, because the optional companion fields below make these shapes\n// structurally overlap (an AudioResponse with `toolCalls`/`content` would also\n// satisfy those guards otherwise).\nexport interface VoiceDesignPreview {\n generated_voice_id: string;\n // Optional like its siblings below: `voiceDesignToJson` substitutes `\"\"`\n // when it is absent, so requiring it here made that documented fallback\n // unreachable from TypeScript.\n audio_base_64?: string;\n media_type?: string;\n duration_secs?: number;\n language?: string | null;\n}\n\n/**\n * Convenience shape for `LLMock.onElevenLabsVoiceDesign`. Stored as a\n * {@link RawJSONResponse} wrapping `{ previews, text }` — the ElevenLabs\n * `/v1/text-to-voice/design` envelope.\n */\nexport interface VoiceDesignResponse {\n previews: VoiceDesignPreview[];\n text?: string;\n}\n\nexport interface AudioResponse {\n audio: string | { b64Json: string; contentType?: string };\n format?: string;\n /**\n * Companion modalities that can accompany streamed audio. A single Gemini turn\n * may interleave inlineData audio with a functionCall and/or text/thought\n * parts; the recorder preserves them here so the tool call / content / reasoning\n * are not silently discarded when audio is also present.\n */\n toolCalls?: ToolCall[];\n content?: string;\n reasoning?: string;\n}\n\nexport interface TranscriptionResponse {\n transcription: {\n text: string;\n language?: string;\n languages?: Array<{ code: string }>;\n duration?: number;\n usage?: Record<string, unknown>;\n words?: Array<{ word: string; start: number; end: number }>;\n segments?: Array<{ id: number; text: string; start: number; end: number }>;\n };\n}\n\nexport interface VideoResponse {\n video: {\n id: string;\n status: \"processing\" | \"completed\" | \"failed\";\n url?: string;\n /** Failure message surfaced by async video jobs (e.g. OpenRouter `error`). */\n error?: string;\n /** Base64-encoded video bytes served by content-download endpoints. */\n b64?: string;\n /** Generation cost surfaced in usage envelopes (e.g. OpenRouter `usage.cost`). */\n cost?: number;\n /** Clip duration in seconds surfaced by some providers (e.g. Grok `video.duration`). */\n duration?: number;\n };\n}\n\n/**\n * Pass-through JSON response. Used by handlers (e.g. fal.ai) that record\n * arbitrary upstream JSON payloads and replay them verbatim, without the\n * provider-specific shape coercion the other response types impose.\n */\nexport interface RawJSONResponse extends ResponseOverrides {\n json: unknown;\n status?: number;\n /**\n * Billed quantity surfaced on the completed fal `queue-result` response via\n * the `x-fal-billable-units` header (emitted alongside `x-fal-request-id` on\n * the completed result). Real fal sets this header on its queue responses;\n * recent versions of `@tanstack/ai-fal` read it to populate a consumer-side\n * billed-units field (e.g. `usage.unitsBilled`). Omit it to preserve the\n * header-less default — present only to let a fixture opt into exercising a\n * consumer's cost/billing accounting path on replay.\n */\n billableUnits?: number;\n}\n\n/**\n * Pass-through LLM response. aimock sends `events` (streaming) or `json`\n * (non-streaming) as written, adding only the named wire's framing. No\n * overrides, reasoning gating, provider shaping or drift checks apply.\n */\nexport interface RawResponse {\n /**\n * The passthrough body for one wire: `events` for a streaming request,\n * `json` for a non-streaming one, plus optional `status`, `headers` and\n * (`openai-chat` only) `done`. The router offers the fixture only to the\n * handler for `wire`. Checked at load (`validateFixtures`) and again when\n * served; a fixture that fails when served gets a 500 with the code\n * `aimock_raw_fixture_invalid`.\n */\n raw: {\n /**\n * The wire these events belong to. The router skips the fixture on every\n * other wire; a factory that returns it for another wire gets a 500.\n */\n wire: RawWireId;\n /** Streamed body: one entry per frame, each framed per `wire`. */\n events?: Record<string, unknown>[];\n /** Non-streaming body, sent as `JSON.stringify(json)`. */\n json?: unknown;\n /** HTTP status. Default 200. */\n status?: number;\n /**\n * Extra response headers. Without a Content-Type here, a stream gets the\n * wire's own type and a non-streaming reply gets `application/json`.\n */\n headers?: Record<string, string>;\n /** `openai-chat` only: send the terminal `data: [DONE]`. Default true. */\n done?: boolean;\n };\n}\n\n/**\n * The wires a {@link RawResponse} can target: every HTTP LLM wire. Ollama has\n * two: `ollama` is `/api/chat` and `ollama-generate` is `/api/generate`, whose\n * frames have a different shape.\n */\nexport type RawWireId = Exclude<WireId, \"openai-realtime\" | \"gemini-live\"> | \"ollama-generate\";\n\nexport type FixtureResponse =\n | TextResponse\n | ToolCallResponse\n | ContentWithToolCallsResponse\n | ErrorResponse\n | EmbeddingResponse\n | ImageResponse\n | AudioResponse\n | TranscriptionResponse\n | VideoResponse\n | RawJSONResponse\n | RawResponse\n | LiveFixtureResponse;\n\n// GA Realtime session types\n\nexport type RealtimePhase = \"final_answer\" | \"commentary\";\n\nexport interface GASessionAudioConfig {\n voice: string | null;\n input_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;\n output_audio_format: { type: string; rate?: number; [key: string]: unknown } | null;\n input_audio_noise_reduction: { type: string } | null;\n input_audio_transcription: { model: string } | null;\n}\n\nexport interface GASessionConfig {\n model: string;\n modalities: string[];\n instructions: string;\n tools: unknown[];\n temperature: number;\n max_response_output_tokens: number | \"inf\";\n audio: GASessionAudioConfig;\n turn_detection: unknown | null;\n input_audio_transcription: { model: string } | null;\n type: \"conversation\" | \"transcription\" | \"translation\";\n reasoning: { effort: string } | null;\n}\n\n// Streaming physics\n\nexport interface StreamingProfile {\n ttft?: number; // Time to first token (ms)\n tps?: number; // Tokens per second\n jitter?: number; // Random variance factor (0-1), default 0\n}\n\n/**\n * Per-frame arrival timestamps captured during proxy recording.\n * Used during replay to reproduce real-world streaming timing instead of\n * the synthetic model (StreamingProfile / flat latency).\n */\nexport interface RecordedTimings {\n ttftMs: number;\n interChunkDelaysMs: number[];\n totalDurationMs: number;\n}\n\n/**\n * Probabilistic chaos injection rates.\n *\n * Rates are evaluated sequentially per request — drop → malformed → rateLimit\n * → disconnect — as four separate draws in that order, and the first draw that\n * hits wins and short-circuits the rest. Each rate is therefore the probability\n * that its OWN draw hits GIVEN that no earlier rate fired, not the share of\n * requests that end in that fault; only `dropRate`, first in the chain, is\n * unconditional. A config of `{ dropRate: 0.5, malformedRate: 0.5 }` yields a\n * ~25 % effective malformed rate, not 50 %, and the same compounding applies to\n * `{ dropRate: 0.5, rateLimitRate: 0.5 }` (~25 % rate limits).\n */\n/**\n * Probabilistic (and one deterministic) fault injection knobs.\n *\n * VALIDATION — ONE policy, whatever the source. Each field is a finite number\n * in its range: the rates in [0, 1], `latencyMs` in [0, 30000]. A value outside\n * it, or one that is not a number at all (`\"0.5abc\"`, `\"Infinity\"`, `NaN`), is\n * REJECTED, never clamped — silent clamping substitutes a fault rate nobody\n * asked for and hides the misconfiguration that produced it.\n *\n * Rejection is reported in the loudest way each source allows, and the three\n * agree: `POST /__aimock/chaos` answers 400, the `--chaos-*` CLI flags refuse\n * to start, and a per-request header or a fixture/server value is refused with\n * a `[chaos]` warning and left unset so the next level of precedence (header >\n * fixture > server) applies.\n */\nexport interface ChaosConfig {\n dropRate?: number;\n malformedRate?: number;\n disconnectRate?: number;\n /**\n * Deterministic delay (ms) injected before the request is handled.\n * Applied when > 0 — header > fixture > server precedence, same as rates.\n * Unlike rates it is not probabilistic: a set latency always fires so\n * timeout/retry suites get a stable signal. Must be in [0, 30000]; a value\n * outside that range is rejected, not clamped (see above).\n */\n latencyMs?: number;\n /**\n * Chance of a 429 rate-limit rejection with `Retry-After`, CONDITIONAL on\n * neither `dropRate` nor `malformedRate` having fired first: it is the third\n * draw in the drop → malformed → rateLimit → disconnect order, and the first\n * hit wins. Paired with a 0.5 `dropRate` a 0.5 `rateLimitRate` produces ~25 %\n * rate limits, not 50 % (see the interface docstring above).\n */\n rateLimitRate?: number;\n}\n\n/**\n * Server-level chaos as the handlers see it: a baseline plus per-`X-Test-Id`\n * overrides installed at runtime via `POST /__aimock/chaos`.\n *\n * Chaos is the LAST link in the precedence chain (request headers > fixture >\n * server), and this makes that last link test-scoped, the way every other\n * mutable axis in aimock is (fixture match-counts, video job maps). Under the\n * shipped topology — one shared `aimock` process serving a parallel suite — a\n * purely global switch would mean one test's chaos failing every other test.\n *\n * Scoping is by testId on BOTH sides: the traffic being evaluated and the\n * control request that installed the override. Both resolve the tag the way\n * `getTestId` does — the `X-Test-Id` header, else `?testId=` in the query\n * string — so a harness that can only tag one of those two ways still lands in\n * the same scope on both sides, and the two can never silently disagree.\n */\nexport interface ChaosScope {\n /** Server-wide baseline: the construction config, or the untagged override. */\n base?: ChaosConfig;\n /**\n * Per-testId overrides, consulted INSTEAD OF `base` — never merged over it.\n *\n * An installed override REPLACES the baseline wholesale for that testId: a\n * server started with `--chaos-latency 500` whose test `t1` installs\n * `{ dropRate: 1 }` gives `t1` traffic a drop and NO latency, because\n * `latencyMs` is not restated. To keep a baseline field, restate it in the\n * override body.\n *\n * Replacement — not a field-wise merge — is deliberate, and it is what makes\n * `POST /__aimock/chaos {}` (\"explicitly no chaos for my test\") different\n * from `DELETE /__aimock/chaos` (\"forget I said anything, fall back to the\n * baseline\"). Under a merge, `POST {}` would be a no-op and that distinction\n * would collapse. It also matches the untagged case, where an override\n * likewise replaces the construction-time config rather than layering on it.\n *\n * The two chain links ABOVE this one — fixture chaos over server chaos, and\n * request headers over both — DO merge field-wise (see `resolveChaosConfig`).\n * Only the scope lookup replaces, because it selects a config rather than\n * refining one. `GET /__aimock/chaos` with the same testId always reports the\n * config actually in effect for that scope, so the replacement is auditable.\n */\n byTestId?: ReadonlyMap<string, ChaosConfig>;\n}\n\n/**\n * What `evaluateChaos` / `applyChaos` accept as server defaults. A plain\n * `ChaosConfig` still works (it is what external callers pass); the server\n * itself passes a `ChaosScope`.\n */\nexport type ChaosDefaults = ChaosConfig | ChaosScope;\n\nexport type ChaosAction = \"drop\" | \"malformed\" | \"rateLimit\" | \"disconnect\";\n\n// Response factory — allows dynamic fixture responses based on the incoming request\n\nexport type ResponseFactory = (\n req: ChatCompletionRequest,\n) => FixtureResponse | Promise<FixtureResponse>;\n\n// Fixture\n\n/** Provider wire identifiers, independent of endpoint response families. */\nexport type WireId =\n | \"openai-chat\"\n | \"openai-responses\"\n | \"openai-realtime\"\n | \"anthropic\"\n | \"bedrock-invoke\"\n | \"bedrock-converse\"\n | \"gemini\"\n | \"gemini-live\"\n | \"gemini-interactions\"\n | \"cohere\"\n | \"ollama\";\n\ninterface MisbehaviorCommon {\n rate?: number;\n times?: number;\n tool?: string;\n providers?: WireId[];\n}\n\nexport type MisbehaviorFault = MisbehaviorCommon &\n (\n | { fault: \"tool-args-invalid-json\"; style?: \"truncated\" | \"trailing-comma\" | \"single-quotes\" }\n | {\n fault: \"tool-args-schema-violation\";\n violation?:\n | \"missing-required\"\n | \"wrong-type\"\n | \"extra-property\"\n | \"enum-mismatch\"\n | \"not-object\";\n property?: string;\n }\n | { fault: \"tool-unknown-name\"; name?: string }\n | { fault: \"tool-call-id-duplicate\" }\n | { fault: \"stop-length-mid-tool\"; at?: number }\n | { fault: \"empty-response\" }\n | { fault: \"refusal\"; message?: string; category?: string | null }\n | { fault: \"content-filter\" }\n | { fault: \"reasoning-only\"; reasoning?: string }\n );\n\nexport type MisbehaviorFaultId = MisbehaviorFault[\"fault\"];\nexport interface MisbehaviorConfig {\n seed?: number | \"random\";\n faults: MisbehaviorFault[];\n}\n\nexport interface MisbehaviorCounterKey {\n testId: string;\n sourceKey: string;\n entryIndex: number;\n}\nexport interface MisbehaviorCounters {\n getFiringCount(key: MisbehaviorCounterKey): number;\n nextOrdinal(key: MisbehaviorCounterKey): number;\n recordFiring(key: MisbehaviorCounterKey): void;\n clearMisbehaviorCounters(testId?: string): void;\n}\nexport interface MisbehaviorScope {\n baseline?: MisbehaviorConfig;\n byTestId: Map<string, MisbehaviorConfig>;\n}\n\nexport interface Fixture {\n match: FixtureMatch;\n response: FixtureResponse | ResponseFactory;\n latency?: number;\n chunkSize?: number;\n truncateAfterChunks?: number;\n disconnectAfterMs?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /**\n * Opt into OpenRouter's `: OPENROUTER PROCESSING` SSE keepalive comment\n * lines on this fixture's streamed OpenRouter responses (default OFF).\n * A faithful client skips `:`-prefixed lines before JSON-parsing; enabling\n * this exercises that path. No effect on non-streaming or OpenAI responses.\n */\n openRouterProcessing?: boolean;\n /**\n * Opt into `streamingProfile` / `recordedTimings` timing on this fixture's\n * non-streaming (`stream: false`) OpenAI chat completion replies (default OFF).\n * When enabled, the single JSON reply waits about as long as the streamed\n * reply would. Without it, non-streaming replies return immediately. Bare\n * `latency` never delays non-streaming replies. Other providers ignore it; to\n * delay them too, use {@link Fixture.nonStreamingTimingAllProviders}.\n */\n nonStreamingTiming?: boolean;\n /**\n * Opt into `streamingProfile` / `recordedTimings` timing on this fixture's\n * non-streaming (`stream: false`) replies on these providers (default OFF):\n * OpenAI Chat Completions, Anthropic Messages, OpenAI Responses, Gemini and\n * Vertex AI `generateContent`, Gemini Interactions, Cohere v2 chat, Bedrock\n * InvokeModel and Converse, Ollama `/api/chat` and `/api/generate`, and\n * OpenRouter chat completions (`/api/v1/chat/completions`), which is served\n * as OpenAI Chat Completions, so `nonStreamingTiming` also delays it.\n * Audio transcription replies are not delayed. The delay is the same as\n * {@link Fixture.nonStreamingTiming} gives on OpenAI Chat Completions: the\n * single JSON reply waits about as long as the streamed reply would. It\n * needs a `streamingProfile` or `recordedTimings`. Bare `latency` never delays\n * non-streaming replies, and error replies are never delayed. Setting it to\n * `false` does not turn off `nonStreamingTiming: true`.\n */\n nonStreamingTimingAllProviders?: boolean;\n metadata?: {\n systemHash?: string;\n toolsHash?: string;\n };\n}\n\nexport type FixtureOpts = Omit<Fixture, \"match\" | \"response\">;\nexport type EmbeddingFixtureOpts = Pick<FixtureOpts, \"latency\" | \"chaos\">;\n/**\n * Options for the fal queue/run fixture builders. Adds `billableUnits`, which\n * is emitted as the `x-fal-billable-units` header on the completed\n * `queue-result` response (see {@link RawJSONResponse.billableUnits}).\n */\nexport type FalQueueOpts = FixtureOpts & { billableUnits?: number };\n\n// Fixture file format (JSON on disk)\n//\n// File-entry types are intentionally relaxed compared to their runtime\n// counterparts so that fixture authors can write JSON objects where the\n// API ultimately expects a JSON *string*. The fixture loader auto-\n// stringifies these before building the runtime Fixture.\n\nexport interface FixtureFileToolCall {\n name: string;\n /** Accepts a JSON object or array for convenience — the loader will JSON.stringify it. */\n arguments: string | Record<string, unknown> | unknown[];\n id?: string;\n /** OpenAI Responses namespace; see {@link ToolCall.namespace}. */\n namespace?: string;\n}\n\n/** On-disk counterpart of {@link CustomToolCall} (`customToolCalls` entries). */\nexport interface FixtureFileCustomToolCall {\n /** Optional in a file: every `customToolCalls` entry is a custom call. */\n type?: \"custom\";\n name: string;\n /** Free-text input, never parsed or stringified. */\n input: string;\n id?: string;\n namespace?: string;\n}\n\n/**\n * On-disk counterpart of {@link FixtureBlock}. A `toolCall` block's\n * `arguments` is relaxed exactly like {@link FixtureFileToolCall} so authors\n * may write a JSON object/array; the loader JSON.stringifies it into the\n * runtime string form. Normalizes to a {@link FixtureBlock}.\n */\nexport type FixtureFileBlock =\n | { type: \"text\"; text: string }\n | {\n type: \"toolCall\";\n name: string;\n /** Accepts a JSON object or array for convenience — the loader will JSON.stringify it. */\n arguments: string | Record<string, unknown> | unknown[];\n id?: string;\n namespace?: string;\n };\n\n/** On-disk counterpart of {@link ResponsesFixtureBlock} (`responsesBlocks` entries). */\nexport type FixtureFileResponsesBlock =\n | FixtureFileBlock\n | { type: \"customToolCall\"; name: string; input: string; id?: string; namespace?: string };\n\nexport interface FixtureFileToolCallResponse extends ResponseOverrides {\n toolCalls: FixtureFileToolCall[];\n /** See {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: FixtureFileCustomToolCall[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface FixtureFileTextResponse extends ResponseOverrides {\n /** Accepts a JSON object or array (structured output) — the loader will JSON.stringify it. */\n content: string | Record<string, unknown> | unknown[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport interface FixtureFileContentWithToolCallsResponse extends ResponseOverrides {\n /**\n * Accepts a JSON object or array (structured output) — the loader will\n * JSON.stringify it. OPTIONAL only for a BLOCKS-ONLY fixture (mirrors\n * {@link ContentWithToolCallsResponse.content}); required for the\n * legacy/combined shapes.\n */\n content?: string | Record<string, unknown> | unknown[];\n /** See {@link FixtureFileContentWithToolCallsResponse.content} — optional only for the blocks-only shape. */\n toolCalls?: FixtureFileToolCall[];\n /** See {@link ToolCallResponse.customToolCalls}. */\n customToolCalls?: FixtureFileCustomToolCall[];\n /**\n * Optional ordered streaming blocks (mirrors the in-memory\n * {@link ContentWithToolCallsResponse.blocks}). When present, builders stream\n * these in array order (tool-first / interleaved); a `toolCall` block's\n * object `arguments` is auto-stringified just like `toolCalls[].arguments`.\n * Absent → legacy text-first path runs unchanged. Purely additive. Uses the\n * on-disk {@link FixtureFileBlock} shape with relaxed `arguments`. A non-empty\n * `blocks` array alone makes this a first-class blocks-only fixture.\n */\n blocks?: FixtureFileBlock[];\n /** See {@link ContentWithToolCallsResponse.responsesBlocks}. */\n responsesBlocks?: FixtureFileResponsesBlock[];\n reasoning?: string;\n /** Real Anthropic thinking-block signature; see {@link TextResponse.reasoningSignature}. */\n reasoningSignature?: string;\n /** Anthropic redacted_thinking block data; see {@link TextResponse.redactedThinking}. */\n redactedThinking?: string[];\n webSearches?: string[];\n}\n\nexport type FixtureFileResponse =\n | FixtureFileTextResponse\n | FixtureFileToolCallResponse\n | FixtureFileContentWithToolCallsResponse\n | ErrorResponse\n | EmbeddingResponse\n | ImageResponse\n | AudioResponse\n | TranscriptionResponse\n | VideoResponse\n | RawJSONResponse\n | RawResponse\n | LiveFixtureResponse;\n\nexport interface FixtureFile {\n fixtures: FixtureFileEntry[];\n}\n\nexport interface FixtureFileEntry {\n match: {\n userMessage?: string;\n /**\n * String (single substring) or array of strings (all must be present).\n * Mirrors the runtime FixtureMatch.systemMessage but without the RegExp\n * form, which JSON cannot express.\n */\n systemMessage?: string | string[];\n inputText?: string;\n /** Mirrors the exact embedding request match in FixtureMatch. */\n embeddingRequest?: FixtureMatch[\"embeddingRequest\"];\n toolCallId?: string;\n /**\n * Substring matched against the last tool-result message's text content.\n * Mirrors the runtime FixtureMatch.toolResultContains.\n */\n toolResultContains?: string;\n toolName?: string;\n /** Mirrors the runtime FixtureMatch.toolNamespace. */\n toolNamespace?: string;\n model?: string;\n responseFormat?: string;\n sequenceIndex?: number;\n turnIndex?: number;\n /**\n * Turn-scoped tool-result presence gate: matches on whether the CURRENT turn\n * (the messages after the last `role: \"user\"` message) contains a `role: \"tool\"`\n * message, not the whole conversation. Falls back to scanning the whole\n * conversation when the request has no user message. Mirrors the runtime\n * `FixtureMatch.hasToolResult`.\n */\n hasToolResult?: boolean;\n endpoint?:\n | \"chat\"\n | \"image\"\n | \"speech\"\n | \"transcription\"\n | \"translation\"\n | \"video\"\n | \"embedding\"\n | \"audio-gen\"\n | \"elevenlabs-tts\"\n | \"elevenlabs-voice-design\"\n | \"elevenlabs-voice\"\n | \"elevenlabs-voice-get\"\n | \"elevenlabs-voice-delete\"\n | \"fal-audio\"\n | \"fal\"\n | \"realtime\"\n | \"realtime-transcription\"\n | \"realtime-translation\"\n | \"openai-live\";\n context?: string;\n // predicate not supported in JSON files\n };\n response: FixtureFileResponse;\n latency?: number;\n chunkSize?: number;\n truncateAfterChunks?: number;\n disconnectAfterMs?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /** See {@link Fixture.openRouterProcessing}. */\n openRouterProcessing?: boolean;\n /** See {@link Fixture.nonStreamingTiming}. */\n nonStreamingTiming?: boolean;\n /** See {@link Fixture.nonStreamingTimingAllProviders}. */\n nonStreamingTimingAllProviders?: boolean;\n metadata?: {\n systemHash?: string;\n toolsHash?: string;\n };\n}\n\n// Request journal\n\n/**\n * A recorded request body.\n *\n * Most journal entries carry a chat-completion request, but every service\n * journals through one entry type and several of them record a body that is\n * not one: the fine-tuning create payload is recorded verbatim so the journal\n * reports what the caller actually sent, and the journal's own size cap\n * substitutes a truncation marker for an oversized body. Neither shape has a\n * `model`/`messages` pair, so typing the field as `ChatCompletionRequest`\n * alone forces those writers through a cast that claims fields that are not\n * there — the second member exists so they do not have to lie.\n *\n * TypeScript collapses the two for ASSIGNMENT (`ChatCompletionRequest` has an\n * index signature, so it is itself a `Record<string, unknown>`); the union is\n * kept two-membered because it is what the field means, not because it narrows\n * anything. For READING, treat the body as opaque JSON — that is already how\n * every consumer uses it (the journal cap re-serializes it, `GET\n * /__aimock/journal` hands it back) — and property-check before reaching for a\n * chat-request field.\n */\nexport type JournalBody = ChatCompletionRequest | Record<string, unknown>;\n\nexport interface JournalEntry {\n id: string;\n timestamp: number;\n method: string;\n path: string;\n headers: Record<string, string>;\n body: JournalBody | null;\n service?: string;\n /**\n * Resolved test id of an MCP request (`\"__default__\"` when none), for an\n * MCP mount to set. Journal test-id filtering does not read it: it\n * re-derives the id from the headers and path (`journalEntryTestId`).\n */\n testId?: string;\n /** Resolved context of an MCP request, `null` when none; for an MCP mount to set. */\n context?: string | null;\n response: {\n status: number;\n fixture: Fixture | null;\n /** For an MCP mount to set on `tools/call` entries a fake answered or failed. */\n mcpFake?: { id: string | null; outcome: McpFakeOutcome };\n /**\n * What was going to serve this request. \"fixture\" = a fixture matched (or\n * would have, before chaos intervened). \"proxy\" = no fixture matched and\n * proxy was configured. \"internal\" = entries where the request was served\n * by aimock's own synthetic logic — neither a matched fixture nor a\n * configured proxy: chaos-path entries (e.g. chaos on the OpenRouter\n * video lifecycle endpoints; in REPLAY mode their normal 200/400/401/404\n * entries omit source, while record-mode 200s on those endpoints carry\n * source:\"proxy\"), the OpenRouter video models listing synthesized as the\n * fallback after a FAILED proxy attempt, and the services whose responses\n * are synthesized outright — every fine-tuning entry carries it, success\n * and error alike, since that store is aimock's own and no fixture or\n * proxy ever serves it. Absent when the distinction doesn't apply\n * (e.g. 404/503 fallback where nothing was going to serve).\n */\n source?: \"fixture\" | \"proxy\" | \"internal\";\n interrupted?: boolean;\n interruptReason?: string;\n /**\n * The error message of a failed request, set in two cases:\n * - The request failed before any content was sent, and `status` is 500:\n * a fixture tool-call error (HTTP or WebSocket), or any builder error on\n * the WebSocket Responses transport.\n * - The handler crashed AFTER the response completed. The client received\n * `status` in full, so this is not an interruption.\n */\n error?: string;\n chaosAction?: ChaosAction;\n /** Full prepared fault output; interrupted delivery is recorded separately above. */\n misbehavior?: MisbehaviorSummary;\n /** When the X-AIMock-Strict header overrode the server default. */\n strictOverride?: boolean;\n /** MR8 / MR11 / MR14: why a forwarded MCP record-mode message was not recorded. */\n recordSkipped?: string;\n };\n}\n\n// SSE chunk types (OpenAI format)\n\n/**\n * OpenRouter-only additions layered onto a `usage` object when a response is\n * shaped for the OpenRouter `/api/v1/...` base. `cost`/`cost_details` are\n * NULLABLE and emitted only when a fixture supplies a cost (default: omitted).\n * When `cost_details` is emitted it carries all three upstream fields (partial\n * objects break the canonical SDK). The detail breakdowns and `is_byok` appear\n * only when a fixture overrides them; an emitted `prompt_tokens_details` always\n * carries `cached_tokens` and an emitted `completion_tokens_details` always\n * carries `reasoning_tokens` (required-if-present in the AI SDK provider).\n */\nexport interface OpenRouterUsageExtras {\n cost?: number;\n cost_details?: {\n upstream_inference_cost: number;\n upstream_inference_prompt_cost: number;\n upstream_inference_completions_cost: number;\n [key: string]: unknown;\n };\n prompt_tokens_details?: {\n cached_tokens: number;\n cache_write_tokens?: number;\n audio_tokens?: number;\n [key: string]: unknown;\n };\n completion_tokens_details?: { reasoning_tokens: number; [key: string]: unknown };\n is_byok?: boolean;\n /**\n * Provider usage fields aimock does not model, passed through verbatim from a\n * fixture's `response.usage` (typically written by the recorder from a real\n * upstream usage frame — e.g. OpenRouter `native_tokens_prompt`).\n */\n [key: string]: unknown;\n}\n\nexport interface SSEChunk {\n id: string;\n object: \"chat.completion.chunk\";\n created: number;\n model: string;\n choices: SSEChoice[];\n /** Nullable: OpenRouter shaping emits this on every chunk (null when unset). */\n system_fingerprint?: string | null;\n usage?: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n } & OpenRouterUsageExtras;\n /** OpenRouter top-level provider that served the chunk. OpenRouter shaping only. */\n provider?: string;\n /** OpenRouter emits `service_tier: null` on the final (usage-bearing) chunk. Shaping only. */\n service_tier?: string | null;\n}\n\nexport interface SSEChoice {\n index: number;\n delta: SSEDelta;\n logprobs: null;\n finish_reason: string | null;\n /** OpenRouter raw upstream finish reason, alongside the normalized one. */\n native_finish_reason?: string | null;\n}\n\n/** OpenRouter text reasoning detail, paired with the same logical reasoning text. */\nexport interface ReasoningTextDetail {\n type: \"reasoning.text\";\n text: string;\n format: \"unknown\";\n index: number;\n}\n\nexport interface SSEDelta {\n role?: string;\n content?: string | null;\n reasoning?: string;\n reasoning_details?: ReasoningTextDetail[];\n reasoning_content?: string;\n tool_calls?: SSEToolCallDelta[];\n /**\n * Fixture URL `citations`, sent once on their own chunk just before the\n * finish chunk: after the content chunks and after any tool-call chunks.\n */\n annotations?: ChatCompletionAnnotation[];\n}\n\n/** A Chat Completions `url_citation` annotation (`message.annotations` / `delta.annotations`). */\nexport interface ChatCompletionAnnotation {\n type: \"url_citation\";\n url_citation: { url: string; title: string; start_index: number; end_index: number };\n}\n\nexport interface SSEToolCallDelta {\n index: number;\n id?: string;\n type?: \"function\";\n function?: { name?: string; arguments?: string };\n}\n\n// Non-streaming completion response types (OpenAI format)\n\nexport interface ChatCompletion {\n id: string;\n object: \"chat.completion\";\n created: number;\n model: string;\n choices: ChatCompletionChoice[];\n usage: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n } & OpenRouterUsageExtras;\n /**\n * Nullable: OpenRouter shaping always emits this on the non-streaming\n * response (null when no override) because the canonical `@openrouter/sdk`\n * requires the field. OpenAI responses omit it unless overridden.\n */\n system_fingerprint?: string | null;\n /** OpenRouter top-level provider that served the request. OpenRouter shaping only. */\n provider?: string;\n /** OpenRouter emits `service_tier: null` on the non-streaming response. Shaping only. */\n service_tier?: string | null;\n}\n\nexport interface ChatCompletionChoice {\n index: number;\n message: ChatCompletionMessage;\n logprobs: null;\n finish_reason: string;\n /** OpenRouter raw upstream finish reason, alongside the normalized one. */\n native_finish_reason?: string;\n}\n\nexport interface ChatCompletionMessage {\n role: string;\n content: string | null;\n refusal: string | null;\n reasoning_details?: ReasoningTextDetail[];\n reasoning_content?: string;\n /**\n * OpenRouter emits `reasoning` (null when absent) on the response message.\n * OpenRouter shaping mirrors `reasoning_content` here (or null). Omitted for\n * OpenAI responses.\n */\n reasoning?: string | null;\n tool_calls?: ToolCallMessage[];\n /**\n * Fixture URL `citations`; omitted when the fixture sets none, or only file\n * citations (Chat Completions has no `file_citation` annotation).\n */\n annotations?: ChatCompletionAnnotation[];\n}\n\n// Server options\n\nexport type RecordProviderKey =\n | \"openai\"\n | \"anthropic\"\n | \"gemini\"\n | \"gemini-interactions\"\n | \"vertexai\"\n | \"bedrock\"\n | \"azure\"\n | \"ollama\"\n | \"cohere\"\n | \"elevenlabs\"\n | \"fal\"\n | \"openrouter\"\n | \"veo\"\n | \"grok\"\n | \"byteplus\";\n\nexport interface RecordConfig {\n providers: Partial<Record<RecordProviderKey, string>>;\n /**\n * aimock's own built-in upstream API keys, keyed by provider. Opt-in and\n * backward-compatible: when set for a provider, aimock injects the key on a\n * fixture-miss passthrough IF the caller sent no credential or a dummy\n * placeholder (see `applyProviderAuth`); a real caller key always overrides.\n * Only static-key providers are eligible: OpenAI/OpenRouter/Cohere/Grok/BytePlus/Ollama\n * (bearer), Anthropic (x-api-key), Gemini/Gemini-Interactions/Veo\n * (x-goog-api-key), Azure OpenAI (api-key), ElevenLabs (xi-api-key), and fal\n * (Authorization: Key). Signed/OAuth providers (Bedrock SigV4, Vertex AI\n * OAuth) are never rewritten. Sourced from AIMOCK_PROVIDER_*_KEY env vars by\n * the CLI (secrets stay out of `ps`).\n */\n providerKeys?: Partial<Record<RecordProviderKey, string>>;\n fixturePath?: string;\n /** Proxy unmatched requests without saving fixtures or caching in memory. */\n proxyOnly?: boolean;\n /**\n * When true, record the exact model version string returned by the provider\n * (e.g. \"gpt-4o-2024-08-06\") instead of stripping the date suffix to a\n * canonical alias (e.g. \"gpt-4o\"). Default: false.\n */\n recordFullModelVersion?: boolean;\n /**\n * fal-specific recording knobs for the queue-walk recorder. During recording\n * the queue handler POSTs submit, polls `status_url` until COMPLETED, then\n * GETs `response_url` for the final job body — saved as the fixture. Tune\n * the poll cadence and timeout here if upstream is unusually slow or fast.\n */\n fal?: FalRecordConfig;\n /**\n * OpenRouter-video-specific recording knobs for the live lifecycle proxy on\n * `/api/v1/videos` (submit/poll proxied 1:1; completed jobs are captured\n * eagerly as fixtures).\n */\n openRouterVideo?: OpenRouterVideoRecordConfig;\n /**\n * Connection idle timeout (ms) on the upstream request socket — fires if the\n * socket is inactive for this duration at any point before the response body\n * begins. Default: 30_000 (30s). Increase for upstreams with slow initial\n * responses (reasoning models, queue-backed providers).\n *\n * Nuance on the OpenRouter video surface: the small-JSON lifecycle fetches\n * (submit, status poll, models listing) apply this value as a TOTAL\n * deadline on the whole fetch — indistinguishable from an idle timeout for\n * envelope-sized bodies. The byte-bearing content fetches (eager capture,\n * proxy-only content relay) gate only the response HEADERS on this value;\n * their body progress is governed by `bodyTimeoutMs` idle semantics.\n *\n * Ceiling note: the fetch-based OpenRouter proxy paths run on Node's\n * built-in undici dispatcher, which carries its own ~300s\n * headers/body-timeout defaults — values above that ceiling do not take\n * effect on those paths without installing a custom undici dispatcher\n * (out of aimock's scope).\n */\n upstreamTimeoutMs?: number;\n /**\n * Idle timeout (ms) on the upstream response body — fires if the upstream\n * goes silent (no bytes) for this long after the response has started.\n * Default: 30_000 (30s). Reasoning models under concurrent load can leave\n * 30s+ gaps between streaming chunks while the model is thinking; lift this\n * to e.g. 180_000 in those setups. The OpenRouter video content fetches use\n * the same idle semantics: the timer re-arms on every chunk, so a steadily\n * downloading multi-minute video never times out — only a silent stall does.\n */\n bodyTimeoutMs?: number;\n /**\n * Maximum number of bytes aimock will accumulate in memory from a single\n * proxied upstream response on the record/proxy path. The full response is\n * still relayed to the client byte-for-byte; this cap only bounds the\n * in-memory buffer used to collapse/journal the response. Once the cap is\n * exceeded aimock stops appending to the buffer, marks the response as\n * truncated, and skips collapse/recording — preventing both unbounded heap\n * growth and the `RangeError: Invalid string length` that a >512MB string\n * would otherwise throw. Default: 64 MiB.\n */\n maxProxyBufferBytes?: number;\n /**\n * Maximum number of SSE/NDJSON/EventStream frames whose per-frame state\n * (`frameTimestamps`, parse buffers) aimock retains for a single proxied\n * response. Frame state is count-indexed, not byte-sized, so a long-lived or\n * never-ending stream accumulates it unbounded even when `maxProxyBufferBytes`\n * is generous. Tripping truncation on EITHER bytes OR frame count bounds both;\n * on the trip the accumulated frame state is freed and collapse/recording is\n * skipped (the full response is still relayed to the client byte-for-byte).\n * Default: 5,000,000 frames.\n */\n maxProxyBufferFrames?: number;\n}\n\nexport interface OpenRouterVideoRecordConfig {\n /**\n * Maximum decoded video size (bytes) embedded as `b64` in a recorded\n * fixture. Captures larger than the cap are persisted WITHOUT `b64` (with a\n * `_warning` in the fixture file) so a multi-hundred-MB video cannot bloat\n * the fixture directory. The cap is also a memory guard: an upstream\n * response that DECLARES an over-cap Content-Length is skipped without\n * downloading, and a response with no declared length is streamed with the\n * byte count enforced during the read — on exceed the download is aborted\n * and nothing oversized is retained in memory. In both cases the\n * same-session job serves the placeholder MP4. Default: 33554432 (32 MB\n * decoded). Set 0 for unlimited. Negative or non-integer values are treated\n * as the default (createServer warns at startup).\n */\n maxContentBytes?: number;\n}\n\nexport interface FalRecordConfig {\n /** Interval between status polls upstream during recording. Default: 1000ms. */\n pollIntervalMs?: number;\n /** Total budget for an upstream queue walk before aborting. Default: 900000ms (15 min) to accommodate video generation. */\n timeoutMs?: number;\n}\n\n/** See {@link MockServerOptions.responsesTools}. */\nexport type ResponsesToolsMode = \"legacy\" | \"extended\";\n\nexport interface AimockPolicy {\n /** Reject requests missing a provider's required headers. Default \"off\"; independent of strict. */\n requiredHeaders?: \"off\" | \"enforce\";\n /**\n * Inbound-auth 401 body. Default \"legacy\" keeps the 1.45 body on every route:\n * `{\"error\":{\"message\",\"type\":\"authentication_error\",\"code\":\"invalid_api_key\"}}`.\n * \"native\" returns provider-native bodies on Anthropic Messages/count_tokens and\n * Gemini/Vertex routes. WebSocket upgrade rejections always use the legacy body.\n */\n authErrors?: \"legacy\" | \"native\";\n /**\n * Shared `GET /v1/models` list body format. Default \"legacy\" keeps the 1.45\n * OpenAI-format list for every request. \"native\" returns the Anthropic-format list\n * when the request sends `anthropic-version`, or `x-api-key` without `Authorization`.\n * `GET /v1/models/{id}` and the Gemini catalog routes always select by request.\n */\n modelCatalog?: \"legacy\" | \"native\";\n}\n\nexport interface MockServerOptions {\n live?: LiveOptions;\n /** Optional inbound test-client access keys. Omit to preserve permissive behavior. */\n auth?: ApiKeyAuthConfig;\n port?: number;\n host?: string;\n latency?: number;\n chunkSize?: number;\n replaySpeed?: number;\n /** Log verbosity. CLI default is \"info\"; programmatic default (when omitted) is \"silent\". */\n logLevel?: \"silent\" | \"warn\" | \"info\" | \"debug\";\n /**\n * Opt in to model misbehavior. Without it, the `misbehavior` option, a\n * fixture's `misbehavior` key and the `X-AIMock-Misbehavior` header are\n * ignored, as in 1.44.0. CLI: `--misbehavior`.\n *\n * When on, addFixture, addFixtures and prependFixture store a per-addition\n * copy, so getFixtures()[i] is the copy. findByFixture, the match-count APIs\n * and journal entry.response.fixture still name the caller's object. A field\n * assigned on that object later (for example `fixture.response = ...`) has no\n * effect; add the fixture again. When off, the caller's object is stored.\n */\n enableMisbehavior?: boolean;\n /** Server-wide misbehavior baseline; read only with `enableMisbehavior: true`. */\n misbehavior?: MisbehaviorConfig | MisbehaviorFaultId;\n chaos?: ChaosConfig;\n /** Enable Prometheus-compatible /metrics endpoint. */\n metrics?: boolean;\n /** Strict mode: return 503 instead of 404 when no fixture matches. */\n strict?: boolean;\n /** Optional request policy. Invalid entries warn and are ignored. */\n policy?: AimockPolicy;\n /**\n * Reject fixture tool calls whose `arguments` are not valid JSON instead of\n * serving them as `{}`. Off by default (the 1.44.0 behavior). When on, wires\n * that carry arguments as an object (Anthropic and Bedrock non-streaming,\n * Converse non-streaming, Gemini, Vertex AI, Gemini Interactions\n * non-streaming, Gemini Live, Ollama) answer 500 `aimock_invalid_tool_arguments`\n * (Gemini Live: an error frame), and wires that carry them as a string\n * (Cohere and the streaming Anthropic, Bedrock, Converse and Gemini\n * Interactions paths) serve the authored text unchanged.\n */\n strictToolArguments?: boolean;\n /**\n * OpenAI Responses tool handling. `\"legacy\"` (the default) matches,\n * counts and journals Responses requests exactly as earlier releases did:\n * only top-level function tools are visible, custom tool call history is not\n * counted, and a `namespace` on a `toolCalls` entry or `toolCall` block is\n * not emitted. `\"extended\"` makes namespaced, custom and `additional_tools`\n * / `tool_search_output` tools visible to `toolName` and predicates, counts\n * custom tool call rounds for `turnIndex` / `hasToolResult` / `toolCallId`,\n * emits fixture namespaces, applies `match.toolNamespace`,\n * `customToolCalls` and `responsesBlocks` (which `\"legacy\"` ignores), and\n * makes the recorder keep namespaces and custom tool calls. The mode is\n * fixed when the server starts; changing `responsesTools` on the options\n * object after `createServer` or `LLMock.start()` is unsupported.\n */\n responsesTools?: ResponsesToolsMode;\n /** Record-and-replay: proxy unmatched requests to upstream and save fixtures. */\n record?: RecordConfig;\n /**\n * Maximum number of request/response entries to retain in the in-memory\n * journal. Oldest entries are dropped FIFO when the cap is exceeded.\n * Set to 0 (or omit) for unbounded retention. Negative values are\n * rejected at the CLI parse layer; programmatically they are treated\n * as 0 (unbounded) for back-compat.\n *\n * Default: 1000 (applied by `createServer` when omitted). The CLI passes\n * through its own default. Short-lived test harnesses that want every\n * request recorded can opt in to unbounded retention by passing 0.\n */\n journalMaxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the journal's fixture\n * match-count map. Oldest testIds are dropped FIFO when the cap is\n * exceeded. Set to 0 (or omit) for unbounded retention. Negative values\n * are rejected at the CLI parse layer; programmatically they are treated\n * as 0 (unbounded) for back-compat.\n *\n * Default: 500 (applied by `createServer` when omitted). Without a cap\n * this map can grow over time in long-running servers that see many\n * unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n /**\n * Normalize requests before matching and recording. Useful for stripping\n * dynamic data (timestamps, UUIDs, session IDs) that would cause fixture\n * mismatches on replay.\n *\n * When set, string matching for `userMessage` and `inputText` uses exact\n * equality (`===`) instead of substring (`includes`) to prevent false\n * positives from shortened keys.\n */\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n /**\n * Configure fal.ai queue polling progression. By default a job completes\n * on submit (preserves the legacy `COMPLETED`-on-submit shape). Opt into a\n * realistic `IN_QUEUE → IN_PROGRESS → COMPLETED` progression by setting\n * positive poll thresholds — useful for exercising client code that polls\n * `/status` and reacts to intermediate states.\n *\n * Applies to the general fal handler (`x-fal-target-host`-routed); the\n * legacy `/fal/queue/...` audio handler is unaffected.\n */\n falQueue?: FalQueueConfig;\n /**\n * Configure OpenRouter async video job polling progression\n * (`pending → in_progress → completed | failed` on `GET /api/v1/videos/{id}`).\n * Same threshold semantics as `falQueue`. By default (0/0) the job is\n * seeded terminal at submit — content is downloadable with zero polls, and\n * the first status poll merely reports the already-terminal status.\n */\n openRouterVideo?: FalQueueConfig;\n /**\n * Configure Google Veo async video job polling progression\n * (`done:false → done:true` on `GET /v1beta/operations/{name}`). Same\n * threshold semantics as `openRouterVideo`; the internal\n * `pending → in_progress → completed | failed` model is serialized to the\n * two-state Veo wire.\n */\n veoVideo?: FalQueueConfig;\n /**\n * Configure xAI Grok Imagine async video job polling progression\n * (`pending → done | failed` on `GET /v1/videos/{request_id}`). Same\n * threshold semantics as `openRouterVideo`; progress is synthesized from the\n * poll count.\n */\n grokVideo?: FalQueueConfig;\n /**\n * Configure BytePlus Ark (Seedance) async video task polling progression\n * (`queued → running → <recorded terminal status>` on\n * `GET /api/v3/contents/generations/tasks/{id}`). Same threshold semantics as\n * `grokVideo`; unlike the others the TERMINAL status is not synthesized — it\n * is whatever the recorded/authored envelope carries.\n */\n bytePlusVideo?: FalQueueConfig;\n}\n\nexport interface ApiKeyAuthConfig {\n apiKeys: readonly string[];\n}\n\n/**\n * Poll-progression thresholds, documented below in fal queue terms; when used\n * as `openRouterVideo` the states map to `pending` / `in_progress` /\n * `completed | failed`.\n */\nexport interface FalQueueConfig {\n /**\n * Status polls before transitioning `IN_QUEUE → IN_PROGRESS`. Unset and an\n * explicit `0` differ: when BOTH fields are unset the job completes\n * synchronously on submit (no IN_QUEUE / IN_PROGRESS polls emitted), but\n * explicitly setting this field — even to `0` — enables progression when\n * `pollsBeforeCompleted` is unset, with `pollsBeforeCompleted` defaulting\n * to `pollsBeforeInProgress + 1` so the job passes through IN_PROGRESS\n * (an explicit `pollsBeforeCompleted: 0` completes on submit only when\n * `pollsBeforeInProgress` is `0` or unset — otherwise the clamp below\n * lifts it to `pollsBeforeInProgress`).\n */\n pollsBeforeInProgress?: number;\n /**\n * Status polls before transitioning to `COMPLETED`. Default: 0 when\n * `pollsBeforeInProgress` is also unset (no progression), otherwise\n * `pollsBeforeInProgress + 1` so the job spends one poll in IN_PROGRESS.\n * An explicit value lower than `pollsBeforeInProgress` is clamped up so\n * IN_PROGRESS is never skipped. When equal to a nonzero\n * `pollsBeforeInProgress` — or when `pollsBeforeInProgress` is unset or 0\n * and this field is 1 (the IN_PROGRESS branch consumes the first poll) —\n * the job still spends one poll in IN_PROGRESS, so the terminal status\n * lands one poll later than configured.\n */\n pollsBeforeCompleted?: number;\n}\n\n// Handler defaults — the common shape passed from server.ts to every handler\n\nexport interface HandlerDefaults {\n /** Present only when misbehavior is enabled; without it no fault is planned. */\n misbehavior?: MisbehaviorScope;\n misbehaviorCounters?: MisbehaviorCounters;\n latency: number;\n chunkSize: number;\n replaySpeed: number;\n logger: Logger;\n /**\n * Reads resolve to a `ChaosScope` on a real server (baseline + per-testId\n * overrides); assigning `undefined` drops every runtime override.\n */\n chaos?: ChaosDefaults;\n /**\n * Live per-testId chaos overrides, owned by the server instance so the\n * control API can install one without reaching through the getter.\n */\n chaosByTestId?: Map<string, ChaosConfig>;\n registry?: MetricsRegistry;\n record?: RecordConfig;\n strict?: boolean;\n /** See {@link MockServerOptions.policy}. Request headers override each valid key. */\n policy?: AimockPolicy;\n /** See {@link MockServerOptions.strictToolArguments}. */\n strictToolArguments?: boolean;\n /** See {@link MockServerOptions.responsesTools}. Absent means `\"legacy\"`. */\n responsesTools?: ResponsesToolsMode;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n falQueue?: FalQueueConfig;\n openRouterVideo?: FalQueueConfig;\n veoVideo?: FalQueueConfig;\n grokVideo?: FalQueueConfig;\n /**\n * Configure BytePlus Ark (Seedance) async video task polling progression\n * (`queued → running → <recorded terminal status>` on\n * `GET /api/v3/contents/generations/tasks/{id}`). Same threshold semantics as\n * `grokVideo`; unlike the others the TERMINAL status is not synthesized — it\n * is whatever the recorded/authored envelope carries.\n */\n bytePlusVideo?: FalQueueConfig;\n}\n\n// MCP fakes — scenario-scoped MCP tool fakes in fixture files (`mcpFakes`)\n\n/**\n * Block scope: `\"shared\"`, or exact test id and/or context (at least one;\n * `{}` is not a scope). Readonly: validated scopes are frozen.\n */\nexport type McpFakeScope =\n | \"shared\"\n | Readonly<{ testId: string; context?: string }>\n | Readonly<{ testId?: string; context: string }>;\n\n/**\n * `T` with every nested property and array read-only: the type of a value the\n * MCP fakes store deep-freezes before it hands it out.\n */\nexport type McpFakeDeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends object\n ? { readonly [K in keyof T]: McpFakeDeepReadonly<T[K]> }\n : T;\n\n/** MCP `Annotations` on a content block. */\nexport interface McpFakeAnnotations {\n audience?: Array<\"user\" | \"assistant\">;\n /** From 0 (least) to 1 (most important). */\n priority?: number;\n /** RFC 3339 date-time with seconds and a `Z` or `±hh:mm` offset. */\n lastModified?: string;\n}\n\n/** MCP `Icon` on a `resource_link` content block. */\nexport interface McpFakeIcon {\n src: string;\n mimeType?: string;\n sizes?: string[];\n theme?: \"light\" | \"dark\";\n}\n\n/** MCP `TextResourceContents` / `BlobResourceContents`: exactly one of `text` / `blob`. */\nexport type McpFakeResourceContents = {\n uri: string;\n mimeType?: string;\n _meta?: Record<string, unknown>;\n} & ({ text: string; blob?: never } | { blob: string; text?: never });\n\ntype McpFakeContentCommon = {\n annotations?: McpFakeAnnotations;\n _meta?: Record<string, unknown>;\n};\n\n/** One MCP content block of a tool result (`ContentBlock`); `data` and `blob` are base64. */\nexport type McpFakeContentBlock = McpFakeContentCommon &\n (\n | { type: \"text\"; text: string }\n | { type: \"image\"; data: string; mimeType: string }\n | { type: \"audio\"; data: string; mimeType: string }\n | {\n type: \"resource_link\";\n uri: string;\n name: string;\n title?: string;\n description?: string;\n mimeType?: string;\n size?: number;\n icons?: McpFakeIcon[];\n }\n | { type: \"resource\"; resource: McpFakeResourceContents }\n );\n\n/** A full MCP `CallToolResult`, served unchanged. No other keys. */\nexport interface McpFakeFullResult {\n content: McpFakeContentBlock[];\n isError?: boolean;\n structuredContent?: Record<string, unknown>;\n _meta?: Record<string, unknown>;\n}\n\n/**\n * Structured data: any JSON object without the `CallToolResult`\n * keys, served as text plus `structuredContent`.\n */\nexport type McpFakeStructuredResult = Record<string, unknown> & {\n content?: never;\n isError?: never;\n structuredContent?: never;\n _meta?: never;\n};\n\n/**\n * A call entry's `result` value. Validation also checks what the type cannot\n * say: base64 `data` / `blob`, `priority` from 0 to 1, the `lastModified`\n * format, and plain JSON throughout. Unknown keys inside a content item are\n * allowed and served as given.\n */\nexport type McpFakeResult =\n | string\n | McpFakeContentBlock[]\n | McpFakeFullResult\n | McpFakeStructuredResult;\n\n/** How a call entry matches: exactly one of `args` / `anyArgs`. */\nexport type McpFakeCallMatch =\n | { args: Record<string, unknown>; anyArgs?: never }\n | { anyArgs: true; args?: never };\n\n/** What a call entry answers: exactly one of `result` / `error`. */\nexport type McpFakeCallAnswer =\n | { result: McpFakeResult; error?: never }\n | { error: string; result?: never };\n\n/** One ordered answer for a tool. */\nexport type McpFakeCall = McpFakeCallMatch &\n McpFakeCallAnswer & {\n id?: string;\n /** FA3: the notifications the upstream sent on the call's stream before the response. */\n notifications?: McpFakeNotification[];\n /** FA4: the recorded response time in ms (a number >= 0). */\n durationMs?: number;\n };\n\n/** One faked tool in a block. */\nexport interface McpFakeTool {\n name: string;\n description?: string;\n inputSchema?: Record<string, unknown>;\n calls: McpFakeCall[];\n}\n\n/** One `mcpFakes` block. */\nexport interface McpFakeBlock {\n scope: McpFakeScope;\n mount?: string;\n undeclaredTools?: McpFakeUndeclaredPolicy;\n tools: McpFakeTool[];\n /** FA1: the recorded `tools/list` (MCP `Tool` objects, kept verbatim). */\n list?: Record<string, unknown>[];\n /** FA2: provenance of a recording. Never used for matching. */\n recorded?: McpFakeRecorded;\n /** FA5: `\"recorded\"` (default) plays FA3/FA4 timing; `\"immediate\"` ignores it. */\n timing?: \"recorded\" | \"immediate\";\n}\n\n/** FA2 `recorded`: where and when a block was recorded. */\nexport interface McpFakeRecorded {\n /** The upstream origin only. */\n upstream: string;\n protocolVersion: string;\n serverInfo?: Record<string, unknown>;\n aimockVersion: string;\n /** ISO 8601 date-time with seconds and an offset. */\n at: string;\n}\n\n/** MR1 `MCPMock.enableRecording` options. */\nexport interface McpRecordConfig {\n upstream: string;\n fixturePath?: string;\n proxyOnly?: boolean;\n maxRecordBufferBytes?: number;\n /** MR14 `\"Name: value\"`; env AIMOCK_MCP_UPSTREAM_AUTH (AM6). */\n upstreamAuth?: string;\n /** S2 (d); env AIMOCK_RECORD_SECRET_VALUES (newline-separated). */\n secretValues?: string[];\n /** MR5 (b): the server's strict default; X-AIMock-Strict still overrides per request. */\n strict?: boolean;\n}\n\n/** FA3: one recorded notification of a call entry. */\nexport interface McpFakeNotification {\n atMs: number;\n method: string;\n params: Record<string, unknown>;\n}\n\nexport type McpFakeUndeclaredPolicy = \"allow\" | \"deny\";\n\n/**\n * The resolved identity of one MCP request: test id, context and undeclared\n * override, each from the request or else from the session.\n * `null` means \"not supplied\". The identity shape the fake store's matching\n * methods take (`applicable`, `selectTier`, `claim`, `policy`, `declaredTools`,\n * `listTools`).\n */\nexport interface McpFakeIdentity {\n testId: string | null;\n context: string | null;\n undeclared: McpFakeUndeclaredPolicy | null;\n}\n\n/**\n * One raw block as a loader hands it off, before validation. `source` is the\n * entry-id source string; `blockIndex` is `null` when `mcpFakes` is a single object.\n */\nexport interface McpFakeSource {\n source: string;\n blockIndex: number | null;\n raw: unknown;\n}\n\n/**\n * `response.mcpFake.outcome` on a `tools/call` journal entry. `evicted`:\n * the request's test id lost its consumption state to the FIFO test-id cap.\n */\nexport type McpFakeOutcome = \"answered\" | \"mismatch\" | \"exhausted\" | \"not_declared\" | \"evicted\";\n\n/**\n * JSON-RPC error codes of the four fake failures, keyed by\n * `error.data.aimock.code`. `-31010` and `-31011` sit outside the JSON-RPC\n * reserved range. `MCP_FAKE_EVICTED` is for a lookup of a declared tool by a\n * test id whose state the FIFO test-id cap evicted, until it is reset.\n */\nexport const MCP_FAKE_ERROR_CODES = {\n MCP_FAKE_NOT_DECLARED: -32602,\n MCP_FAKE_MISMATCH: -32602,\n MCP_FAKE_EXHAUSTED: -31010,\n MCP_FAKE_EVICTED: -31011,\n} as const;\n\nexport type McpFakeErrorCode = keyof typeof MCP_FAKE_ERROR_CODES;\n"],"mappings":";;;;;;;AAojEA,MAAa,uBAAuB;CAClC,uBAAuB;CACvB,mBAAmB;CACnB,oBAAoB;CACpB,kBAAkB;CACnB"}
|
package/dist/vector-mock.cjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
const require_errors = require('./response/errors.cjs');
|
|
2
3
|
const require_helpers = require('./helpers.cjs');
|
|
3
4
|
const require_vector_handler = require('./vector-handler.cjs');
|
|
4
5
|
let node_http = require("node:http");
|
|
@@ -88,10 +89,7 @@ var VectorMock = class {
|
|
|
88
89
|
}
|
|
89
90
|
});
|
|
90
91
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
91
|
-
res.end(JSON.stringify({
|
|
92
|
-
message: "Request body must be a JSON object",
|
|
93
|
-
type: "invalid_request_error"
|
|
94
|
-
} }));
|
|
92
|
+
res.end(JSON.stringify(require_errors.buildOpenAIError("service-validation", { message: "Request body must be a JSON object" })));
|
|
95
93
|
return true;
|
|
96
94
|
}
|
|
97
95
|
}
|
|
@@ -190,10 +188,7 @@ var VectorMock = class {
|
|
|
190
188
|
}
|
|
191
189
|
});
|
|
192
190
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
193
|
-
res.end(JSON.stringify({
|
|
194
|
-
message: "Request body must be a JSON object",
|
|
195
|
-
type: "invalid_request_error"
|
|
196
|
-
} }));
|
|
191
|
+
res.end(JSON.stringify(require_errors.buildOpenAIError("service-validation", { message: "Request body must be a JSON object" })));
|
|
197
192
|
return;
|
|
198
193
|
}
|
|
199
194
|
const url = new URL(req.url ?? "/", `http://${host}`);
|
package/dist/vector-mock.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-mock.cjs","names":["readBody","flattenHeaders","isJsonObject","http","createVectorRequestHandler"],"sources":["../src/vector-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport type { Mountable, JournalEntry } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n VectorMockOptions,\n VectorCollection,\n VectorEntry,\n QueryResult,\n VectorQuery,\n QueryHandler,\n} from \"./vector-types.js\";\nimport { createVectorRequestHandler, type VectorState } from \"./vector-handler.js\";\nimport { flattenHeaders, isJsonObject, readBody } from \"./helpers.js\";\n\nexport class VectorMock implements Mountable {\n private collections: Map<string, VectorCollection> = new Map();\n private queryHandlers: Map<string, QueryHandler> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: VectorMockOptions;\n private requestHandler: ReturnType<typeof createVectorRequestHandler>;\n\n constructor(options?: VectorMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration ----\n\n addCollection(name: string, opts: { dimension: number }): this {\n const collection: VectorCollection = {\n name,\n dimension: opts.dimension,\n vectors: new Map(),\n };\n this.collections.set(name, collection);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n upsert(collection: string, vectors: VectorEntry[]): this {\n let col = this.collections.get(collection);\n if (!col) {\n const dim = vectors.length > 0 ? vectors[0].values.length : 0;\n col = { name: collection, dimension: dim, vectors: new Map() };\n this.collections.set(collection, col);\n }\n for (const v of vectors) {\n col.vectors.set(v.id, v);\n }\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n onQuery(\n collection: string,\n results: QueryResult[] | ((query: VectorQuery) => QueryResult[]),\n ): this {\n this.queryHandlers.set(collection, results);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n deleteCollection(name: string): this {\n this.collections.delete(name);\n this.queryHandlers.delete(name);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only consume the request body for methods that carry a body,\n // avoiding double-consumption when the route doesn't match.\n let parsed: Record<string, unknown> = {};\n if (req.method !== \"GET\" && req.method !== \"DELETE\" && req.method !== \"HEAD\") {\n const body = await readBody(req);\n try {\n if (body) parsed = JSON.parse(body);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: `Malformed JSON body: ${detail}` }));\n return true;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`)\n // before the route handlers touch fields — `body.ids`, `body.vectors`,\n // `body.points` and friends throw a TypeError on `null` that nothing\n // catches, killing the process rather than answering the request.\n // Arrays and scalars are rejected for the same reason the JSON handlers\n // reject them: every field reads as `undefined` and the handler answers\n // a misleading `200` on a degenerate empty result.\n if (!isJsonObject(parsed)) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return true;\n }\n }\n\n const handled = this.requestHandler(req, res, pathname, parsed);\n\n // Record vector operation metric\n if (handled && this.registry) {\n const { operation, provider } = classifyVectorRequest(req.method ?? \"GET\", pathname);\n this.registry.incrementCounter(\"aimock_vector_requests_total\", { operation, provider });\n }\n\n // Journal the request after the handler completes\n if (handled && this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n // When mounted, an unmatched path is answered by the parent server, not\n // here, so journal it once the response is out and only if it really was\n // a 404 — otherwise a path this wrapper declines but the server rewrites\n // and serves would be journaled twice. Keeps mounted mode in step with\n // the standalone 404 journaling below.\n if (!handled && this.journal) {\n const journal = this.journal;\n res.once(\"finish\", () => {\n if (res.statusCode !== 404) return;\n journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 404, fixture: null },\n });\n });\n }\n\n return handled;\n }\n\n health(): { status: string; collections: number } {\n return {\n status: \"ok\",\n collections: this.collections.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n let parsed: Record<string, unknown> = {};\n try {\n if (body) parsed = JSON.parse(body);\n } catch (parseErr) {\n if (req.method !== \"GET\") {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: `Malformed JSON body: ${detail}` }));\n return;\n }\n }\n\n // Same non-object rejection as the mounted path above — a `null`\n // body otherwise reaches the route handlers and throws an uncaught\n // TypeError that takes the standalone server's process down.\n if (req.method !== \"GET\" && !isJsonObject(parsed)) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n const url = new URL(req.url ?? \"/\", `http://${host}`);\n\n const handled = this.requestHandler(req, res, url.pathname, parsed);\n\n if (handled && this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n if (!handled) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 404, fixture: null },\n });\n }\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n }\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): JournalEntry[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"vector\");\n }\n\n reset(): this {\n this.collections.clear();\n this.queryHandlers.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: VectorState = {\n collections: this.collections,\n queryHandlers: this.queryHandlers,\n };\n return createVectorRequestHandler(state);\n }\n}\n\n// ---- Helpers ----\n\n/**\n * Classify a vector request by operation and provider based on HTTP method and pathname.\n */\nfunction classifyVectorRequest(\n method: string,\n pathname: string,\n): { operation: string; provider: string } {\n // Pinecone paths\n if (pathname === \"/query\" && method === \"POST\") {\n return { operation: \"query\", provider: \"pinecone\" };\n }\n if (pathname === \"/vectors/upsert\" && method === \"POST\") {\n return { operation: \"upsert\", provider: \"pinecone\" };\n }\n if (pathname === \"/vectors/delete\" && method === \"POST\") {\n return { operation: \"delete\", provider: \"pinecone\" };\n }\n if (pathname === \"/describe-index-stats\" && method === \"GET\") {\n return { operation: \"describe\", provider: \"pinecone\" };\n }\n\n // Qdrant paths\n if (/^\\/collections\\/[^/]+\\/points\\/search$/.test(pathname) && method === \"POST\") {\n return { operation: \"query\", provider: \"qdrant\" };\n }\n if (/^\\/collections\\/[^/]+\\/points$/.test(pathname) && method === \"PUT\") {\n return { operation: \"upsert\", provider: \"qdrant\" };\n }\n if (/^\\/collections\\/[^/]+\\/points\\/delete$/.test(pathname) && method === \"POST\") {\n return { operation: \"delete\", provider: \"qdrant\" };\n }\n\n // ChromaDB paths\n if (/^\\/api\\/v1\\/collections\\/[^/]+\\/query$/.test(pathname) && method === \"POST\") {\n return { operation: \"query\", provider: \"chromadb\" };\n }\n if (/^\\/api\\/v1\\/collections\\/[^/]+\\/add$/.test(pathname) && method === \"POST\") {\n return { operation: \"upsert\", provider: \"chromadb\" };\n }\n if (pathname === \"/api/v1/collections\" && method === \"GET\") {\n return { operation: \"list\", provider: \"chromadb\" };\n }\n if (/^\\/api\\/v1\\/collections\\/[^/]+$/.test(pathname) && method === \"DELETE\") {\n return { operation: \"delete\", provider: \"chromadb\" };\n }\n\n return { operation: \"unknown\", provider: \"unknown\" };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vector-mock.cjs","names":["readBody","flattenHeaders","isJsonObject","buildOpenAIError","http","createVectorRequestHandler"],"sources":["../src/vector-mock.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport { buildOpenAIError } from \"./response/errors.js\";\nimport type { Mountable, JournalEntry } from \"./types.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { MetricsRegistry } from \"./metrics.js\";\nimport type {\n VectorMockOptions,\n VectorCollection,\n VectorEntry,\n QueryResult,\n VectorQuery,\n QueryHandler,\n} from \"./vector-types.js\";\nimport { createVectorRequestHandler, type VectorState } from \"./vector-handler.js\";\nimport { flattenHeaders, isJsonObject, readBody } from \"./helpers.js\";\n\nexport class VectorMock implements Mountable {\n private collections: Map<string, VectorCollection> = new Map();\n private queryHandlers: Map<string, QueryHandler> = new Map();\n private server: http.Server | null = null;\n private journal: Journal | null = null;\n private registry: MetricsRegistry | null = null;\n private options: VectorMockOptions;\n private requestHandler: ReturnType<typeof createVectorRequestHandler>;\n\n constructor(options?: VectorMockOptions) {\n this.options = options ?? {};\n this.requestHandler = this.buildHandler();\n }\n\n // ---- Configuration ----\n\n addCollection(name: string, opts: { dimension: number }): this {\n const collection: VectorCollection = {\n name,\n dimension: opts.dimension,\n vectors: new Map(),\n };\n this.collections.set(name, collection);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n upsert(collection: string, vectors: VectorEntry[]): this {\n let col = this.collections.get(collection);\n if (!col) {\n const dim = vectors.length > 0 ? vectors[0].values.length : 0;\n col = { name: collection, dimension: dim, vectors: new Map() };\n this.collections.set(collection, col);\n }\n for (const v of vectors) {\n col.vectors.set(v.id, v);\n }\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n onQuery(\n collection: string,\n results: QueryResult[] | ((query: VectorQuery) => QueryResult[]),\n ): this {\n this.queryHandlers.set(collection, results);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n deleteCollection(name: string): this {\n this.collections.delete(name);\n this.queryHandlers.delete(name);\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Mountable interface ----\n\n async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n pathname: string,\n ): Promise<boolean> {\n // Only consume the request body for methods that carry a body,\n // avoiding double-consumption when the route doesn't match.\n let parsed: Record<string, unknown> = {};\n if (req.method !== \"GET\" && req.method !== \"DELETE\" && req.method !== \"HEAD\") {\n const body = await readBody(req);\n try {\n if (body) parsed = JSON.parse(body);\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: `Malformed JSON body: ${detail}` }));\n return true;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`)\n // before the route handlers touch fields — `body.ids`, `body.vectors`,\n // `body.points` and friends throw a TypeError on `null` that nothing\n // catches, killing the process rather than answering the request.\n // Arrays and scalars are rejected for the same reason the JSON handlers\n // reject them: every field reads as `undefined` and the handler answers\n // a misleading `200` on a degenerate empty result.\n if (!isJsonObject(parsed)) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n buildOpenAIError(\"service-validation\", {\n message: \"Request body must be a JSON object\",\n }),\n ),\n );\n return true;\n }\n }\n\n const handled = this.requestHandler(req, res, pathname, parsed);\n\n // Record vector operation metric\n if (handled && this.registry) {\n const { operation, provider } = classifyVectorRequest(req.method ?? \"GET\", pathname);\n this.registry.incrementCounter(\"aimock_vector_requests_total\", { operation, provider });\n }\n\n // Journal the request after the handler completes\n if (handled && this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n\n // When mounted, an unmatched path is answered by the parent server, not\n // here, so journal it once the response is out and only if it really was\n // a 404 — otherwise a path this wrapper declines but the server rewrites\n // and serves would be journaled twice. Keeps mounted mode in step with\n // the standalone 404 journaling below.\n if (!handled && this.journal) {\n const journal = this.journal;\n res.once(\"finish\", () => {\n if (res.statusCode !== 404) return;\n journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 404, fixture: null },\n });\n });\n }\n\n return handled;\n }\n\n health(): { status: string; collections: number } {\n return {\n status: \"ok\",\n collections: this.collections.size,\n };\n }\n\n setJournal(journal: Journal): void {\n this.journal = journal;\n }\n\n setRegistry(registry: MetricsRegistry): void {\n this.registry = registry;\n }\n\n // ---- Standalone mode ----\n\n async start(): Promise<string> {\n if (this.server) {\n throw new Error(\"Server already started\");\n }\n\n const host = this.options.host ?? \"127.0.0.1\";\n const port = this.options.port ?? 0;\n\n return new Promise((resolve, reject) => {\n const srv = http.createServer((req, res) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n const body = Buffer.concat(chunks).toString();\n let parsed: Record<string, unknown> = {};\n try {\n if (body) parsed = JSON.parse(body);\n } catch (parseErr) {\n if (req.method !== \"GET\") {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: `Malformed JSON body: ${detail}` }));\n return;\n }\n }\n\n // Same non-object rejection as the mounted path above — a `null`\n // body otherwise reaches the route handlers and throws an uncaught\n // TypeError that takes the standalone server's process down.\n if (req.method !== \"GET\" && !isJsonObject(parsed)) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 400, fixture: null },\n });\n }\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify(\n buildOpenAIError(\"service-validation\", {\n message: \"Request body must be a JSON object\",\n }),\n ),\n );\n return;\n }\n\n const url = new URL(req.url ?? \"/\", `http://${host}`);\n\n const handled = this.requestHandler(req, res, url.pathname, parsed);\n\n if (handled && this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: res.statusCode, fixture: null },\n });\n }\n if (!handled) {\n if (this.journal) {\n this.journal.add({\n method: req.method ?? \"GET\",\n path: req.url ?? \"/\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"vector\",\n response: { status: 404, fixture: null },\n });\n }\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Not found\" }));\n }\n });\n });\n\n srv.listen(port, host, () => {\n this.server = srv;\n const addr = srv.address();\n if (typeof addr === \"object\" && addr !== null) {\n resolve(`http://${host}:${addr.port}`);\n } else {\n resolve(`http://${host}:${port}`);\n }\n });\n\n srv.on(\"error\", reject);\n });\n }\n\n async stop(): Promise<void> {\n if (!this.server) {\n throw new Error(\"Server not started\");\n }\n const srv = this.server;\n this.server = null;\n await new Promise<void>((resolve, reject) => {\n srv.close((err) => (err ? reject(err) : resolve()));\n });\n }\n\n // ---- Inspection ----\n\n getRequests(): JournalEntry[] {\n if (!this.journal) return [];\n return this.journal.getAll().filter((e) => e.service === \"vector\");\n }\n\n reset(): this {\n this.collections.clear();\n this.queryHandlers.clear();\n this.requestHandler = this.buildHandler();\n return this;\n }\n\n // ---- Internal ----\n\n private buildHandler() {\n const state: VectorState = {\n collections: this.collections,\n queryHandlers: this.queryHandlers,\n };\n return createVectorRequestHandler(state);\n }\n}\n\n// ---- Helpers ----\n\n/**\n * Classify a vector request by operation and provider based on HTTP method and pathname.\n */\nfunction classifyVectorRequest(\n method: string,\n pathname: string,\n): { operation: string; provider: string } {\n // Pinecone paths\n if (pathname === \"/query\" && method === \"POST\") {\n return { operation: \"query\", provider: \"pinecone\" };\n }\n if (pathname === \"/vectors/upsert\" && method === \"POST\") {\n return { operation: \"upsert\", provider: \"pinecone\" };\n }\n if (pathname === \"/vectors/delete\" && method === \"POST\") {\n return { operation: \"delete\", provider: \"pinecone\" };\n }\n if (pathname === \"/describe-index-stats\" && method === \"GET\") {\n return { operation: \"describe\", provider: \"pinecone\" };\n }\n\n // Qdrant paths\n if (/^\\/collections\\/[^/]+\\/points\\/search$/.test(pathname) && method === \"POST\") {\n return { operation: \"query\", provider: \"qdrant\" };\n }\n if (/^\\/collections\\/[^/]+\\/points$/.test(pathname) && method === \"PUT\") {\n return { operation: \"upsert\", provider: \"qdrant\" };\n }\n if (/^\\/collections\\/[^/]+\\/points\\/delete$/.test(pathname) && method === \"POST\") {\n return { operation: \"delete\", provider: \"qdrant\" };\n }\n\n // ChromaDB paths\n if (/^\\/api\\/v1\\/collections\\/[^/]+\\/query$/.test(pathname) && method === \"POST\") {\n return { operation: \"query\", provider: \"chromadb\" };\n }\n if (/^\\/api\\/v1\\/collections\\/[^/]+\\/add$/.test(pathname) && method === \"POST\") {\n return { operation: \"upsert\", provider: \"chromadb\" };\n }\n if (pathname === \"/api/v1/collections\" && method === \"GET\") {\n return { operation: \"list\", provider: \"chromadb\" };\n }\n if (/^\\/api\\/v1\\/collections\\/[^/]+$/.test(pathname) && method === \"DELETE\") {\n return { operation: \"delete\", provider: \"chromadb\" };\n }\n\n return { operation: \"unknown\", provider: \"unknown\" };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-mock.d.cts","names":[],"sources":["../src/vector-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"vector-mock.d.cts","names":[],"sources":["../src/vector-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAgBa,UAAA,YAAsB;;EAAtB,QAAA,aAAW;EAAA,QAAA,MAAA;UASA,OAAA;UAkBc,QAAA;UAgBzB,OAAA;UAAyB,cAAA;aAAgB,CAAA,OAAA,CAAA,EAlC9B,iBAkC8B;eAiBxC,CAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EAAA;IACL,SAAK,EAAA,MAAA;MAET,IAAA;QAyGiB,CAAA,UAAA,EAAA,MAAA,EAAA,OAAA,EA7IgB,WA6IhB,EAAA,CAAA,EAAA,IAAA;SAIE,CAAA,UAAA,EAAA,MAAA,EAAA,OAAA,EAjIX,WAiIW,EAAA,GAAA,CAAA,CAAA,KAAA,EAjIc,WAiId,EAAA,GAjI8B,WAiI9B,EAAA,CAAA,CAAA,EAAA,IAAA;kBAMP,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;eA0GD,CAAA,GAAA,EAhOP,MAAA,CAAK,eAgOE,EAAA,GAAA,EA/NP,MAAA,CAAK,cA+NE,EAAA,QAAA,EAAA,MAAA,CAAA,EA7NX,OA6NW,CAAA,OAAA,CAAA;QAaC,CAAA,CAAA,EAAA;IAzSkB,MAAA,EAAA,MAAA;IAAS,WAAA,EAAA,MAAA;;sBAwKtB;wBAIE;WAMP;UA0GD;iBAaC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-mock.d.ts","names":[],"sources":["../src/vector-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"vector-mock.d.ts","names":[],"sources":["../src/vector-mock.ts"],"sourcesContent":[],"mappings":";;;;;;;cAgBa,UAAA,YAAsB;;EAAtB,QAAA,aAAW;EAAA,QAAA,MAAA;UASA,OAAA;UAkBc,QAAA;UAgBzB,OAAA;UAAyB,cAAA;aAAgB,CAAA,OAAA,CAAA,EAlC9B,iBAkC8B;eAiBxC,CAAA,IAAA,EAAA,MAAA,EAAA,IAAA,EAAA;IACL,SAAK,EAAA,MAAA;MAET,IAAA;QAyGiB,CAAA,UAAA,EAAA,MAAA,EAAA,OAAA,EA7IgB,WA6IhB,EAAA,CAAA,EAAA,IAAA;SAIE,CAAA,UAAA,EAAA,MAAA,EAAA,OAAA,EAjIX,WAiIW,EAAA,GAAA,CAAA,CAAA,KAAA,EAjIc,WAiId,EAAA,GAjI8B,WAiI9B,EAAA,CAAA,CAAA,EAAA,IAAA;kBAMP,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;eA0GD,CAAA,GAAA,EAhOP,MAAA,CAAK,eAgOE,EAAA,GAAA,EA/NP,MAAA,CAAK,cA+NE,EAAA,QAAA,EAAA,MAAA,CAAA,EA7NX,OA6NW,CAAA,OAAA,CAAA;QAaC,CAAA,CAAA,EAAA;IAzSkB,MAAA,EAAA,MAAA;IAAS,WAAA,EAAA,MAAA;;sBAwKtB;wBAIE;WAMP;UA0GD;iBAaC"}
|
package/dist/vector-mock.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildOpenAIError } from "./response/errors.js";
|
|
1
2
|
import { flattenHeaders, isJsonObject, readBody } from "./helpers.js";
|
|
2
3
|
import { createVectorRequestHandler } from "./vector-handler.js";
|
|
3
4
|
import * as http$1 from "node:http";
|
|
@@ -86,10 +87,7 @@ var VectorMock = class {
|
|
|
86
87
|
}
|
|
87
88
|
});
|
|
88
89
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
89
|
-
res.end(JSON.stringify({
|
|
90
|
-
message: "Request body must be a JSON object",
|
|
91
|
-
type: "invalid_request_error"
|
|
92
|
-
} }));
|
|
90
|
+
res.end(JSON.stringify(buildOpenAIError("service-validation", { message: "Request body must be a JSON object" })));
|
|
93
91
|
return true;
|
|
94
92
|
}
|
|
95
93
|
}
|
|
@@ -188,10 +186,7 @@ var VectorMock = class {
|
|
|
188
186
|
}
|
|
189
187
|
});
|
|
190
188
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
191
|
-
res.end(JSON.stringify({
|
|
192
|
-
message: "Request body must be a JSON object",
|
|
193
|
-
type: "invalid_request_error"
|
|
194
|
-
} }));
|
|
189
|
+
res.end(JSON.stringify(buildOpenAIError("service-validation", { message: "Request body must be a JSON object" })));
|
|
195
190
|
return;
|
|
196
191
|
}
|
|
197
192
|
const url = new URL(req.url ?? "/", `http://${host}`);
|