@copilotkit/aimock 1.45.1 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -1
- package/README.md +11 -2
- package/dist/agui-mock.cjs +25 -25
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/aws-event-stream.cjs +20 -23
- package/dist/aws-event-stream.cjs.map +1 -1
- package/dist/aws-event-stream.d.cts +0 -1
- package/dist/aws-event-stream.d.ts +0 -1
- package/dist/aws-event-stream.js +20 -24
- package/dist/aws-event-stream.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +139 -161
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +137 -159
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +175 -216
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +172 -213
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +107 -180
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +106 -179
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +140 -44
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +141 -46
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +113 -156
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +113 -156
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +295 -218
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +295 -219
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +411 -75
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +51 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +51 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +402 -74
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-handler.cjs +7 -3
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +7 -3
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +235 -202
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +234 -202
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +62 -3
- package/dist/misbehavior.cjs.map +1 -1
- package/dist/misbehavior.d.cts.map +1 -1
- package/dist/misbehavior.d.ts.map +1 -1
- package/dist/misbehavior.js +63 -4
- package/dist/misbehavior.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ndjson-writer.cjs +21 -23
- package/dist/ndjson-writer.cjs.map +1 -1
- package/dist/ndjson-writer.d.cts +0 -1
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +0 -1
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ndjson-writer.js +21 -24
- package/dist/ndjson-writer.js.map +1 -1
- package/dist/ollama.cjs +276 -210
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +273 -207
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +51 -74
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +37 -60
- package/dist/openrouter-video.js.map +1 -1
- package/dist/raw-validation.cjs +177 -0
- package/dist/raw-validation.cjs.map +1 -0
- package/dist/raw-validation.js +176 -0
- package/dist/raw-validation.js.map +1 -0
- package/dist/recorder.cjs +176 -95
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/deliver.cjs +144 -0
- package/dist/response/deliver.cjs.map +1 -0
- package/dist/response/deliver.js +143 -0
- package/dist/response/deliver.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/frame-loop.cjs +102 -0
- package/dist/response/frame-loop.cjs.map +1 -0
- package/dist/response/frame-loop.js +100 -0
- package/dist/response/frame-loop.js.map +1 -0
- package/dist/response/lifecycle.cjs +223 -0
- package/dist/response/lifecycle.cjs.map +1 -0
- package/dist/response/lifecycle.js +211 -0
- package/dist/response/lifecycle.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/proxy.cjs +71 -0
- package/dist/response/proxy.cjs.map +1 -0
- package/dist/response/proxy.js +70 -0
- package/dist/response/proxy.js.map +1 -0
- package/dist/response/raw.cjs +228 -0
- package/dist/response/raw.cjs.map +1 -0
- package/dist/response/raw.js +227 -0
- package/dist/response/raw.js.map +1 -0
- package/dist/response/timing.cjs +157 -0
- package/dist/response/timing.cjs.map +1 -0
- package/dist/response/timing.js +150 -0
- package/dist/response/timing.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +304 -196
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +303 -195
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +24 -4
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +7 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -5
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +293 -248
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +294 -249
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +56 -30
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +0 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +0 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +55 -31
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +31 -43
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +31 -43
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +231 -9
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +231 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-framing.cjs +12 -6
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +2 -0
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -0
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +12 -6
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +131 -96
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +130 -95
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +108 -124
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +105 -121
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +80 -59
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +79 -58
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +57 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-registry.js","names":[],"sources":["../src/route-registry.ts"],"sourcesContent":["/**\n * Single source of truth for every first-class HTTP route aimock serves.\n *\n * Both the HTTP dispatcher (`server.ts`) and the machine-readable catalog\n * (`openapi.ts` → `GET /__aimock/openapi.json` + `GET /__aimock/routes`) import\n * from this module. Drift is prevented three ways, not one:\n *\n * 1. Shared bindings — the dispatcher matches against the exact `*_PATH`\n * constants and `*_RE` patterns defined here (never inline literals), and\n * `metrics.ts` path-label normalization imports the same patterns, so the\n * three consumers cannot disagree on what a route looks like.\n * 2. Bidirectional tests (`openapi-catalog.test.ts`) — every catalog entry is\n * probed against the live router (catalog → router) AND every dispatch\n * literal/pattern imported from this module must have a catalog entry\n * whose example path it matches (router → catalog).\n * 3. Runtime drift guard — the dispatcher's terminal 404 consults\n * `matchRouteDefinition` and warns when the registry claims a route the\n * dispatcher just missed.\n *\n * `path` is the OpenAPI path template (`{param}` for dynamic segments).\n * `examplePath` is a concrete path the drift test probes against the live\n * server (a templated path can never be fetched verbatim).\n *\n * Plugin mounts (`Mountable`: A2A/MCP/AG-UI/vector) are dynamic per-server and\n * intentionally excluded — this registry covers the built-in first-class\n * surfaces only. Likewise excluded from `ROUTE_DEFINITIONS` (their bindings\n * above are still shared so dispatch cannot typo them): the WebSocket upgrades\n * (`REALTIME_PATH`, `GEMINI_LIVE_PATH`, `LIVE_PATH` — same URL as an HTTP\n * route or no HTTP method at all, so they cannot appear in an HTTP catalog)\n * and the header-gated fal.ai proxy branch (`FAL_ROUTE_RE` +\n * `x-fal-target-host`, a dynamic upstream mirror rather than a fixed route).\n */\n\nexport const COMPLETIONS_PATH = \"/v1/chat/completions\";\nexport const RESPONSES_PATH = \"/v1/responses\";\nexport const REALTIME_PATH = \"/v1/realtime\";\nexport const LIVE_PATH = \"/v1/live/sessions\";\nexport const GEMINI_LIVE_PATH =\n \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\nexport const MESSAGES_PATH = \"/v1/messages\";\nexport const EMBEDDINGS_PATH = \"/v1/embeddings\";\nexport const COHERE_CHAT_PATH = \"/v2/chat\";\nexport const COHERE_EMBED_PATH = \"/v2/embed\";\nexport const SEARCH_PATH = \"/search\";\nexport const RERANK_PATH = \"/v2/rerank\";\nexport const MODERATIONS_PATH = \"/v1/moderations\";\nexport const IMAGES_PATH = \"/v1/images/generations\";\nexport const IMAGES_EDIT_PATH = \"/v1/images/edits\";\nexport const IMAGES_VARIATIONS_PATH = \"/v1/images/variations\";\nexport const SPEECH_PATH = \"/v1/audio/speech\";\nexport const TRANSCRIPTIONS_PATH = \"/v1/audio/transcriptions\";\nexport const TRANSLATIONS_PATH = \"/v1/audio/translations\";\nexport const VIDEOS_PATH = \"/v1/videos\";\n// Grok submit is an exact literal; Grok status reuses the OpenAI\n// `/v1/videos/{id}` shape (dispatch guards `id !== \"generations\"` and the\n// job-map lookup disambiguates Sora). Shared with metrics.ts normalization.\nexport const GROK_VIDEO_SUBMIT_PATH = \"/v1/videos/generations\";\nexport const GROK_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\nexport const OPENAI_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\nexport const GEMINI_PREDICT_RE = /^\\/v1beta\\/models\\/([^:]+):predict$/;\n// Veo submit (`:predictLongRunning`, anchored so it never collides with the\n// bare Gemini `:predict` route) + Veo operation polling. The operation RE\n// allows multi-segment names (`operations/.+`); the catalog template below\n// uses the single-segment canonical form. Shared with metrics.ts.\nexport const VEO_PREDICT_LRO_RE = /^\\/v1beta\\/models\\/([^:]+):predictLongRunning$/;\nexport const VEO_OPERATION_RE = /^\\/v1beta\\/(operations\\/.+)$/;\n// BytePlus Ark (Seedance) task lifecycle. The `/api/v3` prefix is ENUMERATED\n// rather than wildcarded: a tolerant prefix would additionally claim\n// `/fal/contents/generations/tasks`, which dispatches pre-rewrite ~1,100\n// lines ahead of every fal branch. Shared with metrics.ts normalization.\nexport const BYTEPLUS_VIDEO_SUBMIT_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks$/;\nexport const BYTEPLUS_VIDEO_STATUS_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks\\/([^/]+)$/;\nexport const ELEVENLABS_SOUND_GENERATION_PATH = \"/v1/sound-generation\";\nexport const ELEVENLABS_TTS_RE = /^\\/v1\\/text-to-speech\\/([^/]+)$/;\nexport const ELEVENLABS_MUSIC_RE = /^\\/v1\\/music(?:\\/(.+))?$/;\nexport const ELEVENLABS_VOICE_DESIGN_PATH = \"/v1/text-to-voice/design\";\nexport const ELEVENLABS_VOICE_CREATE_PATH = \"/v1/text-to-voice\";\nexport const ELEVENLABS_VOICE_RE = /^\\/v1\\/voices\\/([^/]+)$/;\nexport const FAL_QUEUE_SUBMIT_RE = /^\\/fal\\/queue\\/submit\\/(.+)$/;\nexport const FAL_QUEUE_REQUESTS_RE = /^\\/fal\\/queue\\/requests\\/(.+)$/;\nexport const FAL_RUN_RE = /^\\/fal\\/run\\/(.+)$/;\n// The fal.ai route shape, shared with server.ts dispatch and metrics.ts path\n// labels: a bare `/fal` is routed (header-gated upstream mirror), so one\n// binding keeps the route rule and the label rule from drifting.\nexport const FAL_ROUTE_RE = /^\\/fal(?:\\/.*)?$/;\nexport const GEMINI_INTERACTIONS_PATH = \"/v1beta/interactions\";\nexport const GEMINI_PATH_RE = /^\\/v1beta\\/models\\/([^:]+):(generateContent|streamGenerateContent)$/;\nexport const GEMINI_EMBED_RE = /^\\/v1beta\\/models\\/([^:]+):embedContent$/;\nexport const AZURE_DEPLOYMENT_RE =\n /^\\/openai\\/deployments\\/([^/]+)\\/(chat\\/completions|embeddings)$/;\nexport const BEDROCK_INVOKE_RE = /^\\/model\\/([^/]+)\\/invoke$/;\nexport const BEDROCK_STREAM_RE = /^\\/model\\/([^/]+)\\/invoke-with-response-stream$/;\nexport const BEDROCK_CONVERSE_RE = /^\\/model\\/([^/]+)\\/converse$/;\nexport const BEDROCK_CONVERSE_STREAM_RE = /^\\/model\\/([^/]+)\\/converse-stream$/;\nexport const VERTEX_AI_RE =\n /^\\/v1\\/projects\\/[^/]+\\/locations\\/[^/]+\\/publishers\\/google\\/models\\/([^/:]+):(generateContent|streamGenerateContent)$/;\n\nexport const OLLAMA_CHAT_PATH = \"/api/chat\";\nexport const OLLAMA_GENERATE_PATH = \"/api/generate\";\nexport const OLLAMA_EMBEDDINGS_PATH = \"/api/embeddings\";\nexport const OLLAMA_EMBED_PATH = \"/api/embed\";\nexport const OLLAMA_TAGS_PATH = \"/api/tags\";\n\nexport const OPENROUTER_VIDEOS_PATH = \"/api/v1/videos\";\nexport const OPENROUTER_VIDEO_MODELS_PATH = \"/api/v1/videos/models\";\nexport const OPENROUTER_MODELS_PATH = \"/api/v1/models\";\nexport const OPENROUTER_KEY_PATH = \"/api/v1/key\";\nexport const OPENROUTER_CREDITS_PATH = \"/api/v1/credits\";\n// Dispatched in server.ts (content RE → models exact → status RE → submit\n// exact) and reused by metrics.ts path-label normalization — one binding so\n// dispatch, catalog, and metric labels cannot disagree.\nexport const OPENROUTER_VIDEO_CONTENT_RE = /^\\/api\\/v1\\/videos\\/([^/]+)\\/content$/;\nexport const OPENROUTER_VIDEO_STATUS_RE = /^\\/api\\/v1\\/videos\\/([^/]+)$/;\n\nexport const HEALTH_PATH = \"/health\";\nexport const READY_PATH = \"/ready\";\nexport const METRICS_PATH = \"/metrics\";\nexport const MODELS_PATH = \"/v1/models\";\nexport const REQUESTS_PATH = \"/v1/_requests\";\n\nexport const FILES_PATH = \"/v1/files\";\n// Shared with metrics.ts path-label normalization (file ids are random\n// `file-…` values; labels collapse to `{id}`).\nexport const FILES_CONTENT_RE = /^\\/v1\\/files\\/([^/]+)\\/content$/;\nexport const FILES_ID_RE = /^\\/v1\\/files\\/([^/]+)$/;\n\n// OpenAI Batches API. Shared with server.ts dispatch and metrics.ts normalization.\nexport const BATCHES_PATH = \"/v1/batches\";\nexport const BATCHES_CANCEL_RE = /^\\/v1\\/batches\\/([^/]+)\\/cancel$/;\nexport const BATCHES_ID_RE = /^\\/v1\\/batches\\/([^/]+)$/;\nexport const FINE_TUNING_JOBS_PATH = \"/v1/fine_tuning/jobs\";\nexport const FINE_TUNING_ID_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)$/;\nexport const FINE_TUNING_CANCEL_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)\\/cancel$/;\nexport const FINE_TUNING_EVENTS_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)\\/events$/;\n\n// OpenAI Vector Stores API. Shared with server.ts dispatch and metrics.ts\n// path-label normalization (store/file/batch ids are caller-minted, so every\n// id-bearing path collapses to a placeholder). Dispatch order in server.ts is\n// most-specific RE first (search → batch-files → batch-cancel → batch → file →\n// id), then the two sub-collection REs below, then the collection exact path.\nexport const VECTOR_STORES_PATH = \"/v1/vector_stores\";\nexport const VECTOR_STORES_SEARCH_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/search$/;\nexport const VECTOR_STORES_BATCH_FILES_RE =\n /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)\\/files$/;\nexport const VECTOR_STORES_BATCH_CANCEL_RE =\n /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)\\/cancel$/;\nexport const VECTOR_STORES_BATCH_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)$/;\nexport const VECTOR_STORES_FILE_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/files\\/([^/]+)$/;\nexport const VECTOR_STORES_ID_RE = /^\\/v1\\/vector_stores\\/([^/]+)$/;\nexport const VECTOR_STORES_FILES_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/files$/;\nexport const VECTOR_STORES_FILE_BATCHES_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches$/;\n\nexport const CONTROL_PREFIX = \"/__aimock\";\n\nexport interface RouteDefinition {\n method: string;\n /** OpenAPI path template, e.g. `/v1/text-to-speech/{voice_id}`. */\n path: string;\n /** Concrete path the live-router drift test probes. Defaults to `path`. */\n examplePath?: string;\n service: string;\n description: string;\n}\n\n/**\n * Every built-in route, in the same order the dispatcher checks them\n * (mounts → ollama → openrouter-video → byteplus → openrouter discovery →\n * azure/compat → ops → models/requests → provider handlers → fal → chat).\n * Regex branches are represented by their canonical OpenAPI template.\n */\nexport const ROUTE_DEFINITIONS: RouteDefinition[] = [\n // Ollama (pre-rewrite band)\n { method: \"POST\", path: OLLAMA_CHAT_PATH, service: \"ollama\", description: \"Ollama chat\" },\n {\n method: \"POST\",\n path: OLLAMA_GENERATE_PATH,\n service: \"ollama\",\n description: \"Ollama generate\",\n },\n {\n method: \"POST\",\n path: OLLAMA_EMBEDDINGS_PATH,\n service: \"ollama\",\n description: \"Ollama embeddings (legacy path)\",\n },\n {\n method: \"POST\",\n path: OLLAMA_EMBED_PATH,\n service: \"ollama\",\n description: \"Ollama embed (current path)\",\n },\n { method: \"GET\", path: OLLAMA_TAGS_PATH, service: \"ollama\", description: \"Ollama tags\" },\n // OpenRouter async video lifecycle\n {\n method: \"GET\",\n path: \"/api/v1/videos/{jobId}/content\",\n examplePath: \"/api/v1/videos/test-job/content\",\n service: \"video\",\n description: \"OpenRouter video content download\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_VIDEO_MODELS_PATH,\n service: \"video\",\n description: \"OpenRouter video model listing\",\n },\n {\n method: \"GET\",\n path: \"/api/v1/videos/{jobId}\",\n examplePath: \"/api/v1/videos/test-job\",\n service: \"video\",\n description: \"OpenRouter video job status\",\n },\n {\n method: \"POST\",\n path: OPENROUTER_VIDEOS_PATH,\n service: \"video\",\n description: \"OpenRouter video job submit\",\n },\n // BytePlus Ark (Seedance) — canonical + /api/v3-prefixed aliases\n {\n method: \"GET\",\n path: \"/contents/generations/tasks/{id}\",\n examplePath: \"/contents/generations/tasks/test-id\",\n service: \"video\",\n description: \"BytePlus video task status\",\n },\n {\n method: \"GET\",\n path: \"/api/v3/contents/generations/tasks/{id}\",\n examplePath: \"/api/v3/contents/generations/tasks/test-id\",\n service: \"video\",\n description: \"BytePlus video task status (api/v3 prefix)\",\n },\n {\n method: \"POST\",\n path: \"/contents/generations/tasks\",\n service: \"video\",\n description: \"BytePlus video task submit\",\n },\n {\n method: \"POST\",\n path: \"/api/v3/contents/generations/tasks\",\n service: \"video\",\n description: \"BytePlus video task submit (api/v3 prefix)\",\n },\n // OpenRouter discovery\n {\n method: \"GET\",\n path: OPENROUTER_MODELS_PATH,\n service: \"openrouter\",\n description: \"OpenRouter models\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_KEY_PATH,\n service: \"openrouter\",\n description: \"OpenRouter key info\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_CREDITS_PATH,\n service: \"openrouter\",\n description: \"OpenRouter credits\",\n },\n // Azure OpenAI deployments (rewritten to /v1/* downstream)\n {\n method: \"POST\",\n path: \"/openai/deployments/{deploymentId}/chat/completions\",\n examplePath: \"/openai/deployments/test-deployment/chat/completions\",\n service: \"azure\",\n description: \"Azure OpenAI chat completions\",\n },\n {\n method: \"POST\",\n path: \"/openai/deployments/{deploymentId}/embeddings\",\n examplePath: \"/openai/deployments/test-deployment/embeddings\",\n service: \"azure\",\n description: \"Azure OpenAI embeddings\",\n },\n // Ops\n { method: \"GET\", path: HEALTH_PATH, service: \"ops\", description: \"Health probe (public)\" },\n { method: \"GET\", path: READY_PATH, service: \"ops\", description: \"Readiness probe (public)\" },\n { method: \"GET\", path: METRICS_PATH, service: \"ops\", description: \"Prometheus metrics (public)\" },\n // Models + legacy journal\n { method: \"GET\", path: MODELS_PATH, service: \"openai\", description: \"Models listing\" },\n { method: \"GET\", path: REQUESTS_PATH, service: \"control\", description: \"Legacy journal listing\" },\n {\n method: \"DELETE\",\n path: REQUESTS_PATH,\n service: \"control\",\n description: \"Legacy journal clear\",\n },\n // OpenAI Responses + Anthropic + Cohere + core OpenAI\n { method: \"POST\", path: RESPONSES_PATH, service: \"openai\", description: \"OpenAI Responses API\" },\n { method: \"POST\", path: MESSAGES_PATH, service: \"anthropic\", description: \"Anthropic messages\" },\n { method: \"POST\", path: COHERE_CHAT_PATH, service: \"cohere\", description: \"Cohere chat\" },\n { method: \"POST\", path: COHERE_EMBED_PATH, service: \"cohere\", description: \"Cohere embed\" },\n { method: \"POST\", path: EMBEDDINGS_PATH, service: \"openai\", description: \"OpenAI embeddings\" },\n {\n method: \"POST\",\n path: IMAGES_PATH,\n service: \"images\",\n description: \"Image generation\",\n },\n {\n method: \"POST\",\n path: IMAGES_EDIT_PATH,\n service: \"images\",\n description: \"Image edits (multipart)\",\n },\n {\n method: \"POST\",\n path: \"/v1/images/variations\",\n service: \"images\",\n description: \"Image variations (removed upstream 2026-05-12 — replays the real 404)\",\n },\n { method: \"POST\", path: SPEECH_PATH, service: \"speech\", description: \"Text-to-speech\" },\n {\n method: \"POST\",\n path: TRANSCRIPTIONS_PATH,\n service: \"transcription\",\n description: \"Audio transcription (multipart)\",\n },\n {\n method: \"POST\",\n path: TRANSLATIONS_PATH,\n service: \"transcription\",\n description: \"Audio translation (multipart)\",\n },\n // Video: Sora submit, Grok submit, shared status\n {\n method: \"POST\",\n path: VIDEOS_PATH,\n service: \"video\",\n description: \"Video generation submit (Sora)\",\n },\n {\n method: \"POST\",\n path: \"/v1/videos/generations\",\n service: \"video\",\n description: \"Grok video generation submit\",\n },\n {\n method: \"GET\",\n path: \"/v1/videos/{id}\",\n examplePath: \"/v1/videos/test-id\",\n service: \"video\",\n description: \"Video job status (Sora/Grok)\",\n },\n // Veo\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:predictLongRunning\",\n examplePath: \"/v1beta/models/veo-3:predictLongRunning\",\n service: \"video\",\n description: \"Veo video submit\",\n },\n {\n method: \"GET\",\n path: \"/v1beta/operations/{name}\",\n examplePath: \"/v1beta/operations/test-op\",\n service: \"video\",\n description: \"Veo operation status\",\n },\n // Gemini Imagen + interactions + embeddings + generate\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:predict\",\n examplePath: \"/v1beta/models/imagen-3:predict\",\n service: \"gemini\",\n description: \"Gemini Imagen predict\",\n },\n {\n method: \"POST\",\n path: GEMINI_INTERACTIONS_PATH,\n service: \"gemini\",\n description: \"Gemini interactions\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:embedContent\",\n examplePath: \"/v1beta/models/text-embedding-004:embedContent\",\n service: \"gemini\",\n description: \"Gemini embedContent\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:generateContent\",\n examplePath: \"/v1beta/models/gemini-2.0-flash:generateContent\",\n service: \"gemini\",\n description: \"Gemini generateContent\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:streamGenerateContent\",\n examplePath: \"/v1beta/models/gemini-2.0-flash:streamGenerateContent\",\n service: \"gemini\",\n description: \"Gemini streaming\",\n },\n // Vertex AI (same shapes, full resource path)\n {\n method: \"POST\",\n path: \"/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent\",\n examplePath:\n \"/v1/projects/test-proj/locations/us-central1/publishers/google/models/gemini-2.0-flash:generateContent\",\n service: \"vertex\",\n description: \"Vertex AI generateContent\",\n },\n {\n method: \"POST\",\n path: \"/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:streamGenerateContent\",\n examplePath:\n \"/v1/projects/test-proj/locations/us-central1/publishers/google/models/gemini-2.0-flash:streamGenerateContent\",\n service: \"vertex\",\n description: \"Vertex AI streaming\",\n },\n // Bedrock\n {\n method: \"POST\",\n path: \"/model/{modelId}/invoke\",\n examplePath: \"/model/anthropic.claude-3/invoke\",\n service: \"bedrock\",\n description: \"Bedrock invoke\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/invoke-with-response-stream\",\n examplePath: \"/model/anthropic.claude-3/invoke-with-response-stream\",\n service: \"bedrock\",\n description: \"Bedrock streaming invoke\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/converse\",\n examplePath: \"/model/anthropic.claude-3/converse\",\n service: \"bedrock\",\n description: \"Bedrock converse\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/converse-stream\",\n examplePath: \"/model/anthropic.claude-3/converse-stream\",\n service: \"bedrock\",\n description: \"Bedrock converse streaming\",\n },\n // Search / rerank / moderation\n { method: \"POST\", path: SEARCH_PATH, service: \"search\", description: \"Tavily search\" },\n { method: \"POST\", path: RERANK_PATH, service: \"rerank\", description: \"Cohere rerank\" },\n { method: \"POST\", path: MODERATIONS_PATH, service: \"moderation\", description: \"Moderation\" },\n // ElevenLabs\n {\n method: \"POST\",\n path: ELEVENLABS_SOUND_GENERATION_PATH,\n service: \"elevenlabs\",\n description: \"ElevenLabs sound generation\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-voice/design\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice design preview\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs save designed voice\",\n },\n {\n method: \"GET\",\n path: \"/v1/voices/{voice_id}\",\n examplePath: \"/v1/voices/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice slot get\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/voices/{voice_id}\",\n examplePath: \"/v1/voices/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice slot delete\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-speech/{voice_id}\",\n examplePath: \"/v1/text-to-speech/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs text-to-speech\",\n },\n {\n method: \"POST\",\n path: \"/v1/music\",\n service: \"elevenlabs\",\n description: \"ElevenLabs music\",\n },\n {\n method: \"POST\",\n path: \"/v1/music/{subtype}\",\n examplePath: \"/v1/music/generation\",\n service: \"elevenlabs\",\n description: \"ElevenLabs music sub-operation\",\n },\n // fal.ai\n {\n method: \"POST\",\n path: \"/fal/queue/submit/{model}\",\n examplePath: \"/fal/queue/submit/fal-ai-test\",\n service: \"fal\",\n description: \"fal.ai queue submit\",\n },\n {\n method: \"GET\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result\",\n },\n {\n method: \"POST\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result (POST)\",\n },\n {\n method: \"PUT\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result (PUT)\",\n },\n {\n method: \"POST\",\n path: \"/fal/run/{model}\",\n examplePath: \"/fal/run/fal-ai-test\",\n service: \"fal\",\n description: \"fal.ai synchronous run\",\n },\n // OpenAI Batches API (cancel before id: the id RE would swallow /cancel)\n {\n method: \"POST\",\n path: \"/v1/batches/{batch_id}/cancel\",\n examplePath: \"/v1/batches/batch-test123/cancel\",\n service: \"batches\",\n description: \"Batch cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/batches/{batch_id}\",\n examplePath: \"/v1/batches/batch-test123\",\n service: \"batches\",\n description: \"Batch retrieve\",\n },\n { method: \"GET\", path: BATCHES_PATH, service: \"batches\", description: \"Batch list\" },\n {\n method: \"POST\",\n path: BATCHES_PATH,\n service: \"batches\",\n description: \"Batch create\",\n },\n // OpenAI Files API\n {\n method: \"GET\",\n path: \"/v1/files/{file_id}/content\",\n examplePath: \"/v1/files/file-test123/content\",\n service: \"files\",\n description: \"File content download\",\n },\n {\n method: \"GET\",\n path: \"/v1/files/{file_id}\",\n examplePath: \"/v1/files/file-test123\",\n service: \"files\",\n description: \"File retrieve\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/files/{file_id}\",\n examplePath: \"/v1/files/file-test123\",\n service: \"files\",\n description: \"File delete\",\n },\n { method: \"GET\", path: \"/v1/files\", service: \"files\", description: \"File list\" },\n {\n method: \"POST\",\n path: \"/v1/files\",\n service: \"files\",\n description: \"File upload (JSON or multipart)\",\n },\n // OpenAI fine-tuning jobs\n {\n method: \"POST\",\n path: \"/v1/fine_tuning/jobs/{job_id}/cancel\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123/cancel\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs/{job_id}/events\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123/events\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job events\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs/{job_id}\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job retrieve\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job list\",\n },\n {\n method: \"POST\",\n path: \"/v1/fine_tuning/jobs\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job create\",\n },\n // OpenAI Vector Stores (cancel before id is not needed: the id RE is\n // anchored to the exact 3-segment path so it cannot swallow sub-resources;\n // most-specific REs still read first in the dispatcher).\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/search\",\n examplePath: \"/v1/vector_stores/vs-test123/search\",\n service: \"vector-stores\",\n description: \"Vector store search\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123/files\",\n service: \"vector-stores\",\n description: \"Vector file batch files list\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123/cancel\",\n service: \"vector-stores\",\n description: \"Vector file batch cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123\",\n service: \"vector-stores\",\n description: \"Vector file batch retrieve\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches\",\n service: \"vector-stores\",\n description: \"Vector file batch create\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/files/{file_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/files/file-test123\",\n service: \"vector-stores\",\n description: \"Vector store file retrieve\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/vector_stores/{vector_store_id}/files/{file_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/files/file-test123\",\n service: \"vector-stores\",\n description: \"Vector store file delete\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/files\",\n service: \"vector-stores\",\n description: \"Vector store file attach\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/files\",\n service: \"vector-stores\",\n description: \"Vector store files list\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store retrieve\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store modify\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store delete\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores\",\n service: \"vector-stores\",\n description: \"Vector store list\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores\",\n service: \"vector-stores\",\n description: \"Vector store create\",\n },\n // Chat completions (terminal dispatcher branch)\n {\n method: \"POST\",\n path: COMPLETIONS_PATH,\n service: \"openai\",\n description: \"OpenAI chat completions\",\n },\n // Control API\n { method: \"GET\", path: \"/__aimock/health\", service: \"control\", description: \"Control health\" },\n { method: \"GET\", path: \"/__aimock/journal\", service: \"control\", description: \"Request journal\" },\n {\n method: \"GET\",\n path: \"/__aimock/fixtures\",\n service: \"control\",\n description: \"Fixture count\",\n },\n { method: \"POST\", path: \"/__aimock/fixtures\", service: \"control\", description: \"Add fixtures\" },\n {\n method: \"DELETE\",\n path: \"/__aimock/fixtures\",\n service: \"control\",\n description: \"Clear fixtures\",\n },\n { method: \"GET\", path: \"/__aimock/chaos\", service: \"control\", description: \"Read chaos config\" },\n { method: \"POST\", path: \"/__aimock/chaos\", service: \"control\", description: \"Set chaos config\" },\n {\n method: \"DELETE\",\n path: \"/__aimock/chaos\",\n service: \"control\",\n description: \"Clear chaos override\",\n },\n { method: \"POST\", path: \"/__aimock/reset\", service: \"control\", description: \"Full reset\" },\n {\n method: \"POST\",\n path: \"/__aimock/reset/journal\",\n service: \"control\",\n description: \"Journal-only reset\",\n },\n {\n method: \"POST\",\n path: \"/__aimock/reset/fixtures\",\n service: \"control\",\n description: \"Full reset (deprecated alias)\",\n },\n { method: \"POST\", path: \"/__aimock/error\", service: \"control\", description: \"One-shot error\" },\n {\n method: \"GET\",\n path: \"/__aimock/openapi.json\",\n service: \"control\",\n description: \"OpenAPI catalog\",\n },\n { method: \"GET\", path: \"/__aimock/routes\", service: \"control\", description: \"Flat route list\" },\n];\n\n/**\n * Compile an OpenAPI path template (`{param}` segments) to the equivalent\n * single-segment matcher. Every `{param}` becomes `[^/]+`; everything else\n * matches literally (including `:action` suffixes like `:predict`).\n *\n * This is intentionally the CANONICAL matcher, not a copy of each\n * dispatcher's regex: a couple of dispatch regexes are deliberately wider\n * (e.g. `VEO_OPERATION_RE` allows multi-segment operation names), so the\n * registry matcher may miss exotic paths the dispatcher serves — but it must\n * never CLAIM a path the dispatcher does not serve, which is the direction\n * the runtime drift guard relies on.\n */\nexport function templateToRegExp(template: string): RegExp {\n const pattern = template\n .split(/(\\{[^}]+\\})/g)\n .map((part) =>\n /^\\{[^}]+\\}$/.test(part) ? \"[^/]+\" : part.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n )\n .join(\"\");\n return new RegExp(`^${pattern}$`);\n}\n\n/**\n * Find the registry entry serving `METHOD pathname`, in dispatcher order\n * (first match wins — `ROUTE_DEFINITIONS` is kept in dispatch order for\n * exactly this reason). Returns `undefined` when no catalog entry covers the\n * request. Method comparison is case-insensitive, like the dispatcher.\n */\nexport function matchRouteDefinition(\n method: string,\n pathname: string,\n): RouteDefinition | undefined {\n const want = method.toUpperCase();\n for (const def of ROUTE_DEFINITIONS) {\n if (def.method.toUpperCase() !== want) continue;\n if (def.path === pathname) return def;\n if (def.path.includes(\"{\") && templateToRegExp(def.path).test(pathname)) return def;\n }\n return undefined;\n}\n"],"mappings":"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S;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAc,SAAS;EAAW,aAAa;EAAc;CACpF;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAa,SAAS;EAAS,aAAa;EAAa;CAChF;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAID;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EAAE,QAAQ;EAAO,MAAM;EAAoB,SAAS;EAAW,aAAa;EAAkB;CAC9F;EAAE,QAAQ;EAAO,MAAM;EAAqB,SAAS;EAAW,aAAa;EAAmB;CAChG;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAsB,SAAS;EAAW,aAAa;EAAgB;CAC/F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAqB;CAChG;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAoB;CAChG;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAc;CAC1F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAkB;CAC9F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAoB,SAAS;EAAW,aAAa;EAAmB;CAChG;;;;;;;;;;;;;AAcD,SAAgB,iBAAiB,UAA0B;CACzD,MAAM,UAAU,SACb,MAAM,eAAe,CACrB,KAAK,SACJ,cAAc,KAAK,KAAK,GAAG,UAAU,KAAK,QAAQ,uBAAuB,OAAO,CACjF,CACA,KAAK,GAAG;AACX,QAAO,IAAI,OAAO,IAAI,QAAQ,GAAG;;;;;;;;AASnC,SAAgB,qBACd,QACA,UAC6B;CAC7B,MAAM,OAAO,OAAO,aAAa;AACjC,MAAK,MAAM,OAAO,mBAAmB;AACnC,MAAI,IAAI,OAAO,aAAa,KAAK,KAAM;AACvC,MAAI,IAAI,SAAS,SAAU,QAAO;AAClC,MAAI,IAAI,KAAK,SAAS,IAAI,IAAI,iBAAiB,IAAI,KAAK,CAAC,KAAK,SAAS,CAAE,QAAO"}
|
|
1
|
+
{"version":3,"file":"route-registry.js","names":[],"sources":["../src/route-registry.ts"],"sourcesContent":["/**\n * Single source of truth for every first-class HTTP route aimock serves.\n *\n * Both the HTTP dispatcher (`server.ts`) and the machine-readable catalog\n * (`openapi.ts` → `GET /__aimock/openapi.json` + `GET /__aimock/routes`) import\n * from this module. Drift is prevented three ways, not one:\n *\n * 1. Shared bindings — the dispatcher matches against the exact `*_PATH`\n * constants and `*_RE` patterns defined here (never inline literals), and\n * `metrics.ts` path-label normalization imports the same patterns, so the\n * three consumers cannot disagree on what a route looks like.\n * 2. Bidirectional tests (`openapi-catalog.test.ts`) — every catalog entry is\n * probed against the live router (catalog → router) AND every dispatch\n * literal/pattern imported from this module must have a catalog entry\n * whose example path it matches (router → catalog).\n * 3. Runtime drift guard — the dispatcher's terminal 404 consults\n * `matchRouteDefinition` and warns when the registry claims a route the\n * dispatcher just missed.\n *\n * `path` is the OpenAPI path template (`{param}` for dynamic segments).\n * `examplePath` is a concrete path the drift test probes against the live\n * server (a templated path can never be fetched verbatim).\n *\n * Plugin mounts (`Mountable`: A2A/MCP/AG-UI/vector) are dynamic per-server and\n * intentionally excluded — this registry covers the built-in first-class\n * surfaces only. Likewise excluded from `ROUTE_DEFINITIONS` (their bindings\n * above are still shared so dispatch cannot typo them): the WebSocket upgrades\n * (`REALTIME_PATH`, `GEMINI_LIVE_PATH`, `LIVE_PATH` — same URL as an HTTP\n * route or no HTTP method at all, so they cannot appear in an HTTP catalog)\n * and the header-gated fal.ai proxy branch (`FAL_ROUTE_RE` +\n * `x-fal-target-host`, a dynamic upstream mirror rather than a fixed route).\n */\n\nexport const COMPLETIONS_PATH = \"/v1/chat/completions\";\nexport const RESPONSES_PATH = \"/v1/responses\";\nexport const REALTIME_PATH = \"/v1/realtime\";\nexport const LIVE_PATH = \"/v1/live/sessions\";\nexport const GEMINI_LIVE_PATH =\n \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\nexport const MESSAGES_PATH = \"/v1/messages\";\nexport const MESSAGES_COUNT_PATH = \"/v1/messages/count_tokens\";\nexport const EMBEDDINGS_PATH = \"/v1/embeddings\";\nexport const COHERE_CHAT_PATH = \"/v2/chat\";\nexport const COHERE_EMBED_PATH = \"/v2/embed\";\nexport const SEARCH_PATH = \"/search\";\nexport const RERANK_PATH = \"/v2/rerank\";\nexport const MODERATIONS_PATH = \"/v1/moderations\";\nexport const IMAGES_PATH = \"/v1/images/generations\";\nexport const IMAGES_EDIT_PATH = \"/v1/images/edits\";\nexport const IMAGES_VARIATIONS_PATH = \"/v1/images/variations\";\nexport const SPEECH_PATH = \"/v1/audio/speech\";\nexport const TRANSCRIPTIONS_PATH = \"/v1/audio/transcriptions\";\nexport const TRANSLATIONS_PATH = \"/v1/audio/translations\";\nexport const VIDEOS_PATH = \"/v1/videos\";\n// Grok submit is an exact literal; Grok status reuses the OpenAI\n// `/v1/videos/{id}` shape (dispatch guards `id !== \"generations\"` and the\n// job-map lookup disambiguates Sora). Shared with metrics.ts normalization.\nexport const GROK_VIDEO_SUBMIT_PATH = \"/v1/videos/generations\";\nexport const GROK_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\nexport const OPENAI_VIDEO_STATUS_RE = /^\\/v1\\/videos\\/([^/]+)$/;\nexport const GEMINI_PREDICT_RE = /^\\/v1beta\\/models\\/([^:]+):predict$/;\n// Veo submit (`:predictLongRunning`, anchored so it never collides with the\n// bare Gemini `:predict` route) + Veo operation polling. The operation RE\n// allows multi-segment names (`operations/.+`); the catalog template below\n// uses the single-segment canonical form. Shared with metrics.ts.\nexport const VEO_PREDICT_LRO_RE = /^\\/v1beta\\/models\\/([^:]+):predictLongRunning$/;\nexport const VEO_OPERATION_RE = /^\\/v1beta\\/(operations\\/.+)$/;\n// BytePlus Ark (Seedance) task lifecycle. The `/api/v3` prefix is ENUMERATED\n// rather than wildcarded: a tolerant prefix would additionally claim\n// `/fal/contents/generations/tasks`, which dispatches pre-rewrite ~1,100\n// lines ahead of every fal branch. Shared with metrics.ts normalization.\nexport const BYTEPLUS_VIDEO_SUBMIT_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks$/;\nexport const BYTEPLUS_VIDEO_STATUS_RE = /^(?:\\/api\\/v3)?\\/contents\\/generations\\/tasks\\/([^/]+)$/;\nexport const ELEVENLABS_SOUND_GENERATION_PATH = \"/v1/sound-generation\";\nexport const ELEVENLABS_TTS_RE = /^\\/v1\\/text-to-speech\\/([^/]+)$/;\nexport const ELEVENLABS_MUSIC_RE = /^\\/v1\\/music(?:\\/(.+))?$/;\nexport const ELEVENLABS_VOICE_DESIGN_PATH = \"/v1/text-to-voice/design\";\nexport const ELEVENLABS_VOICE_CREATE_PATH = \"/v1/text-to-voice\";\nexport const ELEVENLABS_VOICE_RE = /^\\/v1\\/voices\\/([^/]+)$/;\nexport const FAL_QUEUE_SUBMIT_RE = /^\\/fal\\/queue\\/submit\\/(.+)$/;\nexport const FAL_QUEUE_REQUESTS_RE = /^\\/fal\\/queue\\/requests\\/(.+)$/;\nexport const FAL_RUN_RE = /^\\/fal\\/run\\/(.+)$/;\n// The fal.ai route shape, shared with server.ts dispatch and metrics.ts path\n// labels: a bare `/fal` is routed (header-gated upstream mirror), so one\n// binding keeps the route rule and the label rule from drifting.\nexport const FAL_ROUTE_RE = /^\\/fal(?:\\/.*)?$/;\nexport const GEMINI_INTERACTIONS_PATH = \"/v1beta/interactions\";\nexport const GEMINI_PATH_RE = /^\\/v1beta\\/models\\/([^:]+):(generateContent|streamGenerateContent)$/;\nexport const GEMINI_COUNT_RE = /^\\/v1beta\\/models\\/([^:]+):countTokens$/;\nexport const GEMINI_EMBED_RE = /^\\/v1beta\\/models\\/([^:]+):embedContent$/;\nexport const AZURE_DEPLOYMENT_RE =\n /^\\/openai\\/deployments\\/([^/]+)\\/(chat\\/completions|embeddings)$/;\nexport const BEDROCK_INVOKE_RE = /^\\/model\\/([^/]+)\\/invoke$/;\nexport const BEDROCK_STREAM_RE = /^\\/model\\/([^/]+)\\/invoke-with-response-stream$/;\nexport const BEDROCK_CONVERSE_RE = /^\\/model\\/([^/]+)\\/converse$/;\nexport const BEDROCK_CONVERSE_STREAM_RE = /^\\/model\\/([^/]+)\\/converse-stream$/;\nexport const VERTEX_AI_RE =\n /^\\/v1\\/projects\\/[^/]+\\/locations\\/[^/]+\\/publishers\\/google\\/models\\/([^/:]+):(generateContent|streamGenerateContent)$/;\n\nexport const OLLAMA_CHAT_PATH = \"/api/chat\";\nexport const OLLAMA_GENERATE_PATH = \"/api/generate\";\nexport const OLLAMA_EMBEDDINGS_PATH = \"/api/embeddings\";\nexport const OLLAMA_EMBED_PATH = \"/api/embed\";\nexport const OLLAMA_TAGS_PATH = \"/api/tags\";\n\nexport const OPENROUTER_VIDEOS_PATH = \"/api/v1/videos\";\nexport const OPENROUTER_VIDEO_MODELS_PATH = \"/api/v1/videos/models\";\nexport const OPENROUTER_MODELS_PATH = \"/api/v1/models\";\nexport const OPENROUTER_KEY_PATH = \"/api/v1/key\";\nexport const OPENROUTER_CREDITS_PATH = \"/api/v1/credits\";\n// Dispatched in server.ts (content RE → models exact → status RE → submit\n// exact) and reused by metrics.ts path-label normalization — one binding so\n// dispatch, catalog, and metric labels cannot disagree.\nexport const OPENROUTER_VIDEO_CONTENT_RE = /^\\/api\\/v1\\/videos\\/([^/]+)\\/content$/;\nexport const OPENROUTER_VIDEO_STATUS_RE = /^\\/api\\/v1\\/videos\\/([^/]+)$/;\n\nexport const HEALTH_PATH = \"/health\";\nexport const READY_PATH = \"/ready\";\nexport const METRICS_PATH = \"/metrics\";\nexport const MODELS_PATH = \"/v1/models\";\nexport const MODELS_DETAIL_RE = /^\\/v1\\/models\\/(.+)$/;\nexport const GEMINI_MODELS_PATH = \"/v1beta/models\";\n// Colon action suffixes remain with existing routes, before ID decoding.\nexport const GEMINI_MODEL_DETAIL_RE = /^\\/v1beta\\/models\\/((?:(?!%3[aA])[^:])+)$/;\nexport const REQUESTS_PATH = \"/v1/_requests\";\n\nexport const FILES_PATH = \"/v1/files\";\n// Shared with metrics.ts path-label normalization (file ids are random\n// `file-…` values; labels collapse to `{id}`).\nexport const FILES_CONTENT_RE = /^\\/v1\\/files\\/([^/]+)\\/content$/;\nexport const FILES_ID_RE = /^\\/v1\\/files\\/([^/]+)$/;\n\n// OpenAI Batches API. Shared with server.ts dispatch and metrics.ts normalization.\nexport const BATCHES_PATH = \"/v1/batches\";\nexport const BATCHES_CANCEL_RE = /^\\/v1\\/batches\\/([^/]+)\\/cancel$/;\nexport const BATCHES_ID_RE = /^\\/v1\\/batches\\/([^/]+)$/;\nexport const FINE_TUNING_JOBS_PATH = \"/v1/fine_tuning/jobs\";\nexport const FINE_TUNING_ID_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)$/;\nexport const FINE_TUNING_CANCEL_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)\\/cancel$/;\nexport const FINE_TUNING_EVENTS_RE = /^\\/v1\\/fine_tuning\\/jobs\\/([^/]+)\\/events$/;\n\n// OpenAI Vector Stores API. Shared with server.ts dispatch and metrics.ts\n// path-label normalization (store/file/batch ids are caller-minted, so every\n// id-bearing path collapses to a placeholder). Dispatch order in server.ts is\n// most-specific RE first (search → batch-files → batch-cancel → batch → file →\n// id), then the two sub-collection REs below, then the collection exact path.\nexport const VECTOR_STORES_PATH = \"/v1/vector_stores\";\nexport const VECTOR_STORES_SEARCH_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/search$/;\nexport const VECTOR_STORES_BATCH_FILES_RE =\n /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)\\/files$/;\nexport const VECTOR_STORES_BATCH_CANCEL_RE =\n /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)\\/cancel$/;\nexport const VECTOR_STORES_BATCH_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches\\/([^/]+)$/;\nexport const VECTOR_STORES_FILE_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/files\\/([^/]+)$/;\nexport const VECTOR_STORES_ID_RE = /^\\/v1\\/vector_stores\\/([^/]+)$/;\nexport const VECTOR_STORES_FILES_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/files$/;\nexport const VECTOR_STORES_FILE_BATCHES_RE = /^\\/v1\\/vector_stores\\/([^/]+)\\/file_batches$/;\n\nexport const CONTROL_PREFIX = \"/__aimock\";\n\nexport interface RouteDefinition {\n method: string;\n /** OpenAPI path template, e.g. `/v1/text-to-speech/{voice_id}`. */\n path: string;\n /** Concrete path the live-router drift test probes. Defaults to `path`. */\n examplePath?: string;\n service: string;\n description: string;\n}\n\n/**\n * Every built-in route, in the same order the dispatcher checks them\n * (mounts → ollama → openrouter-video → byteplus → openrouter discovery →\n * azure/compat → ops → models/requests → provider handlers → fal → chat).\n * Regex branches are represented by their canonical OpenAPI template.\n */\nexport const ROUTE_DEFINITIONS: RouteDefinition[] = [\n // Ollama (pre-rewrite band)\n { method: \"POST\", path: OLLAMA_CHAT_PATH, service: \"ollama\", description: \"Ollama chat\" },\n {\n method: \"POST\",\n path: OLLAMA_GENERATE_PATH,\n service: \"ollama\",\n description: \"Ollama generate\",\n },\n {\n method: \"POST\",\n path: OLLAMA_EMBEDDINGS_PATH,\n service: \"ollama\",\n description: \"Ollama embeddings (legacy path)\",\n },\n {\n method: \"POST\",\n path: OLLAMA_EMBED_PATH,\n service: \"ollama\",\n description: \"Ollama embed (current path)\",\n },\n { method: \"GET\", path: OLLAMA_TAGS_PATH, service: \"ollama\", description: \"Ollama tags\" },\n // OpenRouter async video lifecycle\n {\n method: \"GET\",\n path: \"/api/v1/videos/{jobId}/content\",\n examplePath: \"/api/v1/videos/test-job/content\",\n service: \"video\",\n description: \"OpenRouter video content download\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_VIDEO_MODELS_PATH,\n service: \"video\",\n description: \"OpenRouter video model listing\",\n },\n {\n method: \"GET\",\n path: \"/api/v1/videos/{jobId}\",\n examplePath: \"/api/v1/videos/test-job\",\n service: \"video\",\n description: \"OpenRouter video job status\",\n },\n {\n method: \"POST\",\n path: OPENROUTER_VIDEOS_PATH,\n service: \"video\",\n description: \"OpenRouter video job submit\",\n },\n // BytePlus Ark (Seedance) — canonical + /api/v3-prefixed aliases\n {\n method: \"GET\",\n path: \"/contents/generations/tasks/{id}\",\n examplePath: \"/contents/generations/tasks/test-id\",\n service: \"video\",\n description: \"BytePlus video task status\",\n },\n {\n method: \"GET\",\n path: \"/api/v3/contents/generations/tasks/{id}\",\n examplePath: \"/api/v3/contents/generations/tasks/test-id\",\n service: \"video\",\n description: \"BytePlus video task status (api/v3 prefix)\",\n },\n {\n method: \"POST\",\n path: \"/contents/generations/tasks\",\n service: \"video\",\n description: \"BytePlus video task submit\",\n },\n {\n method: \"POST\",\n path: \"/api/v3/contents/generations/tasks\",\n service: \"video\",\n description: \"BytePlus video task submit (api/v3 prefix)\",\n },\n // OpenRouter discovery\n {\n method: \"GET\",\n path: OPENROUTER_MODELS_PATH,\n service: \"openrouter\",\n description: \"OpenRouter models\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_KEY_PATH,\n service: \"openrouter\",\n description: \"OpenRouter key info\",\n },\n {\n method: \"GET\",\n path: OPENROUTER_CREDITS_PATH,\n service: \"openrouter\",\n description: \"OpenRouter credits\",\n },\n // Azure OpenAI deployments (rewritten to /v1/* downstream)\n {\n method: \"POST\",\n path: \"/openai/deployments/{deploymentId}/chat/completions\",\n examplePath: \"/openai/deployments/test-deployment/chat/completions\",\n service: \"azure\",\n description: \"Azure OpenAI chat completions\",\n },\n {\n method: \"POST\",\n path: \"/openai/deployments/{deploymentId}/embeddings\",\n examplePath: \"/openai/deployments/test-deployment/embeddings\",\n service: \"azure\",\n description: \"Azure OpenAI embeddings\",\n },\n // Ops\n { method: \"GET\", path: HEALTH_PATH, service: \"ops\", description: \"Health probe (public)\" },\n { method: \"GET\", path: READY_PATH, service: \"ops\", description: \"Readiness probe (public)\" },\n { method: \"GET\", path: METRICS_PATH, service: \"ops\", description: \"Prometheus metrics (public)\" },\n // Models + legacy journal\n { method: \"GET\", path: MODELS_PATH, service: \"openai\", description: \"Models listing\" },\n {\n method: \"GET\",\n path: \"/v1/models/{id}\",\n examplePath: \"/v1/models/gpt-4o\",\n service: \"openai\",\n description: \"Model detail (OpenAI or Anthropic response dialect)\",\n },\n {\n method: \"GET\",\n path: GEMINI_MODELS_PATH,\n service: \"gemini\",\n description: \"Gemini models listing\",\n },\n {\n method: \"GET\",\n path: \"/v1beta/models/{model}\",\n examplePath: \"/v1beta/models/gemini-2.0-flash\",\n service: \"gemini\",\n description: \"Gemini model detail\",\n },\n { method: \"GET\", path: REQUESTS_PATH, service: \"control\", description: \"Legacy journal listing\" },\n {\n method: \"DELETE\",\n path: REQUESTS_PATH,\n service: \"control\",\n description: \"Legacy journal clear\",\n },\n // OpenAI Responses + Anthropic + Cohere + core OpenAI\n { method: \"POST\", path: RESPONSES_PATH, service: \"openai\", description: \"OpenAI Responses API\" },\n { method: \"POST\", path: MESSAGES_PATH, service: \"anthropic\", description: \"Anthropic messages\" },\n {\n method: \"POST\",\n path: MESSAGES_COUNT_PATH,\n service: \"anthropic\",\n description: \"Estimated Anthropic input token count\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:countTokens\",\n examplePath: \"/v1beta/models/gemini-2.0-flash:countTokens\",\n service: \"gemini\",\n description: \"Estimated Gemini input token count\",\n },\n { method: \"POST\", path: COHERE_CHAT_PATH, service: \"cohere\", description: \"Cohere chat\" },\n { method: \"POST\", path: COHERE_EMBED_PATH, service: \"cohere\", description: \"Cohere embed\" },\n { method: \"POST\", path: EMBEDDINGS_PATH, service: \"openai\", description: \"OpenAI embeddings\" },\n {\n method: \"POST\",\n path: IMAGES_PATH,\n service: \"images\",\n description: \"Image generation\",\n },\n {\n method: \"POST\",\n path: IMAGES_EDIT_PATH,\n service: \"images\",\n description: \"Image edits (multipart)\",\n },\n {\n method: \"POST\",\n path: \"/v1/images/variations\",\n service: \"images\",\n description: \"Image variations (removed upstream 2026-05-12 — replays the real 404)\",\n },\n { method: \"POST\", path: SPEECH_PATH, service: \"speech\", description: \"Text-to-speech\" },\n {\n method: \"POST\",\n path: TRANSCRIPTIONS_PATH,\n service: \"transcription\",\n description: \"Audio transcription (multipart)\",\n },\n {\n method: \"POST\",\n path: TRANSLATIONS_PATH,\n service: \"transcription\",\n description: \"Audio translation (multipart)\",\n },\n // Video: Sora submit, Grok submit, shared status\n {\n method: \"POST\",\n path: VIDEOS_PATH,\n service: \"video\",\n description: \"Video generation submit (Sora)\",\n },\n {\n method: \"POST\",\n path: \"/v1/videos/generations\",\n service: \"video\",\n description: \"Grok video generation submit\",\n },\n {\n method: \"GET\",\n path: \"/v1/videos/{id}\",\n examplePath: \"/v1/videos/test-id\",\n service: \"video\",\n description: \"Video job status (Sora/Grok)\",\n },\n // Veo\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:predictLongRunning\",\n examplePath: \"/v1beta/models/veo-3:predictLongRunning\",\n service: \"video\",\n description: \"Veo video submit\",\n },\n {\n method: \"GET\",\n path: \"/v1beta/operations/{name}\",\n examplePath: \"/v1beta/operations/test-op\",\n service: \"video\",\n description: \"Veo operation status\",\n },\n // Gemini Imagen + interactions + embeddings + generate\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:predict\",\n examplePath: \"/v1beta/models/imagen-3:predict\",\n service: \"gemini\",\n description: \"Gemini Imagen predict\",\n },\n {\n method: \"POST\",\n path: GEMINI_INTERACTIONS_PATH,\n service: \"gemini\",\n description: \"Gemini interactions\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:embedContent\",\n examplePath: \"/v1beta/models/text-embedding-004:embedContent\",\n service: \"gemini\",\n description: \"Gemini embedContent\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:generateContent\",\n examplePath: \"/v1beta/models/gemini-2.0-flash:generateContent\",\n service: \"gemini\",\n description: \"Gemini generateContent\",\n },\n {\n method: \"POST\",\n path: \"/v1beta/models/{model}:streamGenerateContent\",\n examplePath: \"/v1beta/models/gemini-2.0-flash:streamGenerateContent\",\n service: \"gemini\",\n description: \"Gemini streaming\",\n },\n // Vertex AI (same shapes, full resource path)\n {\n method: \"POST\",\n path: \"/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent\",\n examplePath:\n \"/v1/projects/test-proj/locations/us-central1/publishers/google/models/gemini-2.0-flash:generateContent\",\n service: \"vertex\",\n description: \"Vertex AI generateContent\",\n },\n {\n method: \"POST\",\n path: \"/v1/projects/{project}/locations/{location}/publishers/google/models/{model}:streamGenerateContent\",\n examplePath:\n \"/v1/projects/test-proj/locations/us-central1/publishers/google/models/gemini-2.0-flash:streamGenerateContent\",\n service: \"vertex\",\n description: \"Vertex AI streaming\",\n },\n // Bedrock\n {\n method: \"POST\",\n path: \"/model/{modelId}/invoke\",\n examplePath: \"/model/anthropic.claude-3/invoke\",\n service: \"bedrock\",\n description: \"Bedrock invoke\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/invoke-with-response-stream\",\n examplePath: \"/model/anthropic.claude-3/invoke-with-response-stream\",\n service: \"bedrock\",\n description: \"Bedrock streaming invoke\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/converse\",\n examplePath: \"/model/anthropic.claude-3/converse\",\n service: \"bedrock\",\n description: \"Bedrock converse\",\n },\n {\n method: \"POST\",\n path: \"/model/{modelId}/converse-stream\",\n examplePath: \"/model/anthropic.claude-3/converse-stream\",\n service: \"bedrock\",\n description: \"Bedrock converse streaming\",\n },\n // Search / rerank / moderation\n { method: \"POST\", path: SEARCH_PATH, service: \"search\", description: \"Tavily search\" },\n { method: \"POST\", path: RERANK_PATH, service: \"rerank\", description: \"Cohere rerank\" },\n { method: \"POST\", path: MODERATIONS_PATH, service: \"moderation\", description: \"Moderation\" },\n // ElevenLabs\n {\n method: \"POST\",\n path: ELEVENLABS_SOUND_GENERATION_PATH,\n service: \"elevenlabs\",\n description: \"ElevenLabs sound generation\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-voice/design\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice design preview\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs save designed voice\",\n },\n {\n method: \"GET\",\n path: \"/v1/voices/{voice_id}\",\n examplePath: \"/v1/voices/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice slot get\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/voices/{voice_id}\",\n examplePath: \"/v1/voices/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs voice slot delete\",\n },\n {\n method: \"POST\",\n path: \"/v1/text-to-speech/{voice_id}\",\n examplePath: \"/v1/text-to-speech/test-voice\",\n service: \"elevenlabs\",\n description: \"ElevenLabs text-to-speech\",\n },\n {\n method: \"POST\",\n path: \"/v1/music\",\n service: \"elevenlabs\",\n description: \"ElevenLabs music\",\n },\n {\n method: \"POST\",\n path: \"/v1/music/{subtype}\",\n examplePath: \"/v1/music/generation\",\n service: \"elevenlabs\",\n description: \"ElevenLabs music sub-operation\",\n },\n // fal.ai\n {\n method: \"POST\",\n path: \"/fal/queue/submit/{model}\",\n examplePath: \"/fal/queue/submit/fal-ai-test\",\n service: \"fal\",\n description: \"fal.ai queue submit\",\n },\n {\n method: \"GET\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result\",\n },\n {\n method: \"POST\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result (POST)\",\n },\n {\n method: \"PUT\",\n path: \"/fal/queue/requests/{requestId}\",\n examplePath: \"/fal/queue/requests/test-id\",\n service: \"fal\",\n description: \"fal.ai queue status/result (PUT)\",\n },\n {\n method: \"POST\",\n path: \"/fal/run/{model}\",\n examplePath: \"/fal/run/fal-ai-test\",\n service: \"fal\",\n description: \"fal.ai synchronous run\",\n },\n // OpenAI Batches API (cancel before id: the id RE would swallow /cancel)\n {\n method: \"POST\",\n path: \"/v1/batches/{batch_id}/cancel\",\n examplePath: \"/v1/batches/batch-test123/cancel\",\n service: \"batches\",\n description: \"Batch cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/batches/{batch_id}\",\n examplePath: \"/v1/batches/batch-test123\",\n service: \"batches\",\n description: \"Batch retrieve\",\n },\n { method: \"GET\", path: BATCHES_PATH, service: \"batches\", description: \"Batch list\" },\n {\n method: \"POST\",\n path: BATCHES_PATH,\n service: \"batches\",\n description: \"Batch create\",\n },\n // OpenAI Files API\n {\n method: \"GET\",\n path: \"/v1/files/{file_id}/content\",\n examplePath: \"/v1/files/file-test123/content\",\n service: \"files\",\n description: \"File content download\",\n },\n {\n method: \"GET\",\n path: \"/v1/files/{file_id}\",\n examplePath: \"/v1/files/file-test123\",\n service: \"files\",\n description: \"File retrieve\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/files/{file_id}\",\n examplePath: \"/v1/files/file-test123\",\n service: \"files\",\n description: \"File delete\",\n },\n { method: \"GET\", path: \"/v1/files\", service: \"files\", description: \"File list\" },\n {\n method: \"POST\",\n path: \"/v1/files\",\n service: \"files\",\n description: \"File upload (JSON or multipart)\",\n },\n // OpenAI fine-tuning jobs\n {\n method: \"POST\",\n path: \"/v1/fine_tuning/jobs/{job_id}/cancel\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123/cancel\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs/{job_id}/events\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123/events\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job events\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs/{job_id}\",\n examplePath: \"/v1/fine_tuning/jobs/ftjob-test123\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job retrieve\",\n },\n {\n method: \"GET\",\n path: \"/v1/fine_tuning/jobs\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job list\",\n },\n {\n method: \"POST\",\n path: \"/v1/fine_tuning/jobs\",\n service: \"fine-tuning\",\n description: \"Fine-tuning job create\",\n },\n // OpenAI Vector Stores (cancel before id is not needed: the id RE is\n // anchored to the exact 3-segment path so it cannot swallow sub-resources;\n // most-specific REs still read first in the dispatcher).\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/search\",\n examplePath: \"/v1/vector_stores/vs-test123/search\",\n service: \"vector-stores\",\n description: \"Vector store search\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123/files\",\n service: \"vector-stores\",\n description: \"Vector file batch files list\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}/cancel\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123/cancel\",\n service: \"vector-stores\",\n description: \"Vector file batch cancel\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches/{batch_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches/vsbf-test123\",\n service: \"vector-stores\",\n description: \"Vector file batch retrieve\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/file_batches\",\n examplePath: \"/v1/vector_stores/vs-test123/file_batches\",\n service: \"vector-stores\",\n description: \"Vector file batch create\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/files/{file_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/files/file-test123\",\n service: \"vector-stores\",\n description: \"Vector store file retrieve\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/vector_stores/{vector_store_id}/files/{file_id}\",\n examplePath: \"/v1/vector_stores/vs-test123/files/file-test123\",\n service: \"vector-stores\",\n description: \"Vector store file delete\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/files\",\n service: \"vector-stores\",\n description: \"Vector store file attach\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}/files\",\n examplePath: \"/v1/vector_stores/vs-test123/files\",\n service: \"vector-stores\",\n description: \"Vector store files list\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store retrieve\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store modify\",\n },\n {\n method: \"DELETE\",\n path: \"/v1/vector_stores/{vector_store_id}\",\n examplePath: \"/v1/vector_stores/vs-test123\",\n service: \"vector-stores\",\n description: \"Vector store delete\",\n },\n {\n method: \"GET\",\n path: \"/v1/vector_stores\",\n service: \"vector-stores\",\n description: \"Vector store list\",\n },\n {\n method: \"POST\",\n path: \"/v1/vector_stores\",\n service: \"vector-stores\",\n description: \"Vector store create\",\n },\n // Chat completions (terminal dispatcher branch)\n {\n method: \"POST\",\n path: COMPLETIONS_PATH,\n service: \"openai\",\n description: \"OpenAI chat completions\",\n },\n // Control API\n { method: \"GET\", path: \"/__aimock/health\", service: \"control\", description: \"Control health\" },\n { method: \"GET\", path: \"/__aimock/journal\", service: \"control\", description: \"Request journal\" },\n {\n method: \"GET\",\n path: \"/__aimock/fixtures\",\n service: \"control\",\n description: \"Fixture count\",\n },\n { method: \"POST\", path: \"/__aimock/fixtures\", service: \"control\", description: \"Add fixtures\" },\n {\n method: \"DELETE\",\n path: \"/__aimock/fixtures\",\n service: \"control\",\n description: \"Clear fixtures\",\n },\n { method: \"GET\", path: \"/__aimock/chaos\", service: \"control\", description: \"Read chaos config\" },\n { method: \"POST\", path: \"/__aimock/chaos\", service: \"control\", description: \"Set chaos config\" },\n {\n method: \"DELETE\",\n path: \"/__aimock/chaos\",\n service: \"control\",\n description: \"Clear chaos override\",\n },\n { method: \"POST\", path: \"/__aimock/reset\", service: \"control\", description: \"Full reset\" },\n {\n method: \"POST\",\n path: \"/__aimock/reset/journal\",\n service: \"control\",\n description: \"Journal-only reset\",\n },\n {\n method: \"POST\",\n path: \"/__aimock/reset/fixtures\",\n service: \"control\",\n description: \"Full reset (deprecated alias)\",\n },\n { method: \"POST\", path: \"/__aimock/error\", service: \"control\", description: \"One-shot error\" },\n {\n method: \"GET\",\n path: \"/__aimock/openapi.json\",\n service: \"control\",\n description: \"OpenAPI catalog\",\n },\n { method: \"GET\", path: \"/__aimock/routes\", service: \"control\", description: \"Flat route list\" },\n];\n\n/**\n * Compile an OpenAPI path template (`{param}` segments) to the equivalent\n * single-segment matcher. Every `{param}` becomes `[^/]+`; everything else\n * matches literally (including `:action` suffixes like `:predict`).\n *\n * This is intentionally the CANONICAL matcher, not a copy of each\n * dispatcher's regex: a couple of dispatch regexes are deliberately wider\n * (e.g. `VEO_OPERATION_RE` allows multi-segment operation names), so the\n * registry matcher may miss exotic paths the dispatcher serves — but it must\n * never CLAIM a path the dispatcher does not serve, which is the direction\n * the runtime drift guard relies on.\n */\nexport function templateToRegExp(template: string): RegExp {\n if (template === \"/v1/models/{id}\") return MODELS_DETAIL_RE;\n if (template === \"/v1beta/models/{model}\") return GEMINI_MODEL_DETAIL_RE;\n const pattern = template\n .split(/(\\{[^}]+\\})/g)\n .map((part) =>\n /^\\{[^}]+\\}$/.test(part) ? \"[^/]+\" : part.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"),\n )\n .join(\"\");\n return new RegExp(`^${pattern}$`);\n}\n\n/**\n * Find the registry entry serving `METHOD pathname`, in dispatcher order\n * (first match wins — `ROUTE_DEFINITIONS` is kept in dispatch order for\n * exactly this reason). Returns `undefined` when no catalog entry covers the\n * request. Method comparison is case-insensitive, like the dispatcher.\n */\nexport function matchRouteDefinition(\n method: string,\n pathname: string,\n): RouteDefinition | undefined {\n const want = method.toUpperCase();\n for (const def of ROUTE_DEFINITIONS) {\n if (def.method.toUpperCase() !== want) continue;\n if (def.path === pathname) return def;\n if (def.path.includes(\"{\") && templateToRegExp(def.path).test(pathname)) return def;\n }\n return undefined;\n}\n"],"mappings":"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ChF;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAID;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,aAAa;EACb,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CAED;EAAE,QAAQ;EAAO,MAAM;EAAoB,SAAS;EAAW,aAAa;EAAkB;CAC9F;EAAE,QAAQ;EAAO,MAAM;EAAqB,SAAS;EAAW,aAAa;EAAmB;CAChG;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAsB,SAAS;EAAW,aAAa;EAAgB;CAC/F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAqB;CAChG;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAoB;CAChG;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAc;CAC1F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAQ,MAAM;EAAmB,SAAS;EAAW,aAAa;EAAkB;CAC9F;EACE,QAAQ;EACR,MAAM;EACN,SAAS;EACT,aAAa;EACd;CACD;EAAE,QAAQ;EAAO,MAAM;EAAoB,SAAS;EAAW,aAAa;EAAmB;CAChG;;;;;;;;;;;;;AAcD,SAAgB,iBAAiB,UAA0B;AACzD,KAAI,aAAa,kBAAmB,QAAO;AAC3C,KAAI,aAAa,yBAA0B,QAAO;CAClD,MAAM,UAAU,SACb,MAAM,eAAe,CACrB,KAAK,SACJ,cAAc,KAAK,KAAK,GAAG,UAAU,KAAK,QAAQ,uBAAuB,OAAO,CACjF,CACA,KAAK,GAAG;AACX,QAAO,IAAI,OAAO,IAAI,QAAQ,GAAG;;;;;;;;AASnC,SAAgB,qBACd,QACA,UAC6B;CAC7B,MAAM,OAAO,OAAO,aAAa;AACjC,MAAK,MAAM,OAAO,mBAAmB;AACnC,MAAI,IAAI,OAAO,aAAa,KAAK,KAAM;AACvC,MAAI,IAAI,SAAS,SAAU,QAAO;AAClC,MAAI,IAAI,KAAK,SAAS,IAAI,IAAI,iBAAiB,IAAI,KAAK,CAAC,KAAK,SAAS,CAAE,QAAO"}
|
package/dist/router.cjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
1
2
|
const require_helpers = require('./helpers.cjs');
|
|
2
3
|
const require_fixture_loader = require('./fixture-loader.cjs');
|
|
4
|
+
const require_embedding_request = require('./embedding-request.cjs');
|
|
5
|
+
let node_util = require("node:util");
|
|
3
6
|
|
|
4
7
|
//#region src/router.ts
|
|
5
8
|
function getLastMessageByRole(messages, role) {
|
|
@@ -212,8 +215,10 @@ function matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, op
|
|
|
212
215
|
if (extendedTools) require_helpers.markExtendedResponsesToolsRequest(req);
|
|
213
216
|
let skippedBySequenceOrTurn = 0;
|
|
214
217
|
const contentMatches = [];
|
|
218
|
+
const rawWireMisses = [];
|
|
215
219
|
for (const fixture of fixtures) {
|
|
216
220
|
const { match } = fixture;
|
|
221
|
+
if (match.embeddingRequest !== void 0 && !(0, node_util.isDeepStrictEqual)(match.embeddingRequest, require_embedding_request.getEmbeddingRequest(req))) continue;
|
|
217
222
|
if (match.predicate !== void 0) {
|
|
218
223
|
if (!match.predicate(req)) continue;
|
|
219
224
|
}
|
|
@@ -223,6 +228,7 @@ function matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, op
|
|
|
223
228
|
if (reqEndpoint === "openai-live") {
|
|
224
229
|
if (match.endpoint !== "openai-live" || effective.model !== "gpt-live-1" || typeof response !== "function" && (!liveResponse || effective.model !== response.live.model)) continue;
|
|
225
230
|
} else if (liveResponse) continue;
|
|
231
|
+
const rawWire = typeof response !== "function" && require_helpers.isRawResponse(response) ? { wire: response.raw.wire } : void 0;
|
|
226
232
|
if (match.endpoint !== void 0) {
|
|
227
233
|
if (match.endpoint !== reqEndpoint) continue;
|
|
228
234
|
if (reqEndpoint === "video" && req._videoProvider !== "byteplus" && typeof fixture.response !== "function" && require_helpers.isJSONResponse(fixture.response)) continue;
|
|
@@ -304,6 +310,13 @@ function matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, op
|
|
|
304
310
|
if (match.hasToolResult !== void 0) {
|
|
305
311
|
if (currentTurnHasToolResult(effective.messages) !== match.hasToolResult) continue;
|
|
306
312
|
}
|
|
313
|
+
if (rawWire !== void 0 && rawWire.wire !== options?.wire) {
|
|
314
|
+
rawWireMisses.push({
|
|
315
|
+
fixture,
|
|
316
|
+
wire: rawWire.wire
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
307
320
|
if (match.sequenceIndex !== void 0 && matchCounts !== void 0) {
|
|
308
321
|
if ((matchCounts.get(fixture) ?? 0) !== match.sequenceIndex) {
|
|
309
322
|
skippedBySequenceOrTurn++;
|
|
@@ -318,10 +331,17 @@ function matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, op
|
|
|
318
331
|
}
|
|
319
332
|
contentMatches.push(fixture);
|
|
320
333
|
}
|
|
321
|
-
if (contentMatches.length === 0)
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
334
|
+
if (contentMatches.length === 0) {
|
|
335
|
+
if (options?.logger) for (const miss of rawWireMisses) {
|
|
336
|
+
const fixtureWire = miss.wire === void 0 ? "raw.wire is missing" : `raw.wire=${JSON.stringify(miss.wire)}`;
|
|
337
|
+
const requestWire = options.wire === void 0 ? "request wire is not set" : `request wire "${options.wire}"`;
|
|
338
|
+
options.logger.warn(`raw fixture not served: ${require_helpers.describeMatch(miss.fixture.match, fixtures.indexOf(miss.fixture))} matched the request except its wire (${fixtureWire}, ${requestWire})`);
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
fixture: null,
|
|
342
|
+
skippedBySequenceOrTurn
|
|
343
|
+
};
|
|
344
|
+
}
|
|
325
345
|
const assistantCount = effective.messages.filter((m) => m.role === "assistant").length;
|
|
326
346
|
const { fixture: selected, byUniquePosition } = selectByTurnIndex(contentMatches, assistantCount);
|
|
327
347
|
const selectedTurn = selected.match.turnIndex;
|
package/dist/router.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.cjs","names":["isExtendedResponsesToolsList","isLiveResponse","isJSONResponse","isImageResponse","isAudioResponse","isErrorResponse","isTranscriptionResponse","isVideoResponse","describeMatch","isOneShotError","claimOneShotError"],"sources":["../src/router.ts"],"sourcesContent":["import { claimOneShotError, isOneShotError } from \"./fixture-loader.js\";\nimport type {\n ChatCompletionRequest,\n ChatMessage,\n ContentPart,\n CustomToolDefinition,\n Fixture,\n ToolDefinition,\n} from \"./types.js\";\nimport {\n describeMatch,\n isLiveResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isErrorResponse,\n isExtendedResponsesToolsList,\n markExtendedResponsesToolsRequest,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Every tool an OpenAI Responses request offered (top level, inside\n * `namespace` tools, and from `additional_tools` / `tool_search_output`\n * items), keyed by the converted request. Kept off the request object so the\n * request that predicates and the journal see is unchanged by default.\n */\nconst responsesOfferedTools = new WeakMap<\n object,\n { tools: ToolDefinition[]; customTools: CustomToolDefinition[] }\n>();\n\n/** Record the tools a Responses request offered (see {@link getResponsesOfferedTools}). */\nexport function setResponsesOfferedTools(\n request: ChatCompletionRequest,\n offered: { tools: ToolDefinition[]; customTools: CustomToolDefinition[] },\n): void {\n responsesOfferedTools.set(request, offered);\n}\n\n/** The tools a Responses request offered, for `toolNamespace` matching. */\nexport function getResponsesOfferedTools(\n request: ChatCompletionRequest,\n): { tools: ToolDefinition[]; customTools: CustomToolDefinition[] } | undefined {\n return responsesOfferedTools.get(request);\n}\n\n/**\n * `hasToolResult` request-SHAPE predicate — does the CURRENT turn contain a\n * tool result message? \"Current turn\" = the messages AFTER the last `user`\n * message, so this is scoped to the turn being matched rather than the whole\n * conversation. This is what keeps leg-1 (tool call, `hasToolResult` false) vs\n * leg-2 (narration, `hasToolResult` true) fixtures working across MULTI-TURN\n * sessions: on the 2nd+ user turn the request still carries earlier turns' tool\n * results, and a whole-conversation check would force `hasToolResult=true`\n * forever, so the turn's leg-1 fixture (false) could never match again (\"No\n * fixture matched\" on every pill after the first).\n *\n * This is the SINGLE source of truth shared by the RECORD side\n * ({@link buildFixtureMatch} in recorder.ts, which STAMPS the value onto a new\n * fixture) and the MATCH side ({@link matchFixtureDiagnostic} below, which\n * CHECKS a fixture's stored value against the incoming request). The two MUST\n * agree byte-for-byte: if the recorder stamps a value the matcher would never\n * compute for the same request, a genuinely-recorded fixture can never match\n * its own request. Keeping the predicate in one function is what guarantees\n * they cannot drift.\n *\n * INVARIANT: callers MUST pass the POST-`requestTransform` (normalized)\n * messages — the same array `buildFixtureMatch` and `matchFixtureDiagnostic`\n * both feed here. Passing raw/untransformed messages on one side would let the\n * set-side and check-side diverge again and silently reopen the multi-turn bug.\n *\n * When there is no `user` message at all, `lastUserIdx` stays -1 and the scan\n * covers the whole conversation — the safe/consistent fallback for a user-less\n * request (both sides degrade identically). For a single-turn request whose\n * only tool message trails a lone user message this is identical to the old\n * whole-conversation check.\n */\nexport function currentTurnHasToolResult(messages: ChatMessage[]): boolean {\n let lastUserIdx = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n lastUserIdx = i;\n break;\n }\n }\n return messages.slice(lastUserIdx + 1).some((m) => m.role === \"tool\");\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins SEPARATE system messages with newlines so a substring\n * matcher sees the whole context as one body.\n *\n * Empty handling is symmetric with {@link getTextContent}: a system message\n * with no extractable text (`null`) contributes nothing, while a message that\n * extracts to an empty string is a present-but-empty body. We skip only the\n * `null` (no-text) case so a genuinely empty system message does not inject a\n * stray newline; this matches getTextContent treating \"no text\" and \"empty\n * text\" consistently.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text === null) continue;\n parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n *\n * Multi-part text is joined with `\"\"` (the parts form one logical body split\n * across segments). Empty handling is symmetric with the string path: a string\n * `\"\"` returns `\"\"`, and an array containing at least one text part whose\n * combined text is empty likewise returns `\"\"` (NOT `null`). `null` is reserved\n * for \"no text content at all\" — null content, or an array with no text parts —\n * so callers can distinguish \"absent\" from \"present but empty\" the same way for\n * both content shapes.\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\n/**\n * Text of the last user message, for `userMessage` fixture matching. Normally\n * this is the text of the final `user` message. But some SDKs serialise a\n * single multimodal user turn (prompt text + attachment) into TWO consecutive\n * user messages — a text-only one FOLLOWED by an attachment-only one, e.g.\n * `[{role:\"user\", content:\"describe this\"}, {role:\"user\", content:[{type:\"image_url\",...}]}]`\n * (observed with Microsoft Agent Framework's `agent_framework_openai` image\n * path). The trailing attachment-only message has NO extractable text, so the\n * naive \"last user message\" lookup returns `null` and — because no fixture can\n * key on empty text — every such multimodal fixture misses. We therefore skip\n * trailing user messages that carry no text and use the nearest preceding user\n * message that does. This is deliberately narrow: it only skips text-LESS\n * (`null`) user messages — a message whose text extracts to an explicit empty\n * string `\"\"` is a present-but-empty body and IS returned (so a fixture can key\n * on it), while a genuine multi-message user turn (each message HAS text) is\n * unaffected and still matched on its final message. Returns the nearest\n * preceding user message whose text is non-null (including `\"\"`); returns `null`\n * only when no user message carries any text part at all.\n */\nexport function getLastUserText(messages: ChatMessage[]): string | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role !== \"user\") continue;\n const text = getTextContent(messages[i].content);\n if (text !== null) return text;\n }\n return null;\n}\n\n/**\n * Test a matcher RegExp against a string WITHOUT mutating the caller-supplied\n * regex. `RegExp.prototype.test` advances `lastIndex` as a side effect on\n * `/g` and `/y` regexes; fixtures hold caller-owned RegExp objects, so testing\n * them in place would clobber the caller's positional state (and a `/g` regex\n * reused across match calls would match intermittently). We test a fresh clone\n * (same source + flags) so the caller's object is never touched and every test\n * starts from index 0.\n */\nfunction regexTest(re: RegExp, text: string): boolean {\n return new RegExp(re.source, re.flags).test(text);\n}\n\n/**\n * Result of {@link matchFixtureDiagnostic}: the matched fixture (or `null`) plus\n * the number of fixtures that matched the request SHAPE (every predicate above\n * the sequenceIndex/turnIndex gates) but were rejected ONLY by the\n * sequenceIndex/turnIndex count state.\n *\n * `skippedBySequenceOrTurn > 0` with `fixture === null` distinguishes a\n * \"sequence/turn exhausted\" miss (candidate fixtures existed but their count\n * gate had moved on) from a true \"no fixture had a matching shape\" miss — used\n * to disambiguate the strict-mode 503 message.\n */\nexport interface MatchFixtureDiagnostic {\n fixture: Fixture | null;\n skippedBySequenceOrTurn: number;\n /**\n * `true` when the served fixture was selected by relaxed content-anchored\n * matching even though its `turnIndex` is defined and does NOT equal the\n * request's assistant-message count — i.e. the legacy strict turnIndex gate\n * (now opt-in via `AIMOCK_STRICT_TURN_INDEX`) WOULD HAVE rejected it. Absent /\n * falsy on canonical-position matches, non-turnIndexed matches, and misses.\n * Additive optional field — existing handler destructures are unaffected.\n */\n turnIndexRelaxed?: boolean;\n /**\n * How the served fixture was selected: `\"turnIndex\"` when its `turnIndex`\n * sits exactly at the current assistant count (canonical position),\n * `\"content\"` otherwise (a non-turnIndexed match or a relaxed off-by-N\n * match). Absent on misses. Additive optional field.\n */\n matchedBy?: \"content\" | \"turnIndex\";\n}\n\n/**\n * Optional matcher tuning.\n *\n * `strictTurnIndex` restores the legacy behaviour where `turnIndex` must equal\n * the request's assistant-message count exactly (a hard reject gate). It is set\n * by the record path, where a miss proxies upstream to capture a fresh turn; an\n * earlier-turn fixture must not shadow a longer request or the new turn would\n * never be recorded. Replay (the default, `false`) treats `turnIndex` as a\n * non-fatal disambiguator instead — see {@link selectByTurnIndex}.\n */\nexport interface MatchOptions {\n strictTurnIndex?: boolean;\n /**\n * Optional sink for the one-shot relaxed-turnIndex divergence warning. Handlers\n * pass their `defaults.logger`; the structural `{ warn }` shape avoids an\n * import cycle with logger.ts and keeps the matcher decoupled. When omitted no\n * warning is emitted (the diagnostic fields are still populated). The Logger's\n * own level gate keeps a passing programmatic run (silent default) quiet.\n */\n logger?: { warn(...args: unknown[]): void };\n}\n\n/**\n * Process-level opt-out: when `AIMOCK_STRICT_TURN_INDEX=1` (or `true`) is set,\n * REPLAY selection restores the legacy hard turnIndex gate — a content-matching\n * fixture whose `turnIndex` is defined and `!== assistantCount` is rejected,\n * reproducing origin/main semantics. Follows the `AIMOCK_ALLOW_PRIVATE_URLS`\n * precedent for parsing/precedence. Read per-call (not cached) so tests can flip\n * it. Does NOT affect the record path, which is already strict regardless.\n */\nfunction strictTurnIndexEnv(): boolean {\n const v = process.env.AIMOCK_STRICT_TURN_INDEX;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Process-level set of fixtures for which the relaxed-turnIndex divergence\n * warning has already fired, so each divergent fixture warns at most ONCE per\n * process (throttle). Keyed by the selected fixture's OBJECT IDENTITY: the\n * `Fixture` references in the server's fixtures array are stable across replays\n * (the array is held by reference and only fully replaced on a fixtures reset),\n * so identity uniquely distinguishes divergent fixtures and warns each exactly\n * once. A `WeakSet` was chosen over the previous `JSON.stringify(match)` key for\n * two reasons: (1) stringifying the match DROPS `predicate` functions and\n * serialises any RegExp matcher to `{}`, so two distinct fixtures differing only\n * by a predicate/regex collided to one key and the second's warning was silently\n * suppressed; and (2) a string `Set` only grows, accumulating an entry per\n * divergent shape on a long-lived server, whereas a `WeakSet` auto-evicts when a\n * fixture object is released (e.g. after a fixtures reset drops the references).\n * `let` because `WeakSet` has no `.clear()`, so the test hook reassigns a fresh\n * one.\n */\nlet warnedRelaxedFixtures = new WeakSet<Fixture>();\n\n/**\n * Test-only hook to clear the throttle state between cases. Not part of the\n * public contract. `WeakSet` has no `.clear()`, so reassign a fresh instance.\n */\nexport function _resetTurnIndexRelaxWarnings(): void {\n warnedRelaxedFixtures = new WeakSet<Fixture>();\n}\n\n/**\n * Build the {@link MatchOptions} a request handler must pass to\n * {@link matchFixtureDiagnostic} / {@link matchFixture}, derived from whether\n * the handler is about to record on a miss.\n *\n * EVERY record-capable handler (OpenAI chat, Anthropic messages, Responses,\n * Gemini, Bedrock, Bedrock-Converse, Cohere, Ollama, …) must build its match\n * options through THIS helper rather than hand-rolling `{ strictTurnIndex }` at\n * the call site. Recording proxies upstream on a miss to capture a fresh turn;\n * if `strictTurnIndex` is left false during recording, an earlier-turn fixture\n * can content-shadow a longer request, the `if (!fixture)` record branch never\n * fires, and the new turn is SILENTLY never recorded. Funnelling the decision\n * through one helper makes that wiring impossible for a future handler to miss:\n * pass `recording = true` whenever the handler's own record gate is satisfied\n * (i.e. it will call `proxyAndRecord` on a miss), `false` otherwise.\n */\nexport function recordMatchOptions(\n recording: boolean,\n logger?: { warn(...args: unknown[]): void },\n): MatchOptions {\n return { strictTurnIndex: recording, logger };\n}\n\n/**\n * Match a fixture against a request, additionally reporting WHY a `null` result\n * occurred. Shares the exact predicate loop with {@link matchFixture}; a fixture\n * that passes every shape predicate but fails ONLY the sequenceIndex or\n * turnIndex gate increments `skippedBySequenceOrTurn`. {@link matchFixture} is a\n * thin wrapper that returns the `.fixture` field, so existing callers are\n * unaffected.\n */\nexport function matchFixtureDiagnostic(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n options?: MatchOptions,\n): MatchFixtureDiagnostic {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n // In record mode the server proxies to the upstream on a miss, so a fixture\n // already captured for an EARLIER turn must NOT shadow a longer (later-turn)\n // request — otherwise the new turn would never be proxied and recorded.\n // There turnIndex stays a strict hard gate. Replay (the default) instead\n // treats turnIndex as a non-fatal disambiguator so a canonical multi-bubble\n // run isn't falsely rejected for an off-by-N assistant count.\n // Strict turnIndex is in force when the record path requests it OR the\n // process-level AIMOCK_STRICT_TURN_INDEX opt-out is set (which restores the\n // legacy hard gate for replay too). Record mode passes `true` explicitly; the\n // env only matters when the caller left it `false`/unset (replay).\n const strictTurnIndex = (options?.strictTurnIndex ?? false) || strictTurnIndexEnv();\n // `match.toolNamespace` (and, in resolveResponse, `customToolCalls` /\n // `responsesBlocks`) apply only with responsesTools \"extended\". Earlier\n // releases ignored these keys, so the default ignores them too.\n const extendedTools = isExtendedResponsesToolsList(fixtures);\n // An extended server's list gives each fixture with its held keys applied\n // (createServerFixtureList), so the fixtures are matched as they are read.\n if (extendedTools) markExtendedResponsesToolsRequest(req);\n\n let skippedBySequenceOrTurn = 0;\n // Every fixture whose content / shape predicates (and sequenceIndex gate)\n // pass. turnIndex is applied afterwards as a non-fatal disambiguator.\n const contentMatches: Fixture[] = [];\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n const response = fixture.response;\n const liveResponse = isLiveResponse(response);\n if (reqEndpoint === \"openai-live\") {\n if (\n match.endpoint !== \"openai-live\" ||\n effective.model !== \"gpt-live-1\" ||\n (typeof response !== \"function\" &&\n (!liveResponse || effective.model !== response.live.model))\n )\n continue;\n } else if (liveResponse) {\n continue;\n }\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n // A declared endpoint used to END the shape discussion — the router\n // matched on the name and never looked at the response. That was safe\n // only while one endpoint name meant one response shape. `video` no\n // longer does: it is a SHARED namespace holding both VideoResponse\n // fixtures (Sora / Grok / Veo / OpenRouter) and RawJSONResponse Ark task\n // envelopes (BytePlus), and EVERY BytePlus fixture is required to declare\n // `endpoint: \"video\"`. One such fixture therefore became a first-class\n // candidate for all four other handlers, each of which then failed its\n // own isVideoResponse guard and returned 500 — a single BytePlus fixture\n // broke every other video provider in the same suite.\n //\n // Deliberately narrow: it filters ONLY the raw-JSON half of the shared\n // video namespace out of the non-BytePlus handlers. Every other\n // wrong-shape fixture still reaches its handler, which names the problem\n // itself (\"Fixture response is not a video type\", BytePlus's \"NOT a\n // VideoResponse\" warn) — a clearer diagnostic than a silent no-match.\n // `_videoProvider` is a routing fact of the INBOUND request, so it is\n // read off `req`, not off the post-`requestTransform` copy.\n if (\n reqEndpoint === \"video\" &&\n req._videoProvider !== \"byteplus\" &&\n typeof fixture.response !== \"function\" &&\n isJSONResponse(fixture.response)\n ) {\n continue;\n }\n } else if (\n reqEndpoint &&\n reqEndpoint !== \"chat\" &&\n reqEndpoint !== \"embedding\" &&\n !reqEndpoint.startsWith(\"realtime\")\n ) {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"elevenlabs-tts\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-design\" &&\n (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-get\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-delete\" &&\n (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"translation\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // context — opt-in exact match against the request's _context field.\n // If fixture specifies a context, only match requests with that exact context.\n // If fixture omits context, match any request regardless of _context.\n if (match.context !== undefined) {\n if (effective._context !== match.context) continue;\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n // Use the last user message that actually carries text — see\n // getLastUserText for why a trailing attachment-only user message\n // (multimodal serialisation split) must not shadow the real prompt.\n const text = getLastUserText(effective.messages);\n // `text === null` means no user message carried any text (e.g. a pure\n // attachment turn) — skip. An explicit empty-string body (`\"\"`) is a\n // present-but-empty user message and must be allowed through so a fixture\n // keyed on empty text can match it (see getLastUserText).\n if (text === null) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n if (!regexTest(match.userMessage, text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring, regexp, or array-of-substrings\n // match against the joined text of every system message in the request.\n // Use to gate a fixture on host-supplied context (e.g. agent-context\n // entries) so that when the calling app changes that context the fixture\n // stops matching and the request falls through to the next fixture or\n // upstream proxy.\n //\n // Array form (string[]) requires ALL substrings to be present — useful\n // when the gate must combine multiple non-adjacent tokens (e.g. a default\n // name AND a default activity list whose positions in the serialised\n // context JSON aren't stable).\n if (match.systemMessage !== undefined) {\n const sm = match.systemMessage;\n // Empty array is treated as \"no constraint\" → matches unconditionally,\n // INCLUDING requests with no system text at all. This is the documented\n // contract (same permissive behaviour as not setting systemMessage), so\n // it must be honored BEFORE the no-system-text guard below — otherwise a\n // request without a system message would be wrongly skipped. Validation\n // rejects [] at load time for JSON fixtures; programmatic callers that\n // pass [] get this permissive behaviour.\n if (Array.isArray(sm) && sm.length === 0) {\n // no constraint — fall through to the next predicate\n } else {\n const text = getSystemText(effective.messages);\n // Deliberately `!text` (not `text === null`): unlike userMessage/inputText,\n // getSystemText returns `\"\"` for BOTH \"a present but empty system message\"\n // AND \"no system message at all\", so it exposes no absent-vs-empty\n // distinction at the request level. Allowing `\"\"` through would make a\n // `systemMessage: \"\"` / `/^$/` fixture a catch-all firing on every\n // no-system-message request. There is no shipped-fixture demand for\n // matching an empty system prompt, so we keep the falsy guard here. If a\n // real need arises, add a getSystemText sibling that returns `null` when\n // absent and `\"\"` when present-empty, then switch to `text === null`.\n if (!text) continue;\n if (Array.isArray(sm)) {\n let allPresent = true;\n for (const needle of sm) {\n if (!text.includes(needle)) {\n allPresent = false;\n break;\n }\n }\n if (!allPresent) continue;\n } else if (typeof sm === \"string\") {\n if (useExactMatch) {\n if (text !== sm) continue;\n } else {\n if (!text.includes(sm)) continue;\n }\n } else {\n if (!regexTest(sm, text)) continue;\n }\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolResultContains — substring gate on the LAST message's text content\n // when that message is a tool result. Same last-message rule as toolCallId\n // (a tool-result fixture answers the model's next call after a tool round),\n // but discriminates on the result PAYLOAD instead of the call id — needed\n // when approve/cancel resumes share the same toolCallId and differ only\n // inside the tool-result JSON.\n if (match.toolResultContains !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\") continue;\n const text = getTextContent(last.content);\n if (text === null || !text.includes(match.toolResultContains)) continue;\n }\n\n // toolName — match against any tool definition by function.name (and,\n // with responsesTools \"extended\", any Responses custom tool by name).\n // toolNamespace — exact OpenAI Responses namespace of an offered tool; with\n // toolName, ONE tool must carry both (Codex routes by the exact pair). Only\n // with responsesTools \"extended\"; there a toolNamespace fixture sees every\n // tool the Responses request offered. The default ignores the key.\n const toolNamespace = extendedTools ? match.toolNamespace : undefined;\n if (match.toolName !== undefined || toolNamespace !== undefined) {\n const offered =\n toolNamespace !== undefined\n ? (getResponsesOfferedTools(effective) ?? getResponsesOfferedTools(req))\n : undefined;\n const tools = offered?.tools ?? (Array.isArray(effective.tools) ? effective.tools : []);\n const customTools =\n offered?.customTools ?? (Array.isArray(effective.customTools) ? effective.customTools : []);\n const found =\n tools.some(\n (t) =>\n (match.toolName === undefined || t?.function?.name === match.toolName) &&\n (toolNamespace === undefined || t?.namespace === toolNamespace),\n ) ||\n customTools.some(\n (t) =>\n (match.toolName === undefined || t?.name === match.toolName) &&\n (toolNamespace === undefined || t?.namespace === toolNamespace),\n );\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n // `undefined` means the request carried no embedding input (a non-embedding\n // request) — skip. An explicit empty string (`\"\"`) is a genuinely-empty\n // embedding input and must be allowed through so `inputText: \"\"` can match.\n if (embeddingInput === undefined) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n if (!regexTest(match.inputText, embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact match or prefix + dash-digit boundary for strings (so that\n // \"claude-opus-4\" matches \"claude-opus-4-20250514\" but \"gpt-4\" does NOT\n // match \"gpt-4o\" and \"gpt-4o\" does NOT match \"gpt-4o-mini\"), regexp unchanged\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) {\n if (!effective.model?.startsWith(match.model)) continue;\n const rest = effective.model.slice(match.model.length);\n if (!/^-\\d/.test(rest)) continue;\n }\n } else {\n if (!regexTest(match.model, effective.model ?? \"\")) continue;\n }\n }\n\n // hasToolResult — request-SHAPE predicate scoped to the CURRENT turn. Shares\n // the exact predicate with the recorder via {@link currentTurnHasToolResult}\n // so the value the matcher CHECKS can never drift from the value the recorder\n // STAMPED for the same request (see that helper's doc). Must be evaluated\n // with the other shape predicates ABOVE the sequence/turn state gates so that\n // a fixture whose shape never matched is not miscounted as \"skipped by\n // sequence/turn state\".\n if (match.hasToolResult !== undefined) {\n if (currentTurnHasToolResult(effective.messages) !== match.hasToolResult) continue;\n }\n\n // At this point every SHAPE / CONTENT predicate above has passed, so this\n // fixture is a genuine CONTENT match for the request. The sequenceIndex and\n // turnIndex constraints below are POSITION state, not request shape.\n //\n // sequenceIndex remains a hard, stateful gate: it consumes sequenced\n // siblings one call at a time (and an exhausted index intentionally falls\n // through to a later fixture). A fixture that matched the shape but fails\n // ONLY the sequenceIndex gate is a \"candidate skipped by sequence/turn\n // state\", counted separately so callers can disambiguate the strict-mode\n // 503 message.\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) {\n skippedBySequenceOrTurn++;\n continue;\n }\n }\n\n // turnIndex is normally NOT a hard gate (replay). Multi-step agents emit\n // several assistant bubbles per logical turn, so a canonical run's assistant\n // count routinely differs from a fixture's hardcoded turnIndex even when the\n // request content matches exactly. Rejecting a uniquely content-matching\n // fixture on absolute position produced false \"empty assistant response\"\n // misses. Instead we collect every content match and use turnIndex only as a\n // non-fatal DISAMBIGUATOR to choose AMONG several content-matching fixtures\n // (see selectByTurnIndex below). Content that does not match any fixture\n // still matches nothing — only the position gate is relaxed.\n //\n // Under strictTurnIndex (record mode) turnIndex stays a hard, exact gate so\n // an earlier-turn capture can't shadow a longer request; the miss then\n // proxies upstream and records the new turn.\n if (strictTurnIndex && match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) {\n skippedBySequenceOrTurn++;\n continue;\n }\n }\n\n contentMatches.push(fixture);\n }\n\n if (contentMatches.length === 0) {\n return { fixture: null, skippedBySequenceOrTurn };\n }\n\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n const { fixture: selected, byUniquePosition } = selectByTurnIndex(contentMatches, assistantCount);\n\n // Divergence predicate: the served fixture carries a turnIndex that does NOT\n // sit at the current assistant position. Under strict matching this fixture\n // would have been rejected at the gate above, so serving it here is the (rare,\n // off-by-N) relaxed behaviour change PR #276 introduced. Computed from values\n // already in hand — no second matching pass.\n const selectedTurn = selected.match.turnIndex;\n const turnIndexRelaxed = selectedTurn !== undefined && selectedTurn !== assistantCount;\n // `matchedBy` reports \"turnIndex\" ONLY when the selection was genuinely decided\n // by a UNIQUE positional criterion (a single candidate whose turnIndex sits\n // exactly at the current assistant count). A canonical-position fixture that\n // tied with another at-position candidate, or that lost the exact-turn\n // tie-break to an earlier fallback, was decided by REGISTRATION ORDER, not by\n // position — those are \"content\". `selectByTurnIndex` reports which it was.\n const matchedBy: \"content\" | \"turnIndex\" = byUniquePosition ? \"turnIndex\" : \"content\";\n\n if (turnIndexRelaxed && options?.logger) {\n // Throttle: warn at most once per divergent fixture per process. Keyed by\n // the fixture's OBJECT IDENTITY so distinct fixtures whose match serialises\n // identically (predicate/regex collisions) each warn, and entries auto-evict\n // when the fixture is released (see warnedRelaxedFixtures above).\n if (!warnedRelaxedFixtures.has(selected)) {\n warnedRelaxedFixtures.add(selected);\n // Human-readable description for the message only (NOT the throttle key,\n // which is the fixture's object identity). `JSON.stringify(match)` is\n // unfit here: it DROPS `predicate` functions entirely and collapses any\n // RegExp matcher to `{}`, so a predicate/regex fixture's warning read\n // \"served fixture {}\" / \"{\"userMessage\":{}}\". `describeMatch` instead\n // summarizes the present matcher KEYS (annotating predicate/regex values)\n // so the warned fixture is identifiable.\n const idx = fixtures.indexOf(selected);\n const desc = describeMatch(selected.match, idx);\n options.logger.warn(\n `turnIndex relaxed: served fixture ${desc} at assistantCount=${assistantCount} ` +\n `(scripted turnIndex=${selectedTurn}); set AIMOCK_STRICT_TURN_INDEX=1 to restore strict matching`,\n );\n }\n }\n\n return { fixture: selected, skippedBySequenceOrTurn, turnIndexRelaxed, matchedBy };\n}\n\n/**\n * Choose one fixture from a set that all CONTENT-matched the same request,\n * using `turnIndex` purely as a position disambiguator (never as a reject\n * gate).\n *\n * The selection rule is applied UNIFORMLY regardless of candidate count (a\n * single candidate is NOT special-cased), so the same request never flips its\n * answer just because an unrelated content-matching fixture was registered.\n * Within every tier ties are broken by REGISTRATION ORDER — the\n * earliest-registered eligible candidate wins — preserving the historical\n * greedy \"first matching fixture wins\" contract.\n *\n * 1. Prefer the turnIndexed candidate whose `turnIndex` is closest to\n * `assistantCount` WITHOUT exceeding it (the highest `turnIndex <=\n * assistantCount`). A behind-the-count scripted turn (turnIndex <\n * assistantCount) beats a plain fallback — an explicit position is a\n * stronger signal than an unpositioned default. A negative `turnIndex` such\n * as -1 is a valid at/behind position (the seed is `-Infinity`, never a `-1`\n * sentinel that would mis-skip it). Earlier registration breaks ties among\n * equal turnIndexes.\n * 2. EXACT-turn tie-break: when the best at/behind scripted turn sits at the\n * EXACT current position (`turnIndex === assistantCount`) a plain fallback\n * also answers \"right now\", so the two are equally eligible and REGISTRATION\n * ORDER decides — a later-registered `turnIndex:0` does NOT override an\n * earlier-registered fallback, and vice-versa.\n * 3. Otherwise every turnIndexed candidate is still AHEAD of the conversation.\n * An explicit future turn must NOT answer an earlier point, so a plain\n * fallback (eligible at every position) is the better answer — applied\n * uniformly, INCLUDING when the fallback is the sole partner of a single\n * future-turn fixture (the single/multi asymmetry this fixes).\n * 4. Otherwise (pure script, every candidate turnIndexed and all ahead) the\n * script genuinely has no earlier answer, so serve the lowest `turnIndex`\n * candidate — the false-red-kill for a lone scripted turn whose run has\n * FEWER assistant bubbles than its `turnIndex`; registration order breaks\n * ties.\n *\n * A future-turn fixture therefore NEVER answers an earlier-point request when an\n * eligible alternative (a fallback, or an at/behind scripted turn) exists — the\n * future-turn guard is enforced uniformly for single and multiple candidates.\n *\n * Returns the selected fixture alongside `byUniquePosition`: `true` ONLY when the\n * choice was decided by a UNIQUE positional criterion — the served fixture's\n * `turnIndex` sits EXACTLY at `assistantCount`, no earlier fallback overrode it\n * (tier 2), and no other candidate shared that exact position (so registration\n * order did not break a tie). `matchFixtureDiagnostic` maps this to\n * `matchedBy === \"turnIndex\"`; every other selection path (tie-break,\n * registration order, behind/ahead scripted turn, fallback) is `\"content\"`.\n */\nfunction selectByTurnIndex(\n candidates: Fixture[],\n assistantCount: number,\n): { fixture: Fixture; byUniquePosition: boolean } {\n // The first non-turnIndexed candidate is the registration-order-first plain\n // fallback (eligible at every position). Tracked by index so the exact-turn\n // tie-break can compare registration order against the chosen scripted turn.\n const fallbackIdx = candidates.findIndex((f) => f.match.turnIndex === undefined);\n\n // Tier 1: closest scripted turn at/before the current count. Strict `>`\n // preserves registration order on equal turnIndexes; `-Infinity` seed so a\n // negative turnIndex is a legitimate at/behind candidate, not a sentinel skip.\n let bestIdx = -1;\n let bestTurn = -Infinity;\n for (let i = 0; i < candidates.length; i++) {\n const t = candidates[i].match.turnIndex;\n if (t === undefined) continue;\n if (t <= assistantCount && t > bestTurn) {\n bestIdx = i;\n bestTurn = t;\n }\n }\n\n if (bestIdx !== -1) {\n // Tier 2: exact-turn tie with a fallback → earlier registration wins. A\n // fallback won the tie, so position did NOT uniquely decide → content.\n if (bestTurn === assistantCount && fallbackIdx !== -1 && fallbackIdx < bestIdx) {\n return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n }\n // A UNIQUE positional decision requires the chosen turn to sit EXACTLY at\n // the current count AND to be the only candidate at that exact position —\n // otherwise registration order, not position, broke the tie.\n const atExactPosition =\n bestTurn === assistantCount &&\n candidates.filter((f) => f.match.turnIndex === assistantCount).length === 1;\n return { fixture: candidates[bestIdx], byUniquePosition: atExactPosition };\n }\n\n // Tier 3: every scripted turn is ahead. A plain fallback answers this earlier\n // point; first-registered fallback wins.\n if (fallbackIdx !== -1) return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n\n // Tier 4: pure script, all turnIndexed and all ahead. Serve the lowest\n // scripted turn; registration order breaks ties (first of the lowest wins).\n let lowest = candidates[0];\n for (const f of candidates) {\n if ((f.match.turnIndex as number) < (lowest.match.turnIndex as number)) lowest = f;\n }\n return { fixture: lowest, byUniquePosition: false };\n}\n\n/**\n * Match a fixture against a request, returning the fixture or `null`. Thin\n * wrapper over {@link matchFixtureDiagnostic} that discards the skip diagnostic\n * — preserves the historical signature for all existing callers.\n */\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n options?: MatchOptions,\n): Fixture | null {\n return matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, options).fixture;\n}\n\n/** Select for serving and reserve an injected error before the caller can await.\n * Diagnostic-only matching stays non-consuming. A competing selection retries\n * against the live queue; cancellation must release the reserved fixture.\n */\nexport function selectFixtureForServing(\n ...args: Parameters<typeof matchFixtureDiagnostic>\n): MatchFixtureDiagnostic {\n for (;;) {\n const attempt = matchFixtureDiagnostic(...args);\n if (\n attempt.fixture &&\n isOneShotError(attempt.fixture) &&\n !claimOneShotError(args[0], attempt.fixture)\n )\n continue;\n return attempt;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router.cjs","names":["isExtendedResponsesToolsList","getEmbeddingRequest","isLiveResponse","isRawResponse","isJSONResponse","isImageResponse","isAudioResponse","isErrorResponse","isTranscriptionResponse","isVideoResponse","describeMatch","isOneShotError","claimOneShotError"],"sources":["../src/router.ts"],"sourcesContent":["import { claimOneShotError, isOneShotError } from \"./fixture-loader.js\";\nimport { isDeepStrictEqual } from \"node:util\";\nimport { getEmbeddingRequest } from \"./embedding-request.js\";\nimport type {\n ChatCompletionRequest,\n ChatMessage,\n ContentPart,\n CustomToolDefinition,\n Fixture,\n ToolDefinition,\n RawWireId,\n WireId,\n} from \"./types.js\";\nimport {\n describeMatch,\n isLiveResponse,\n isImageResponse,\n isAudioResponse,\n isTranscriptionResponse,\n isVideoResponse,\n isJSONResponse,\n isRawResponse,\n isErrorResponse,\n isExtendedResponsesToolsList,\n markExtendedResponsesToolsRequest,\n} from \"./helpers.js\";\n\nexport function getLastMessageByRole(messages: ChatMessage[], role: string): ChatMessage | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === role) return messages[i];\n }\n return null;\n}\n\n/**\n * Every tool an OpenAI Responses request offered (top level, inside\n * `namespace` tools, and from `additional_tools` / `tool_search_output`\n * items), keyed by the converted request. Kept off the request object so the\n * request that predicates and the journal see is unchanged by default.\n */\nconst responsesOfferedTools = new WeakMap<\n object,\n { tools: ToolDefinition[]; customTools: CustomToolDefinition[] }\n>();\n\n/** Record the tools a Responses request offered (see {@link getResponsesOfferedTools}). */\nexport function setResponsesOfferedTools(\n request: ChatCompletionRequest,\n offered: { tools: ToolDefinition[]; customTools: CustomToolDefinition[] },\n): void {\n responsesOfferedTools.set(request, offered);\n}\n\n/** The tools a Responses request offered, for `toolNamespace` matching. */\nexport function getResponsesOfferedTools(\n request: ChatCompletionRequest,\n): { tools: ToolDefinition[]; customTools: CustomToolDefinition[] } | undefined {\n return responsesOfferedTools.get(request);\n}\n\n/**\n * `hasToolResult` request-SHAPE predicate — does the CURRENT turn contain a\n * tool result message? \"Current turn\" = the messages AFTER the last `user`\n * message, so this is scoped to the turn being matched rather than the whole\n * conversation. This is what keeps leg-1 (tool call, `hasToolResult` false) vs\n * leg-2 (narration, `hasToolResult` true) fixtures working across MULTI-TURN\n * sessions: on the 2nd+ user turn the request still carries earlier turns' tool\n * results, and a whole-conversation check would force `hasToolResult=true`\n * forever, so the turn's leg-1 fixture (false) could never match again (\"No\n * fixture matched\" on every pill after the first).\n *\n * This is the SINGLE source of truth shared by the RECORD side\n * ({@link buildFixtureMatch} in recorder.ts, which STAMPS the value onto a new\n * fixture) and the MATCH side ({@link matchFixtureDiagnostic} below, which\n * CHECKS a fixture's stored value against the incoming request). The two MUST\n * agree byte-for-byte: if the recorder stamps a value the matcher would never\n * compute for the same request, a genuinely-recorded fixture can never match\n * its own request. Keeping the predicate in one function is what guarantees\n * they cannot drift.\n *\n * INVARIANT: callers MUST pass the POST-`requestTransform` (normalized)\n * messages — the same array `buildFixtureMatch` and `matchFixtureDiagnostic`\n * both feed here. Passing raw/untransformed messages on one side would let the\n * set-side and check-side diverge again and silently reopen the multi-turn bug.\n *\n * When there is no `user` message at all, `lastUserIdx` stays -1 and the scan\n * covers the whole conversation — the safe/consistent fallback for a user-less\n * request (both sides degrade identically). For a single-turn request whose\n * only tool message trails a lone user message this is identical to the old\n * whole-conversation check.\n */\nexport function currentTurnHasToolResult(messages: ChatMessage[]): boolean {\n let lastUserIdx = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n lastUserIdx = i;\n break;\n }\n }\n return messages.slice(lastUserIdx + 1).some((m) => m.role === \"tool\");\n}\n\n/**\n * Concatenate the text content of every `system` role message in order.\n * Hosts that build a system context from multiple sources (persona, agent\n * context entries, tool guidance) often emit several system messages in one\n * request; this joins SEPARATE system messages with newlines so a substring\n * matcher sees the whole context as one body.\n *\n * Empty handling is symmetric with {@link getTextContent}: a system message\n * with no extractable text (`null`) contributes nothing, while a message that\n * extracts to an empty string is a present-but-empty body. We skip only the\n * `null` (no-text) case so a genuinely empty system message does not inject a\n * stray newline; this matches getTextContent treating \"no text\" and \"empty\n * text\" consistently.\n */\nexport function getSystemText(messages: ChatMessage[]): string {\n const parts: string[] = [];\n for (const m of messages) {\n if (m.role !== \"system\") continue;\n const text = getTextContent(m.content);\n if (text === null) continue;\n parts.push(text);\n }\n return parts.join(\"\\n\");\n}\n\n/**\n * Extract the text content from a message's content field.\n * Handles both plain string content and array-of-parts content\n * (e.g. `[{type: \"text\", text: \"...\"}]` as sent by some SDKs).\n *\n * Multi-part text is joined with `\"\"` (the parts form one logical body split\n * across segments). Empty handling is symmetric with the string path: a string\n * `\"\"` returns `\"\"`, and an array containing at least one text part whose\n * combined text is empty likewise returns `\"\"` (NOT `null`). `null` is reserved\n * for \"no text content at all\" — null content, or an array with no text parts —\n * so callers can distinguish \"absent\" from \"present but empty\" the same way for\n * both content shapes.\n */\nexport function getTextContent(content: string | ContentPart[] | null): string | null {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const texts = content\n .filter((p) => p.type === \"text\" && typeof p.text === \"string\")\n .map((p) => p.text as string);\n return texts.length > 0 ? texts.join(\"\") : null;\n }\n return null;\n}\n\n/**\n * Text of the last user message, for `userMessage` fixture matching. Normally\n * this is the text of the final `user` message. But some SDKs serialise a\n * single multimodal user turn (prompt text + attachment) into TWO consecutive\n * user messages — a text-only one FOLLOWED by an attachment-only one, e.g.\n * `[{role:\"user\", content:\"describe this\"}, {role:\"user\", content:[{type:\"image_url\",...}]}]`\n * (observed with Microsoft Agent Framework's `agent_framework_openai` image\n * path). The trailing attachment-only message has NO extractable text, so the\n * naive \"last user message\" lookup returns `null` and — because no fixture can\n * key on empty text — every such multimodal fixture misses. We therefore skip\n * trailing user messages that carry no text and use the nearest preceding user\n * message that does. This is deliberately narrow: it only skips text-LESS\n * (`null`) user messages — a message whose text extracts to an explicit empty\n * string `\"\"` is a present-but-empty body and IS returned (so a fixture can key\n * on it), while a genuine multi-message user turn (each message HAS text) is\n * unaffected and still matched on its final message. Returns the nearest\n * preceding user message whose text is non-null (including `\"\"`); returns `null`\n * only when no user message carries any text part at all.\n */\nexport function getLastUserText(messages: ChatMessage[]): string | null {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role !== \"user\") continue;\n const text = getTextContent(messages[i].content);\n if (text !== null) return text;\n }\n return null;\n}\n\n/**\n * Test a matcher RegExp against a string WITHOUT mutating the caller-supplied\n * regex. `RegExp.prototype.test` advances `lastIndex` as a side effect on\n * `/g` and `/y` regexes; fixtures hold caller-owned RegExp objects, so testing\n * them in place would clobber the caller's positional state (and a `/g` regex\n * reused across match calls would match intermittently). We test a fresh clone\n * (same source + flags) so the caller's object is never touched and every test\n * starts from index 0.\n */\nfunction regexTest(re: RegExp, text: string): boolean {\n return new RegExp(re.source, re.flags).test(text);\n}\n\n/**\n * Result of {@link matchFixtureDiagnostic}: the matched fixture (or `null`) plus\n * the number of fixtures that matched the request SHAPE (every predicate above\n * the sequenceIndex/turnIndex gates) but were rejected ONLY by the\n * sequenceIndex/turnIndex count state.\n *\n * `skippedBySequenceOrTurn > 0` with `fixture === null` distinguishes a\n * \"sequence/turn exhausted\" miss (candidate fixtures existed but their count\n * gate had moved on) from a true \"no fixture had a matching shape\" miss — used\n * to disambiguate the strict-mode 503 message.\n */\nexport interface MatchFixtureDiagnostic {\n fixture: Fixture | null;\n skippedBySequenceOrTurn: number;\n /**\n * `true` when the served fixture was selected by relaxed content-anchored\n * matching even though its `turnIndex` is defined and does NOT equal the\n * request's assistant-message count — i.e. the legacy strict turnIndex gate\n * (now opt-in via `AIMOCK_STRICT_TURN_INDEX`) WOULD HAVE rejected it. Absent /\n * falsy on canonical-position matches, non-turnIndexed matches, and misses.\n * Additive optional field — existing handler destructures are unaffected.\n */\n turnIndexRelaxed?: boolean;\n /**\n * How the served fixture was selected: `\"turnIndex\"` when its `turnIndex`\n * sits exactly at the current assistant count (canonical position),\n * `\"content\"` otherwise (a non-turnIndexed match or a relaxed off-by-N\n * match). Absent on misses. Additive optional field.\n */\n matchedBy?: \"content\" | \"turnIndex\";\n}\n\n/**\n * Optional matcher tuning.\n *\n * `strictTurnIndex` restores the legacy behaviour where `turnIndex` must equal\n * the request's assistant-message count exactly (a hard reject gate). It is set\n * by the record path, where a miss proxies upstream to capture a fresh turn; an\n * earlier-turn fixture must not shadow a longer request or the new turn would\n * never be recorded. Replay (the default, `false`) treats `turnIndex` as a\n * non-fatal disambiguator instead — see {@link selectByTurnIndex}.\n */\nexport interface MatchOptions {\n strictTurnIndex?: boolean;\n /**\n * Optional sink for the one-shot relaxed-turnIndex divergence warning. Handlers\n * pass their `defaults.logger`; the structural `{ warn }` shape avoids an\n * import cycle with logger.ts and keeps the matcher decoupled. When omitted no\n * warning is emitted (the diagnostic fields are still populated). The Logger's\n * own level gate keeps a passing programmatic run (silent default) quiet.\n */\n logger?: { warn(...args: unknown[]): void };\n /**\n * @internal The wire of the calling LLM handler. Set by handlers, never\n * read from the request body. A raw fixture (see `isRawResponse`) matches\n * only when its `raw.wire` equals this; with no wire, raw never matches.\n */\n wire?: WireId | RawWireId;\n}\n\n/**\n * Process-level opt-out: when `AIMOCK_STRICT_TURN_INDEX=1` (or `true`) is set,\n * REPLAY selection restores the legacy hard turnIndex gate — a content-matching\n * fixture whose `turnIndex` is defined and `!== assistantCount` is rejected,\n * reproducing origin/main semantics. Follows the `AIMOCK_ALLOW_PRIVATE_URLS`\n * precedent for parsing/precedence. Read per-call (not cached) so tests can flip\n * it. Does NOT affect the record path, which is already strict regardless.\n */\nfunction strictTurnIndexEnv(): boolean {\n const v = process.env.AIMOCK_STRICT_TURN_INDEX;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Process-level set of fixtures for which the relaxed-turnIndex divergence\n * warning has already fired, so each divergent fixture warns at most ONCE per\n * process (throttle). Keyed by the selected fixture's OBJECT IDENTITY: the\n * `Fixture` references in the server's fixtures array are stable across replays\n * (the array is held by reference and only fully replaced on a fixtures reset),\n * so identity uniquely distinguishes divergent fixtures and warns each exactly\n * once. A `WeakSet` was chosen over the previous `JSON.stringify(match)` key for\n * two reasons: (1) stringifying the match DROPS `predicate` functions and\n * serialises any RegExp matcher to `{}`, so two distinct fixtures differing only\n * by a predicate/regex collided to one key and the second's warning was silently\n * suppressed; and (2) a string `Set` only grows, accumulating an entry per\n * divergent shape on a long-lived server, whereas a `WeakSet` auto-evicts when a\n * fixture object is released (e.g. after a fixtures reset drops the references).\n * `let` because `WeakSet` has no `.clear()`, so the test hook reassigns a fresh\n * one.\n */\nlet warnedRelaxedFixtures = new WeakSet<Fixture>();\n\n/**\n * Test-only hook to clear the throttle state between cases. Not part of the\n * public contract. `WeakSet` has no `.clear()`, so reassign a fresh instance.\n */\nexport function _resetTurnIndexRelaxWarnings(): void {\n warnedRelaxedFixtures = new WeakSet<Fixture>();\n}\n\n/**\n * Build the {@link MatchOptions} a request handler must pass to\n * {@link matchFixtureDiagnostic} / {@link matchFixture}, derived from whether\n * the handler is about to record on a miss.\n *\n * EVERY record-capable handler (OpenAI chat, Anthropic messages, Responses,\n * Gemini, Bedrock, Bedrock-Converse, Cohere, Ollama, …) must build its match\n * options through THIS helper rather than hand-rolling `{ strictTurnIndex }` at\n * the call site. Recording proxies upstream on a miss to capture a fresh turn;\n * if `strictTurnIndex` is left false during recording, an earlier-turn fixture\n * can content-shadow a longer request, the `if (!fixture)` record branch never\n * fires, and the new turn is SILENTLY never recorded. Funnelling the decision\n * through one helper makes that wiring impossible for a future handler to miss:\n * pass `recording = true` whenever the handler's own record gate is satisfied\n * (i.e. it will call `proxyAndRecord` on a miss), `false` otherwise.\n */\nexport function recordMatchOptions(\n recording: boolean,\n logger?: { warn(...args: unknown[]): void },\n): MatchOptions {\n return { strictTurnIndex: recording, logger };\n}\n\n/**\n * Match a fixture against a request, additionally reporting WHY a `null` result\n * occurred. Shares the exact predicate loop with {@link matchFixture}; a fixture\n * that passes every shape predicate but fails ONLY the sequenceIndex or\n * turnIndex gate increments `skippedBySequenceOrTurn`. {@link matchFixture} is a\n * thin wrapper that returns the `.fixture` field, so existing callers are\n * unaffected.\n */\nexport function matchFixtureDiagnostic(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n options?: MatchOptions,\n): MatchFixtureDiagnostic {\n // Apply transform once before matching — used for stripping dynamic data\n const effective = requestTransform ? requestTransform(req) : req;\n const useExactMatch = !!requestTransform;\n // In record mode the server proxies to the upstream on a miss, so a fixture\n // already captured for an EARLIER turn must NOT shadow a longer (later-turn)\n // request — otherwise the new turn would never be proxied and recorded.\n // There turnIndex stays a strict hard gate. Replay (the default) instead\n // treats turnIndex as a non-fatal disambiguator so a canonical multi-bubble\n // run isn't falsely rejected for an off-by-N assistant count.\n // Strict turnIndex is in force when the record path requests it OR the\n // process-level AIMOCK_STRICT_TURN_INDEX opt-out is set (which restores the\n // legacy hard gate for replay too). Record mode passes `true` explicitly; the\n // env only matters when the caller left it `false`/unset (replay).\n const strictTurnIndex = (options?.strictTurnIndex ?? false) || strictTurnIndexEnv();\n // `match.toolNamespace` (and, in resolveResponse, `customToolCalls` /\n // `responsesBlocks`) apply only with responsesTools \"extended\". Earlier\n // releases ignored these keys, so the default ignores them too.\n const extendedTools = isExtendedResponsesToolsList(fixtures);\n // An extended server's list gives each fixture with its held keys applied\n // (createServerFixtureList), so the fixtures are matched as they are read.\n if (extendedTools) markExtendedResponsesToolsRequest(req);\n\n let skippedBySequenceOrTurn = 0;\n // Every fixture whose content / shape predicates (and sequenceIndex gate)\n // pass. turnIndex is applied afterwards as a non-fatal disambiguator.\n const contentMatches: Fixture[] = [];\n // Raw fixtures whose content matched but whose `raw.wire` is not this\n // request's wire, kept only for the miss warning below.\n const rawWireMisses: { fixture: Fixture; wire: unknown }[] = [];\n\n for (const fixture of fixtures) {\n const { match } = fixture;\n\n // Recorded embeddings retain the operation and exact wire inputs so a\n // changed batch or width cannot reuse another operation's response.\n if (\n match.embeddingRequest !== undefined &&\n !isDeepStrictEqual(match.embeddingRequest, getEmbeddingRequest(req))\n ) {\n continue;\n }\n\n // predicate — if present, must return true (receives original request)\n if (match.predicate !== undefined) {\n if (!match.predicate(req)) continue;\n }\n\n // endpoint — bidirectional filtering:\n // 1. If fixture has endpoint set, only match requests of that type\n // 2. If request has _endpointType but fixture doesn't, skip fixtures\n // whose response type is incompatible (prevents generic chat fixtures\n // from matching image/speech/video requests and causing 500s)\n const reqEndpoint = effective._endpointType as string | undefined;\n const response = fixture.response;\n const liveResponse = isLiveResponse(response);\n if (reqEndpoint === \"openai-live\") {\n if (\n match.endpoint !== \"openai-live\" ||\n effective.model !== \"gpt-live-1\" ||\n (typeof response !== \"function\" &&\n (!liveResponse || effective.model !== response.live.model))\n )\n continue;\n } else if (liveResponse) {\n continue;\n }\n // A raw fixture is wire-specific: never offer it to another wire's handler.\n // The wire comes from the calling handler via MatchOptions, never from the\n // request body: body fields are journaled, and a `requestTransform` copy\n // may drop them. No wire given (direct matchFixture callers, non-LLM\n // handlers) means raw never matches. Factories are checked at serve time.\n // The skip is deferred until every content predicate has run, so a fixture\n // that failed ONLY on its wire can be named in a miss warning below.\n const rawWire =\n typeof response !== \"function\" && isRawResponse(response)\n ? { wire: response.raw.wire }\n : undefined;\n if (match.endpoint !== undefined) {\n if (match.endpoint !== reqEndpoint) continue;\n // A declared endpoint used to END the shape discussion — the router\n // matched on the name and never looked at the response. That was safe\n // only while one endpoint name meant one response shape. `video` no\n // longer does: it is a SHARED namespace holding both VideoResponse\n // fixtures (Sora / Grok / Veo / OpenRouter) and RawJSONResponse Ark task\n // envelopes (BytePlus), and EVERY BytePlus fixture is required to declare\n // `endpoint: \"video\"`. One such fixture therefore became a first-class\n // candidate for all four other handlers, each of which then failed its\n // own isVideoResponse guard and returned 500 — a single BytePlus fixture\n // broke every other video provider in the same suite.\n //\n // Deliberately narrow: it filters ONLY the raw-JSON half of the shared\n // video namespace out of the non-BytePlus handlers. Every other\n // wrong-shape fixture still reaches its handler, which names the problem\n // itself (\"Fixture response is not a video type\", BytePlus's \"NOT a\n // VideoResponse\" warn) — a clearer diagnostic than a silent no-match.\n // `_videoProvider` is a routing fact of the INBOUND request, so it is\n // read off `req`, not off the post-`requestTransform` copy.\n if (\n reqEndpoint === \"video\" &&\n req._videoProvider !== \"byteplus\" &&\n typeof fixture.response !== \"function\" &&\n isJSONResponse(fixture.response)\n ) {\n continue;\n }\n } else if (\n reqEndpoint &&\n reqEndpoint !== \"chat\" &&\n reqEndpoint !== \"embedding\" &&\n !reqEndpoint.startsWith(\"realtime\")\n ) {\n // Fixture has no endpoint restriction but request is multimedia —\n // only match if the response type is compatible.\n // Function responses cannot be checked statically, so treat them as compatible.\n const r = fixture.response;\n if (typeof r !== \"function\") {\n const compatible =\n (reqEndpoint === \"image\" && isImageResponse(r)) ||\n (reqEndpoint === \"speech\" && isAudioResponse(r)) ||\n (reqEndpoint === \"elevenlabs-tts\" && isAudioResponse(r)) ||\n (reqEndpoint === \"audio-gen\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal-audio\" && isAudioResponse(r)) ||\n (reqEndpoint === \"fal\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-design\" &&\n (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-get\" && (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"elevenlabs-voice-delete\" &&\n (isJSONResponse(r) || isErrorResponse(r))) ||\n (reqEndpoint === \"transcription\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"translation\" && isTranscriptionResponse(r)) ||\n (reqEndpoint === \"video\" && isVideoResponse(r));\n if (!compatible) continue;\n }\n }\n\n // context — opt-in exact match against the request's _context field.\n // If fixture specifies a context, only match requests with that exact context.\n // If fixture omits context, match any request regardless of _context.\n if (match.context !== undefined) {\n if (effective._context !== match.context) continue;\n }\n\n // userMessage — case-sensitive match against the last user message content.\n // String matching is intentionally case-sensitive so fixture authors can\n // rely on exact string values. This differs from the case-insensitive\n // matchesPattern() in helpers.ts, which is used for search/rerank/moderation\n // where exact casing rarely matters.\n if (match.userMessage !== undefined) {\n // Use the last user message that actually carries text — see\n // getLastUserText for why a trailing attachment-only user message\n // (multimodal serialisation split) must not shadow the real prompt.\n const text = getLastUserText(effective.messages);\n // `text === null` means no user message carried any text (e.g. a pure\n // attachment turn) — skip. An explicit empty-string body (`\"\"`) is a\n // present-but-empty user message and must be allowed through so a fixture\n // keyed on empty text can match it (see getLastUserText).\n if (text === null) continue;\n if (typeof match.userMessage === \"string\") {\n if (useExactMatch) {\n if (text !== match.userMessage) continue;\n } else {\n if (!text.includes(match.userMessage)) continue;\n }\n } else {\n if (!regexTest(match.userMessage, text)) continue;\n }\n }\n\n // systemMessage — case-sensitive substring, regexp, or array-of-substrings\n // match against the joined text of every system message in the request.\n // Use to gate a fixture on host-supplied context (e.g. agent-context\n // entries) so that when the calling app changes that context the fixture\n // stops matching and the request falls through to the next fixture or\n // upstream proxy.\n //\n // Array form (string[]) requires ALL substrings to be present — useful\n // when the gate must combine multiple non-adjacent tokens (e.g. a default\n // name AND a default activity list whose positions in the serialised\n // context JSON aren't stable).\n if (match.systemMessage !== undefined) {\n const sm = match.systemMessage;\n // Empty array is treated as \"no constraint\" → matches unconditionally,\n // INCLUDING requests with no system text at all. This is the documented\n // contract (same permissive behaviour as not setting systemMessage), so\n // it must be honored BEFORE the no-system-text guard below — otherwise a\n // request without a system message would be wrongly skipped. Validation\n // rejects [] at load time for JSON fixtures; programmatic callers that\n // pass [] get this permissive behaviour.\n if (Array.isArray(sm) && sm.length === 0) {\n // no constraint — fall through to the next predicate\n } else {\n const text = getSystemText(effective.messages);\n // Deliberately `!text` (not `text === null`): unlike userMessage/inputText,\n // getSystemText returns `\"\"` for BOTH \"a present but empty system message\"\n // AND \"no system message at all\", so it exposes no absent-vs-empty\n // distinction at the request level. Allowing `\"\"` through would make a\n // `systemMessage: \"\"` / `/^$/` fixture a catch-all firing on every\n // no-system-message request. There is no shipped-fixture demand for\n // matching an empty system prompt, so we keep the falsy guard here. If a\n // real need arises, add a getSystemText sibling that returns `null` when\n // absent and `\"\"` when present-empty, then switch to `text === null`.\n if (!text) continue;\n if (Array.isArray(sm)) {\n let allPresent = true;\n for (const needle of sm) {\n if (!text.includes(needle)) {\n allPresent = false;\n break;\n }\n }\n if (!allPresent) continue;\n } else if (typeof sm === \"string\") {\n if (useExactMatch) {\n if (text !== sm) continue;\n } else {\n if (!text.includes(sm)) continue;\n }\n } else {\n if (!regexTest(sm, text)) continue;\n }\n }\n }\n\n // toolCallId — a toolCallId fixture answers the model's response to a tool\n // result, which by API contract only happens when the conversation's LAST\n // message is a tool result. If a newer user (or other) turn follows the\n // tool message, the stale tool_call_id must not shadow userMessage matchers.\n if (match.toolCallId !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\" || last.tool_call_id !== match.toolCallId) continue;\n }\n\n // toolResultContains — substring gate on the LAST message's text content\n // when that message is a tool result. Same last-message rule as toolCallId\n // (a tool-result fixture answers the model's next call after a tool round),\n // but discriminates on the result PAYLOAD instead of the call id — needed\n // when approve/cancel resumes share the same toolCallId and differ only\n // inside the tool-result JSON.\n if (match.toolResultContains !== undefined) {\n const last = effective.messages[effective.messages.length - 1];\n if (!last || last.role !== \"tool\") continue;\n const text = getTextContent(last.content);\n if (text === null || !text.includes(match.toolResultContains)) continue;\n }\n\n // toolName — match against any tool definition by function.name (and,\n // with responsesTools \"extended\", any Responses custom tool by name).\n // toolNamespace — exact OpenAI Responses namespace of an offered tool; with\n // toolName, ONE tool must carry both (Codex routes by the exact pair). Only\n // with responsesTools \"extended\"; there a toolNamespace fixture sees every\n // tool the Responses request offered. The default ignores the key.\n const toolNamespace = extendedTools ? match.toolNamespace : undefined;\n if (match.toolName !== undefined || toolNamespace !== undefined) {\n const offered =\n toolNamespace !== undefined\n ? (getResponsesOfferedTools(effective) ?? getResponsesOfferedTools(req))\n : undefined;\n const tools = offered?.tools ?? (Array.isArray(effective.tools) ? effective.tools : []);\n const customTools =\n offered?.customTools ?? (Array.isArray(effective.customTools) ? effective.customTools : []);\n const found =\n tools.some(\n (t) =>\n (match.toolName === undefined || t?.function?.name === match.toolName) &&\n (toolNamespace === undefined || t?.namespace === toolNamespace),\n ) ||\n customTools.some(\n (t) =>\n (match.toolName === undefined || t?.name === match.toolName) &&\n (toolNamespace === undefined || t?.namespace === toolNamespace),\n );\n if (!found) continue;\n }\n\n // inputText — case-sensitive match against the embedding input text.\n // Same rationale as userMessage above: fixture authors specify exact strings.\n if (match.inputText !== undefined) {\n const embeddingInput = effective.embeddingInput;\n // `undefined` means the request carried no embedding input (a non-embedding\n // request) — skip. An explicit empty string (`\"\"`) is a genuinely-empty\n // embedding input and must be allowed through so `inputText: \"\"` can match.\n if (embeddingInput === undefined) continue;\n if (typeof match.inputText === \"string\") {\n if (useExactMatch) {\n if (embeddingInput !== match.inputText) continue;\n } else {\n if (!embeddingInput.includes(match.inputText)) continue;\n }\n } else {\n if (!regexTest(match.inputText, embeddingInput)) continue;\n }\n }\n\n // responseFormat — exact string match against request response_format.type\n if (match.responseFormat !== undefined) {\n const reqType = effective.response_format?.type;\n if (reqType !== match.responseFormat) continue;\n }\n\n // model — exact match or prefix + dash-digit boundary for strings (so that\n // \"claude-opus-4\" matches \"claude-opus-4-20250514\" but \"gpt-4\" does NOT\n // match \"gpt-4o\" and \"gpt-4o\" does NOT match \"gpt-4o-mini\"), regexp unchanged\n if (match.model !== undefined) {\n if (typeof match.model === \"string\") {\n if (effective.model !== match.model) {\n if (!effective.model?.startsWith(match.model)) continue;\n const rest = effective.model.slice(match.model.length);\n if (!/^-\\d/.test(rest)) continue;\n }\n } else {\n if (!regexTest(match.model, effective.model ?? \"\")) continue;\n }\n }\n\n // hasToolResult — request-SHAPE predicate scoped to the CURRENT turn. Shares\n // the exact predicate with the recorder via {@link currentTurnHasToolResult}\n // so the value the matcher CHECKS can never drift from the value the recorder\n // STAMPED for the same request (see that helper's doc). Must be evaluated\n // with the other shape predicates ABOVE the sequence/turn state gates so that\n // a fixture whose shape never matched is not miscounted as \"skipped by\n // sequence/turn state\".\n if (match.hasToolResult !== undefined) {\n if (currentTurnHasToolResult(effective.messages) !== match.hasToolResult) continue;\n }\n\n // A raw fixture for another wire is skipped here, after its content\n // matched and before the sequence/turn gates (it is not a count skip).\n if (rawWire !== undefined && rawWire.wire !== options?.wire) {\n rawWireMisses.push({ fixture, wire: rawWire.wire });\n continue;\n }\n\n // At this point every SHAPE / CONTENT predicate above has passed, so this\n // fixture is a genuine CONTENT match for the request. The sequenceIndex and\n // turnIndex constraints below are POSITION state, not request shape.\n //\n // sequenceIndex remains a hard, stateful gate: it consumes sequenced\n // siblings one call at a time (and an exhausted index intentionally falls\n // through to a later fixture). A fixture that matched the shape but fails\n // ONLY the sequenceIndex gate is a \"candidate skipped by sequence/turn\n // state\", counted separately so callers can disambiguate the strict-mode\n // 503 message.\n if (match.sequenceIndex !== undefined && matchCounts !== undefined) {\n const count = matchCounts.get(fixture) ?? 0;\n if (count !== match.sequenceIndex) {\n skippedBySequenceOrTurn++;\n continue;\n }\n }\n\n // turnIndex is normally NOT a hard gate (replay). Multi-step agents emit\n // several assistant bubbles per logical turn, so a canonical run's assistant\n // count routinely differs from a fixture's hardcoded turnIndex even when the\n // request content matches exactly. Rejecting a uniquely content-matching\n // fixture on absolute position produced false \"empty assistant response\"\n // misses. Instead we collect every content match and use turnIndex only as a\n // non-fatal DISAMBIGUATOR to choose AMONG several content-matching fixtures\n // (see selectByTurnIndex below). Content that does not match any fixture\n // still matches nothing — only the position gate is relaxed.\n //\n // Under strictTurnIndex (record mode) turnIndex stays a hard, exact gate so\n // an earlier-turn capture can't shadow a longer request; the miss then\n // proxies upstream and records the new turn.\n if (strictTurnIndex && match.turnIndex !== undefined) {\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n if (assistantCount !== match.turnIndex) {\n skippedBySequenceOrTurn++;\n continue;\n }\n }\n\n contentMatches.push(fixture);\n }\n\n if (contentMatches.length === 0) {\n // Near-miss: a raw fixture matched this request in every way except its\n // `raw.wire`. Name both wires so a wrong or missing wire is not a silent 404.\n if (options?.logger) {\n for (const miss of rawWireMisses) {\n const fixtureWire =\n miss.wire === undefined ? \"raw.wire is missing\" : `raw.wire=${JSON.stringify(miss.wire)}`;\n const requestWire =\n options.wire === undefined ? \"request wire is not set\" : `request wire \"${options.wire}\"`;\n options.logger.warn(\n `raw fixture not served: ${describeMatch(miss.fixture.match, fixtures.indexOf(miss.fixture))} ` +\n `matched the request except its wire (${fixtureWire}, ${requestWire})`,\n );\n }\n }\n return { fixture: null, skippedBySequenceOrTurn };\n }\n\n const assistantCount = effective.messages.filter((m) => m.role === \"assistant\").length;\n const { fixture: selected, byUniquePosition } = selectByTurnIndex(contentMatches, assistantCount);\n\n // Divergence predicate: the served fixture carries a turnIndex that does NOT\n // sit at the current assistant position. Under strict matching this fixture\n // would have been rejected at the gate above, so serving it here is the (rare,\n // off-by-N) relaxed behaviour change PR #276 introduced. Computed from values\n // already in hand — no second matching pass.\n const selectedTurn = selected.match.turnIndex;\n const turnIndexRelaxed = selectedTurn !== undefined && selectedTurn !== assistantCount;\n // `matchedBy` reports \"turnIndex\" ONLY when the selection was genuinely decided\n // by a UNIQUE positional criterion (a single candidate whose turnIndex sits\n // exactly at the current assistant count). A canonical-position fixture that\n // tied with another at-position candidate, or that lost the exact-turn\n // tie-break to an earlier fallback, was decided by REGISTRATION ORDER, not by\n // position — those are \"content\". `selectByTurnIndex` reports which it was.\n const matchedBy: \"content\" | \"turnIndex\" = byUniquePosition ? \"turnIndex\" : \"content\";\n\n if (turnIndexRelaxed && options?.logger) {\n // Throttle: warn at most once per divergent fixture per process. Keyed by\n // the fixture's OBJECT IDENTITY so distinct fixtures whose match serialises\n // identically (predicate/regex collisions) each warn, and entries auto-evict\n // when the fixture is released (see warnedRelaxedFixtures above).\n if (!warnedRelaxedFixtures.has(selected)) {\n warnedRelaxedFixtures.add(selected);\n // Human-readable description for the message only (NOT the throttle key,\n // which is the fixture's object identity). `JSON.stringify(match)` is\n // unfit here: it DROPS `predicate` functions entirely and collapses any\n // RegExp matcher to `{}`, so a predicate/regex fixture's warning read\n // \"served fixture {}\" / \"{\"userMessage\":{}}\". `describeMatch` instead\n // summarizes the present matcher KEYS (annotating predicate/regex values)\n // so the warned fixture is identifiable.\n const idx = fixtures.indexOf(selected);\n const desc = describeMatch(selected.match, idx);\n options.logger.warn(\n `turnIndex relaxed: served fixture ${desc} at assistantCount=${assistantCount} ` +\n `(scripted turnIndex=${selectedTurn}); set AIMOCK_STRICT_TURN_INDEX=1 to restore strict matching`,\n );\n }\n }\n\n return { fixture: selected, skippedBySequenceOrTurn, turnIndexRelaxed, matchedBy };\n}\n\n/**\n * Choose one fixture from a set that all CONTENT-matched the same request,\n * using `turnIndex` purely as a position disambiguator (never as a reject\n * gate).\n *\n * The selection rule is applied UNIFORMLY regardless of candidate count (a\n * single candidate is NOT special-cased), so the same request never flips its\n * answer just because an unrelated content-matching fixture was registered.\n * Within every tier ties are broken by REGISTRATION ORDER — the\n * earliest-registered eligible candidate wins — preserving the historical\n * greedy \"first matching fixture wins\" contract.\n *\n * 1. Prefer the turnIndexed candidate whose `turnIndex` is closest to\n * `assistantCount` WITHOUT exceeding it (the highest `turnIndex <=\n * assistantCount`). A behind-the-count scripted turn (turnIndex <\n * assistantCount) beats a plain fallback — an explicit position is a\n * stronger signal than an unpositioned default. A negative `turnIndex` such\n * as -1 is a valid at/behind position (the seed is `-Infinity`, never a `-1`\n * sentinel that would mis-skip it). Earlier registration breaks ties among\n * equal turnIndexes.\n * 2. EXACT-turn tie-break: when the best at/behind scripted turn sits at the\n * EXACT current position (`turnIndex === assistantCount`) a plain fallback\n * also answers \"right now\", so the two are equally eligible and REGISTRATION\n * ORDER decides — a later-registered `turnIndex:0` does NOT override an\n * earlier-registered fallback, and vice-versa.\n * 3. Otherwise every turnIndexed candidate is still AHEAD of the conversation.\n * An explicit future turn must NOT answer an earlier point, so a plain\n * fallback (eligible at every position) is the better answer — applied\n * uniformly, INCLUDING when the fallback is the sole partner of a single\n * future-turn fixture (the single/multi asymmetry this fixes).\n * 4. Otherwise (pure script, every candidate turnIndexed and all ahead) the\n * script genuinely has no earlier answer, so serve the lowest `turnIndex`\n * candidate — the false-red-kill for a lone scripted turn whose run has\n * FEWER assistant bubbles than its `turnIndex`; registration order breaks\n * ties.\n *\n * A future-turn fixture therefore NEVER answers an earlier-point request when an\n * eligible alternative (a fallback, or an at/behind scripted turn) exists — the\n * future-turn guard is enforced uniformly for single and multiple candidates.\n *\n * Returns the selected fixture alongside `byUniquePosition`: `true` ONLY when the\n * choice was decided by a UNIQUE positional criterion — the served fixture's\n * `turnIndex` sits EXACTLY at `assistantCount`, no earlier fallback overrode it\n * (tier 2), and no other candidate shared that exact position (so registration\n * order did not break a tie). `matchFixtureDiagnostic` maps this to\n * `matchedBy === \"turnIndex\"`; every other selection path (tie-break,\n * registration order, behind/ahead scripted turn, fallback) is `\"content\"`.\n */\nfunction selectByTurnIndex(\n candidates: Fixture[],\n assistantCount: number,\n): { fixture: Fixture; byUniquePosition: boolean } {\n // The first non-turnIndexed candidate is the registration-order-first plain\n // fallback (eligible at every position). Tracked by index so the exact-turn\n // tie-break can compare registration order against the chosen scripted turn.\n const fallbackIdx = candidates.findIndex((f) => f.match.turnIndex === undefined);\n\n // Tier 1: closest scripted turn at/before the current count. Strict `>`\n // preserves registration order on equal turnIndexes; `-Infinity` seed so a\n // negative turnIndex is a legitimate at/behind candidate, not a sentinel skip.\n let bestIdx = -1;\n let bestTurn = -Infinity;\n for (let i = 0; i < candidates.length; i++) {\n const t = candidates[i].match.turnIndex;\n if (t === undefined) continue;\n if (t <= assistantCount && t > bestTurn) {\n bestIdx = i;\n bestTurn = t;\n }\n }\n\n if (bestIdx !== -1) {\n // Tier 2: exact-turn tie with a fallback → earlier registration wins. A\n // fallback won the tie, so position did NOT uniquely decide → content.\n if (bestTurn === assistantCount && fallbackIdx !== -1 && fallbackIdx < bestIdx) {\n return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n }\n // A UNIQUE positional decision requires the chosen turn to sit EXACTLY at\n // the current count AND to be the only candidate at that exact position —\n // otherwise registration order, not position, broke the tie.\n const atExactPosition =\n bestTurn === assistantCount &&\n candidates.filter((f) => f.match.turnIndex === assistantCount).length === 1;\n return { fixture: candidates[bestIdx], byUniquePosition: atExactPosition };\n }\n\n // Tier 3: every scripted turn is ahead. A plain fallback answers this earlier\n // point; first-registered fallback wins.\n if (fallbackIdx !== -1) return { fixture: candidates[fallbackIdx], byUniquePosition: false };\n\n // Tier 4: pure script, all turnIndexed and all ahead. Serve the lowest\n // scripted turn; registration order breaks ties (first of the lowest wins).\n let lowest = candidates[0];\n for (const f of candidates) {\n if ((f.match.turnIndex as number) < (lowest.match.turnIndex as number)) lowest = f;\n }\n return { fixture: lowest, byUniquePosition: false };\n}\n\n/**\n * Match a fixture against a request, returning the fixture or `null`. Thin\n * wrapper over {@link matchFixtureDiagnostic} that discards the skip diagnostic\n * — preserves the historical signature for all existing callers.\n */\nexport function matchFixture(\n fixtures: Fixture[],\n req: ChatCompletionRequest,\n matchCounts?: Map<Fixture, number>,\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest,\n options?: MatchOptions,\n): Fixture | null {\n return matchFixtureDiagnostic(fixtures, req, matchCounts, requestTransform, options).fixture;\n}\n\n/** Select for serving and reserve an injected error before the caller can await.\n * Diagnostic-only matching stays non-consuming. A competing selection retries\n * against the live queue; cancellation must release the reserved fixture.\n */\nexport function selectFixtureForServing(\n ...args: Parameters<typeof matchFixtureDiagnostic>\n): MatchFixtureDiagnostic {\n for (;;) {\n const attempt = matchFixtureDiagnostic(...args);\n if (\n attempt.fixture &&\n isOneShotError(attempt.fixture) &&\n !claimOneShotError(args[0], attempt.fixture)\n )\n continue;\n return attempt;\n }\n}\n"],"mappings":"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"}
|
package/dist/router.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatCompletionRequest, ContentPart, Fixture } from "./types.cjs";
|
|
1
|
+
import { ChatCompletionRequest, ContentPart, Fixture, RawWireId, WireId } from "./types.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/router.d.ts
|
|
4
4
|
|
|
@@ -89,6 +89,12 @@ interface MatchOptions {
|
|
|
89
89
|
logger?: {
|
|
90
90
|
warn(...args: unknown[]): void;
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* @internal The wire of the calling LLM handler. Set by handlers, never
|
|
94
|
+
* read from the request body. A raw fixture (see `isRawResponse`) matches
|
|
95
|
+
* only when its `raw.wire` equals this; with no wire, raw never matches.
|
|
96
|
+
*/
|
|
97
|
+
wire?: WireId | RawWireId;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* Test-only hook to clear the throttle state between cases. Not part of the
|
package/dist/router.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.cts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"router.d.cts","names":[],"sources":["../src/router.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;iBA4IgB,cAAA,mBAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA+DhC,sBAAA;WACN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8BM,YAAA;;;;;;;;;;;;;;;;;SAeR,SAAS;;;;;;;;;;;;;;;iBA0EF,sBAAA,WACJ,gBACL,qCACS,IAAI,2CACO,0BAA0B,iCACzC,eACT;;;;;;iBA8hBa,YAAA,WACJ,gBACL,qCACS,IAAI,2CACO,0BAA0B,iCACzC,eACT"}
|
package/dist/router.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatCompletionRequest, ContentPart, Fixture } from "./types.js";
|
|
1
|
+
import { ChatCompletionRequest, ContentPart, Fixture, RawWireId, WireId } from "./types.js";
|
|
2
2
|
|
|
3
3
|
//#region src/router.d.ts
|
|
4
4
|
|
|
@@ -89,6 +89,12 @@ interface MatchOptions {
|
|
|
89
89
|
logger?: {
|
|
90
90
|
warn(...args: unknown[]): void;
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* @internal The wire of the calling LLM handler. Set by handlers, never
|
|
94
|
+
* read from the request body. A raw fixture (see `isRawResponse`) matches
|
|
95
|
+
* only when its `raw.wire` equals this; with no wire, raw never matches.
|
|
96
|
+
*/
|
|
97
|
+
wire?: WireId | RawWireId;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* Test-only hook to clear the throttle state between cases. Not part of the
|