@copilotkit/aimock 1.45.1 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -1
- package/README.md +11 -2
- package/dist/agui-mock.cjs +25 -25
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/aws-event-stream.cjs +20 -23
- package/dist/aws-event-stream.cjs.map +1 -1
- package/dist/aws-event-stream.d.cts +0 -1
- package/dist/aws-event-stream.d.ts +0 -1
- package/dist/aws-event-stream.js +20 -24
- package/dist/aws-event-stream.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +139 -161
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +137 -159
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +175 -216
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +172 -213
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +107 -180
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +106 -179
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +140 -44
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +141 -46
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +113 -156
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +113 -156
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +295 -218
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +295 -219
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +411 -75
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +51 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +51 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +402 -74
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-handler.cjs +7 -3
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +7 -3
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +235 -202
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +234 -202
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/misbehavior.cjs +62 -3
- package/dist/misbehavior.cjs.map +1 -1
- package/dist/misbehavior.d.cts.map +1 -1
- package/dist/misbehavior.d.ts.map +1 -1
- package/dist/misbehavior.js +63 -4
- package/dist/misbehavior.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ndjson-writer.cjs +21 -23
- package/dist/ndjson-writer.cjs.map +1 -1
- package/dist/ndjson-writer.d.cts +0 -1
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +0 -1
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ndjson-writer.js +21 -24
- package/dist/ndjson-writer.js.map +1 -1
- package/dist/ollama.cjs +276 -210
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +273 -207
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +51 -74
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +37 -60
- package/dist/openrouter-video.js.map +1 -1
- package/dist/raw-validation.cjs +177 -0
- package/dist/raw-validation.cjs.map +1 -0
- package/dist/raw-validation.js +176 -0
- package/dist/raw-validation.js.map +1 -0
- package/dist/recorder.cjs +176 -95
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/deliver.cjs +144 -0
- package/dist/response/deliver.cjs.map +1 -0
- package/dist/response/deliver.js +143 -0
- package/dist/response/deliver.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/frame-loop.cjs +102 -0
- package/dist/response/frame-loop.cjs.map +1 -0
- package/dist/response/frame-loop.js +100 -0
- package/dist/response/frame-loop.js.map +1 -0
- package/dist/response/lifecycle.cjs +223 -0
- package/dist/response/lifecycle.cjs.map +1 -0
- package/dist/response/lifecycle.js +211 -0
- package/dist/response/lifecycle.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/proxy.cjs +71 -0
- package/dist/response/proxy.cjs.map +1 -0
- package/dist/response/proxy.js +70 -0
- package/dist/response/proxy.js.map +1 -0
- package/dist/response/raw.cjs +228 -0
- package/dist/response/raw.cjs.map +1 -0
- package/dist/response/raw.js +227 -0
- package/dist/response/raw.js.map +1 -0
- package/dist/response/timing.cjs +157 -0
- package/dist/response/timing.cjs.map +1 -0
- package/dist/response/timing.js +150 -0
- package/dist/response/timing.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +304 -196
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +303 -195
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +24 -4
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +7 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -5
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +293 -248
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +294 -249
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +56 -30
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +0 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +0 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +55 -31
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +31 -43
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +31 -43
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +231 -9
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +231 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-framing.cjs +12 -6
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +2 -0
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -0
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +12 -6
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +131 -96
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +130 -95
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +108 -124
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +105 -121
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +80 -59
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +79 -58
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +57 -16
package/dist/model-utils.js
CHANGED
package/dist/model-utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-utils.js","names":[],"sources":["../src/model-utils.ts"],"sourcesContent":["const DATE_SUFFIX_RE = /[-](\\d{8}(-v\\d+([:.]\\d+)*)?)$|[-]\\d{4}-\\d{2}-\\d{2}$/;\n\nexport function normalizeModelName(\n model: string | undefined,\n skipNormalization?: boolean,\n): string | undefined {\n if (!model || skipNormalization) return model;\n return model.replace(DATE_SUFFIX_RE, \"\");\n}\n\n// ─── Reasoning-capability map ───────────────────────────────────────────────\n//\n// `isReasoningModel` answers: \"would the REAL provider for this model id emit a\n// reasoning/thinking channel?\" aimock uses it to gate synthesized reasoning so a\n// fixture's `reasoning` field is not replayed for a model that cannot reason\n// (the gpt-4.1 false-green described in aimock#254).\n//\n// Policy (see the #254 spec for the full rationale):\n// - The KNOWN-NON-REASONING list (NONREASONING_FAMILIES) is the load-bearing\n// guard. A match there → false.\n// - The reasoning list (REASONING_FAMILIES) is mostly documentation; because\n// unknown ids default to `true` it is not load-bearing for correctness.\n// - UNKNOWN id → true (assume reasoning-capable). A false negative silently\n// breaks legitimate replays and is hard to notice; a false positive merely\n// fails to catch one mis-wiring class. Failing open keeps the blast radius\n// small and bounds maintenance to the explicit non-reasoning list.\n//\n// Env overrides let users correct drift without a release:\n// AIMOCK_REASONING_MODELS — comma-separated ids/prefixes forced capable.\n// AIMOCK_NONREASONING_MODELS — comma-separated ids/prefixes forced incapable.\n// Env entries are normalized IDENTICALLY to the incoming model id (date suffix\n// + provider/region prefix stripped, lowercased) so a prefixed entry such as\n// `anthropic.claude-opus-4` matches the already-stripped id.\n//\n// Precedence is BETWEEN lists, enforced by the sequential `if` checks below:\n// env-nonreasoning > env-reasoning > built-in-nonreasoning > built-in-reasoning > unknown(true)\n// Within any single list the result is order-INDEPENDENT — `matchesAny` uses\n// `Array.some(startsWith)`, so any prefix match yields that list's verdict.\n\n// Known non-reasoning families — the load-bearing denylist. Entries are matched\n// as prefixes against the normalized, lowercased, provider-stripped model id.\n// Order within this list does not matter (see `matchesAny`): any prefix that\n// matches the id produces `false`.\nconst NONREASONING_FAMILIES = [\n \"gpt-4.1\", // the exact model from aimock#254 (covers -mini / -nano)\n \"gpt-4o\", // covers gpt-4o-mini\n \"gpt-4-turbo\",\n \"gpt-4\", // plain gpt-4; order-independent, and prefix-safe since longer ids (gpt-4.1/gpt-4o/gpt-4-turbo) are also denylisted\n \"gpt-3.5\",\n \"claude-3-5-\", // Sonnet/Haiku 3.5 — no extended thinking\n \"claude-3-haiku\",\n \"claude-3-opus\",\n \"claude-3-sonnet\",\n \"gemini-1.5-\",\n];\n\n// Reasoning-capable families (informational; unknown already defaults to true).\nconst REASONING_FAMILIES = [\n \"o1\",\n \"o3\",\n \"o4\",\n \"gpt-5\",\n \"deepseek-r1\",\n \"deepseek-reasoner\",\n \"claude-3-7-sonnet\",\n \"claude-opus-4\",\n \"claude-sonnet-4\",\n \"claude-haiku-4\",\n \"gpt-oss\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-2.0-flash-thinking\",\n \"qwq\",\n \"qwen3\",\n];\n\n/**\n * Strip a leading provider segment that Bedrock (and similar) prepend before\n * the actual family token — e.g. `anthropic.`, `us.anthropic.`, `eu.`. Keeps\n * everything from the recognised family token onward so prefix matching works.\n */\
|
|
1
|
+
{"version":3,"file":"model-utils.js","names":[],"sources":["../src/model-utils.ts"],"sourcesContent":["const DATE_SUFFIX_RE = /[-](\\d{8}(-v\\d+([:.]\\d+)*)?)$|[-]\\d{4}-\\d{2}-\\d{2}$/;\n\nexport function normalizeModelName(\n model: string | undefined,\n skipNormalization?: boolean,\n): string | undefined {\n if (!model || skipNormalization) return model;\n return model.replace(DATE_SUFFIX_RE, \"\");\n}\n\n// ─── Reasoning-capability map ───────────────────────────────────────────────\n//\n// `isReasoningModel` answers: \"would the REAL provider for this model id emit a\n// reasoning/thinking channel?\" aimock uses it to gate synthesized reasoning so a\n// fixture's `reasoning` field is not replayed for a model that cannot reason\n// (the gpt-4.1 false-green described in aimock#254).\n//\n// Policy (see the #254 spec for the full rationale):\n// - The KNOWN-NON-REASONING list (NONREASONING_FAMILIES) is the load-bearing\n// guard. A match there → false.\n// - The reasoning list (REASONING_FAMILIES) is mostly documentation; because\n// unknown ids default to `true` it is not load-bearing for correctness.\n// - UNKNOWN id → true (assume reasoning-capable). A false negative silently\n// breaks legitimate replays and is hard to notice; a false positive merely\n// fails to catch one mis-wiring class. Failing open keeps the blast radius\n// small and bounds maintenance to the explicit non-reasoning list.\n//\n// Env overrides let users correct drift without a release:\n// AIMOCK_REASONING_MODELS — comma-separated ids/prefixes forced capable.\n// AIMOCK_NONREASONING_MODELS — comma-separated ids/prefixes forced incapable.\n// Env entries are normalized IDENTICALLY to the incoming model id (date suffix\n// + provider/region prefix stripped, lowercased) so a prefixed entry such as\n// `anthropic.claude-opus-4` matches the already-stripped id.\n//\n// Precedence is BETWEEN lists, enforced by the sequential `if` checks below:\n// env-nonreasoning > env-reasoning > built-in-nonreasoning > built-in-reasoning > unknown(true)\n// Within any single list the result is order-INDEPENDENT — `matchesAny` uses\n// `Array.some(startsWith)`, so any prefix match yields that list's verdict.\n\n// Known non-reasoning families — the load-bearing denylist. Entries are matched\n// as prefixes against the normalized, lowercased, provider-stripped model id.\n// Order within this list does not matter (see `matchesAny`): any prefix that\n// matches the id produces `false`.\nconst NONREASONING_FAMILIES = [\n \"gpt-4.1\", // the exact model from aimock#254 (covers -mini / -nano)\n \"gpt-4o\", // covers gpt-4o-mini\n \"gpt-4-turbo\",\n \"gpt-4\", // plain gpt-4; order-independent, and prefix-safe since longer ids (gpt-4.1/gpt-4o/gpt-4-turbo) are also denylisted\n \"gpt-3.5\",\n \"claude-3-5-\", // Sonnet/Haiku 3.5 — no extended thinking\n \"claude-3-haiku\",\n \"claude-3-opus\",\n \"claude-3-sonnet\",\n \"gemini-1.5-\",\n];\n\n// Reasoning-capable families (informational; unknown already defaults to true).\nconst REASONING_FAMILIES = [\n \"o1\",\n \"o3\",\n \"o4\",\n \"gpt-5\",\n \"deepseek-r1\",\n \"deepseek-reasoner\",\n \"claude-3-7-sonnet\",\n \"claude-opus-4\",\n \"claude-sonnet-4\",\n \"claude-haiku-4\",\n \"gpt-oss\",\n \"gemini-2.5-pro\",\n \"gemini-2.5-flash\",\n \"gemini-2.0-flash-thinking\",\n \"qwq\",\n \"qwen3\",\n];\n\n/**\n * Strip a leading provider segment that Bedrock (and similar) prepend before\n * the actual family token — e.g. `anthropic.`, `us.anthropic.`, `eu.`. Keeps\n * everything from the recognised family token onward so prefix matching works.\n */\nexport function stripProviderPrefix(id: string): string {\n // Remove leading region/provider segments (dot-separated) until we hit the\n // model token. Provider/region segments never contain a hyphen, whereas\n // model families do (e.g. `claude-3-5-sonnet`, `gpt-4.1`). So drop any\n // leading dot-separated segment that has no hyphen.\n let rest = id;\n while (true) {\n const dot = rest.indexOf(\".\");\n if (dot === -1) break;\n const head = rest.slice(0, dot);\n if (head.length === 0 || head.includes(\"-\")) break;\n rest = rest.slice(dot + 1);\n }\n return rest;\n}\n\n// Normalize each comma-separated env entry IDENTICALLY to the incoming model id\n// (date-suffix + provider/region prefix stripped, lowercased) so a prefixed\n// entry such as `anthropic.claude-opus-4` matches the already-stripped id.\nfunction parseEnvList(raw: string | undefined): string[] {\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((s) => stripProviderPrefix(normalizeModelName(s.trim())?.toLowerCase() ?? \"\"))\n .filter((s) => s.length > 0);\n}\n\nfunction matchesAny(id: string, families: readonly string[]): boolean {\n return families.some((fam) => id.startsWith(fam));\n}\n\n/**\n * Returns whether the real provider for `model` would emit a reasoning/thinking\n * channel. Absence of a model id, or an unrecognised id, defaults to `true`\n * (assume capable) — see the policy note above.\n */\nexport function isReasoningModel(model: string | undefined): boolean {\n if (!model) return true;\n\n const normalized = normalizeModelName(model)?.toLowerCase() ?? \"\";\n const id = stripProviderPrefix(normalized);\n\n // Env overrides win, parsed fresh so they remain test-stubbable.\n const envNon = parseEnvList(process.env.AIMOCK_NONREASONING_MODELS);\n if (matchesAny(id, envNon)) return false;\n const envReasoning = parseEnvList(process.env.AIMOCK_REASONING_MODELS);\n if (matchesAny(id, envReasoning)) return true;\n\n // Built-in denylist is the active guard.\n if (matchesAny(id, NONREASONING_FAMILIES)) return false;\n // Built-in reasoning list short-circuits to capable.\n if (matchesAny(id, REASONING_FAMILIES)) return true;\n\n // Unknown → assume reasoning-capable (fail open).\n return true;\n}\n"],"mappings":";AAAA,MAAM,iBAAiB;AAEvB,SAAgB,mBACd,OACA,mBACoB;AACpB,KAAI,CAAC,SAAS,kBAAmB,QAAO;AACxC,QAAO,MAAM,QAAQ,gBAAgB,GAAG;;AAoC1C,MAAM,wBAAwB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAGD,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;AAOD,SAAgB,oBAAoB,IAAoB;CAKtD,IAAI,OAAO;AACX,QAAO,MAAM;EACX,MAAM,MAAM,KAAK,QAAQ,IAAI;AAC7B,MAAI,QAAQ,GAAI;EAChB,MAAM,OAAO,KAAK,MAAM,GAAG,IAAI;AAC/B,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,IAAI,CAAE;AAC7C,SAAO,KAAK,MAAM,MAAM,EAAE;;AAE5B,QAAO;;AAMT,SAAS,aAAa,KAAmC;AACvD,KAAI,CAAC,IAAK,QAAO,EAAE;AACnB,QAAO,IACJ,MAAM,IAAI,CACV,KAAK,MAAM,oBAAoB,mBAAmB,EAAE,MAAM,CAAC,EAAE,aAAa,IAAI,GAAG,CAAC,CAClF,QAAQ,MAAM,EAAE,SAAS,EAAE;;AAGhC,SAAS,WAAW,IAAY,UAAsC;AACpE,QAAO,SAAS,MAAM,QAAQ,GAAG,WAAW,IAAI,CAAC;;;;;;;AAQnD,SAAgB,iBAAiB,OAAoC;AACnE,KAAI,CAAC,MAAO,QAAO;CAGnB,MAAM,KAAK,oBADQ,mBAAmB,MAAM,EAAE,aAAa,IAAI,GACrB;AAI1C,KAAI,WAAW,IADA,aAAa,QAAQ,IAAI,2BAA2B,CACzC,CAAE,QAAO;AAEnC,KAAI,WAAW,IADM,aAAa,QAAQ,IAAI,wBAAwB,CACtC,CAAE,QAAO;AAGzC,KAAI,WAAW,IAAI,sBAAsB,CAAE,QAAO;AAElD,KAAI,WAAW,IAAI,mBAAmB,CAAE,QAAO;AAG/C,QAAO"}
|
package/dist/moderation.cjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const require_errors = require('./response/errors.cjs');
|
|
2
|
+
const require_policy = require('./response/policy.cjs');
|
|
1
3
|
const require_helpers = require('./helpers.cjs');
|
|
2
4
|
|
|
3
5
|
//#region src/moderation.ts
|
|
@@ -62,11 +64,11 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
66
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
65
|
-
res.end(
|
|
67
|
+
res.end(require_errors.renderError("openai", "validation", {
|
|
66
68
|
message: `Malformed JSON: ${detail}`,
|
|
67
69
|
type: "invalid_request_error",
|
|
68
70
|
code: "invalid_json"
|
|
69
|
-
}
|
|
71
|
+
}).body);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
if (!require_helpers.isJsonObject(body)) {
|
|
@@ -82,10 +84,10 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
82
84
|
}
|
|
83
85
|
});
|
|
84
86
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
85
|
-
res.end(
|
|
87
|
+
res.end(require_errors.renderError("openai", "validation", {
|
|
86
88
|
message: "Request body must be a JSON object",
|
|
87
89
|
type: "invalid_request_error"
|
|
88
|
-
}
|
|
90
|
+
}).body);
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
93
|
const normalized = require_helpers.normalizeTextInput(body.input ?? "");
|
|
@@ -102,12 +104,13 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
102
104
|
}
|
|
103
105
|
});
|
|
104
106
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
105
|
-
res.end(
|
|
107
|
+
res.end(require_errors.renderError("openai", "validation", {
|
|
106
108
|
message: "Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts",
|
|
107
109
|
type: "invalid_request_error",
|
|
108
110
|
param: null,
|
|
109
|
-
code: null
|
|
110
|
-
|
|
111
|
+
code: null,
|
|
112
|
+
includeParam: true
|
|
113
|
+
}).body);
|
|
111
114
|
return;
|
|
112
115
|
}
|
|
113
116
|
const inputText = normalized;
|
|
@@ -121,7 +124,7 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
121
124
|
}
|
|
122
125
|
if (matchedFixture) logger.debug(`Moderation fixture matched for input "${inputText.slice(0, 80)}"`);
|
|
123
126
|
else logger.debug(`No moderation fixture matched for input "${inputText.slice(0, 80)}" — returning unflagged`);
|
|
124
|
-
if (!matchedFixture &&
|
|
127
|
+
if (!matchedFixture && require_policy.resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
125
128
|
const strictMessage = require_helpers.strictNoMatchMessage(0);
|
|
126
129
|
logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", req.url ?? "/v1/moderations", 0));
|
|
127
130
|
journal.add({
|
|
@@ -133,15 +136,15 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
133
136
|
response: {
|
|
134
137
|
status: 503,
|
|
135
138
|
fixture: null,
|
|
136
|
-
...
|
|
139
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
137
140
|
}
|
|
138
141
|
});
|
|
139
142
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
140
|
-
res.end(
|
|
143
|
+
res.end(require_errors.renderError("openai", "strict-miss", {
|
|
141
144
|
message: strictMessage,
|
|
142
145
|
type: "invalid_request_error",
|
|
143
146
|
code: "no_fixture_match"
|
|
144
|
-
}
|
|
147
|
+
}).body);
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
journal.add({
|
|
@@ -153,7 +156,7 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
153
156
|
response: {
|
|
154
157
|
status: 200,
|
|
155
158
|
fixture: null,
|
|
156
|
-
...
|
|
159
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
157
160
|
}
|
|
158
161
|
});
|
|
159
162
|
res.writeHead(200, { "Content-Type": "application/json" });
|
package/dist/moderation.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.cjs","names":["flattenHeaders","isJsonObject","normalizeTextInput","matchesPattern","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","generateId"],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n resolveStrictMode,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n strictOverrideField,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n/**\n * Model echoed when the caller sends no `model`.\n *\n * The real API echoes back the model the request asked for. `text-moderation-*`\n * was removed from the OpenAI API on 2025-10-27; `omni-moderation-latest` is\n * the current moderation model, so it stands in when the caller omits one.\n */\nconst DEFAULT_MODERATION_MODEL = \"omni-moderation-latest\";\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[]; model?: unknown };\n try {\n body = JSON.parse(raw) as { input?: string | string[]; model?: unknown };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n const inputText = normalized;\n\n // Echo the requested model, like the real API. A missing (or non-string)\n // `model` falls back to the current default rather than a removed id.\n const requestedModel =\n typeof body.model === \"string\" && body.model.length > 0 ? body.model : DEFAULT_MODERATION_MODEL;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: requestedModel,\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"moderation.cjs","names":["flattenHeaders","renderError","isJsonObject","normalizeTextInput","matchesPattern","resolveRequestDecisions","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","generateId"],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport { renderError } from \"./response/errors.js\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n/**\n * Model echoed when the caller sends no `model`.\n *\n * The real API echoes back the model the request asked for. `text-moderation-*`\n * was removed from the OpenAI API on 2025-10-27; `omni-moderation-latest` is\n * the current moderation model, so it stands in when the caller omits one.\n */\nconst DEFAULT_MODERATION_MODEL = \"omni-moderation-latest\";\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[]; model?: unknown };\n try {\n body = JSON.parse(raw) as { input?: string | string[]; model?: unknown };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n includeParam: true,\n }).body,\n );\n return;\n }\n const inputText = normalized;\n\n // Echo the requested model, like the real API. A missing (or non-string)\n // `model` falls back to the current default rather than a removed id.\n const requestedModel =\n typeof body.model === \"string\" && body.model.length > 0 ? body.model : DEFAULT_MODERATION_MODEL;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"strict-miss\", {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: requestedModel,\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/moderation.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderError } from "./response/errors.js";
|
|
2
|
+
import { resolveRequestDecisions, strictOverrideField } from "./response/policy.js";
|
|
3
|
+
import { flattenHeaders, generateId, isJsonObject, matchesPattern, normalizeTextInput, strictNoMatchLogLine, strictNoMatchMessage } from "./helpers.js";
|
|
2
4
|
|
|
3
5
|
//#region src/moderation.ts
|
|
4
6
|
/**
|
|
@@ -62,11 +64,11 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
66
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
65
|
-
res.end(
|
|
67
|
+
res.end(renderError("openai", "validation", {
|
|
66
68
|
message: `Malformed JSON: ${detail}`,
|
|
67
69
|
type: "invalid_request_error",
|
|
68
70
|
code: "invalid_json"
|
|
69
|
-
}
|
|
71
|
+
}).body);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
if (!isJsonObject(body)) {
|
|
@@ -82,10 +84,10 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
82
84
|
}
|
|
83
85
|
});
|
|
84
86
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
85
|
-
res.end(
|
|
87
|
+
res.end(renderError("openai", "validation", {
|
|
86
88
|
message: "Request body must be a JSON object",
|
|
87
89
|
type: "invalid_request_error"
|
|
88
|
-
}
|
|
90
|
+
}).body);
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
93
|
const normalized = normalizeTextInput(body.input ?? "");
|
|
@@ -102,12 +104,13 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
102
104
|
}
|
|
103
105
|
});
|
|
104
106
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
105
|
-
res.end(
|
|
107
|
+
res.end(renderError("openai", "validation", {
|
|
106
108
|
message: "Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts",
|
|
107
109
|
type: "invalid_request_error",
|
|
108
110
|
param: null,
|
|
109
|
-
code: null
|
|
110
|
-
|
|
111
|
+
code: null,
|
|
112
|
+
includeParam: true
|
|
113
|
+
}).body);
|
|
111
114
|
return;
|
|
112
115
|
}
|
|
113
116
|
const inputText = normalized;
|
|
@@ -121,7 +124,7 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
121
124
|
}
|
|
122
125
|
if (matchedFixture) logger.debug(`Moderation fixture matched for input "${inputText.slice(0, 80)}"`);
|
|
123
126
|
else logger.debug(`No moderation fixture matched for input "${inputText.slice(0, 80)}" — returning unflagged`);
|
|
124
|
-
if (!matchedFixture &&
|
|
127
|
+
if (!matchedFixture && resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
125
128
|
const strictMessage = strictNoMatchMessage(0);
|
|
126
129
|
logger.error(strictNoMatchLogLine(req.method ?? "POST", req.url ?? "/v1/moderations", 0));
|
|
127
130
|
journal.add({
|
|
@@ -137,11 +140,11 @@ async function handleModeration(req, res, raw, fixtures, journal, defaults, setC
|
|
|
137
140
|
}
|
|
138
141
|
});
|
|
139
142
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
140
|
-
res.end(
|
|
143
|
+
res.end(renderError("openai", "strict-miss", {
|
|
141
144
|
message: strictMessage,
|
|
142
145
|
type: "invalid_request_error",
|
|
143
146
|
code: "no_fixture_match"
|
|
144
|
-
}
|
|
147
|
+
}).body);
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
journal.add({
|
package/dist/moderation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"moderation.js","names":[],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n resolveStrictMode,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n strictOverrideField,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n/**\n * Model echoed when the caller sends no `model`.\n *\n * The real API echoes back the model the request asked for. `text-moderation-*`\n * was removed from the OpenAI API on 2025-10-27; `omni-moderation-latest` is\n * the current moderation model, so it stands in when the caller omits one.\n */\nconst DEFAULT_MODERATION_MODEL = \"omni-moderation-latest\";\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[]; model?: unknown };\n try {\n body = JSON.parse(raw) as { input?: string | string[]; model?: unknown };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n },\n }),\n );\n return;\n }\n const inputText = normalized;\n\n // Echo the requested model, like the real API. A missing (or non-string)\n // `model` falls back to the current default rather than a removed id.\n const requestedModel =\n typeof body.model === \"string\" && body.model.length > 0 ? body.model : DEFAULT_MODERATION_MODEL;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveStrictMode(defaults.strict, req.headers)) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: requestedModel,\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"moderation.js","names":[],"sources":["../src/moderation.ts"],"sourcesContent":["/**\n * Moderation API support for LLMock.\n *\n * Handles POST /v1/moderations requests (OpenAI-compatible). Matches\n * fixtures by comparing the request `input` field against registered\n * patterns. First match wins; no match returns a default unflagged result.\n */\n\nimport type * as http from \"node:http\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport { renderError } from \"./response/errors.js\";\nimport {\n flattenHeaders,\n generateId,\n isJsonObject,\n matchesPattern,\n normalizeTextInput,\n strictNoMatchLogLine,\n strictNoMatchMessage,\n} from \"./helpers.js\";\nimport type { Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\n\n// ─── Moderation types ─────────────────────────────────────────────────────\n\nexport interface ModerationResult {\n flagged: boolean;\n categories: Record<string, boolean>;\n category_scores?: Record<string, number>;\n}\n\nexport interface ModerationFixture {\n match: string | RegExp;\n result: ModerationResult;\n}\n\n/**\n * Model echoed when the caller sends no `model`.\n *\n * The real API echoes back the model the request asked for. `text-moderation-*`\n * was removed from the OpenAI API on 2025-10-27; `omni-moderation-latest` is\n * the current moderation model, so it stands in when the caller omits one.\n */\nconst DEFAULT_MODERATION_MODEL = \"omni-moderation-latest\";\n\n// ─── Default unflagged result ─────────────────────────────────────────────\n\nconst DEFAULT_RESULT: ModerationResult = {\n flagged: false,\n categories: {\n sexual: false,\n hate: false,\n harassment: false,\n \"self-harm\": false,\n \"sexual/minors\": false,\n \"hate/threatening\": false,\n \"violence/graphic\": false,\n \"self-harm/intent\": false,\n \"self-harm/instructions\": false,\n \"harassment/threatening\": false,\n violence: false,\n illicit: false,\n \"illicit/violent\": false,\n },\n category_scores: {\n sexual: 0,\n hate: 0,\n harassment: 0,\n \"self-harm\": 0,\n \"sexual/minors\": 0,\n \"hate/threatening\": 0,\n \"violence/graphic\": 0,\n \"self-harm/intent\": 0,\n \"self-harm/instructions\": 0,\n \"harassment/threatening\": 0,\n violence: 0,\n illicit: 0,\n \"illicit/violent\": 0,\n },\n};\n\n// ─── Request handler ──────────────────────────────────────────────────────\n\nexport async function handleModeration(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: ModerationFixture[],\n journal: Journal,\n defaults: { logger: Logger; strict?: boolean },\n setCorsHeaders: (res: http.ServerResponse) => void,\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let body: { input?: string | string[]; model?: unknown };\n try {\n body = JSON.parse(raw) as { input?: string | string[]; model?: unknown };\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `body.input` throws a TypeError that surfaces\n // as a 500 instead of a 400.\n if (!isJsonObject(body)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message: \"Request body must be a JSON object\",\n type: \"invalid_request_error\",\n }).body,\n );\n return;\n }\n\n // Normalize input to a single string for matching — reject non-string\n // inputs with 400 instead of crashing in matchesPattern()/slice() (500).\n const rawInput: unknown = body.input ?? \"\";\n const normalized = normalizeTextInput(rawInput);\n if (normalized === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: { status: 400, fixture: null },\n });\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"validation\", {\n message:\n \"Invalid parameter: 'input' must be a string, an array of strings, or an array of multimodal parts\",\n type: \"invalid_request_error\",\n param: null,\n code: null,\n includeParam: true,\n }).body,\n );\n return;\n }\n const inputText = normalized;\n\n // Echo the requested model, like the real API. A missing (or non-string)\n // `model` falls back to the current default rather than a removed id.\n const requestedModel =\n typeof body.model === \"string\" && body.model.length > 0 ? body.model : DEFAULT_MODERATION_MODEL;\n\n // Find first matching fixture\n let matchedResult: ModerationResult = DEFAULT_RESULT;\n let matchedFixture: ModerationFixture | null = null;\n\n for (const fixture of fixtures) {\n if (matchesPattern(inputText, fixture.match)) {\n matchedFixture = fixture;\n matchedResult = fixture.result;\n break;\n }\n }\n\n if (matchedFixture) {\n logger.debug(`Moderation fixture matched for input \"${inputText.slice(0, 80)}\"`);\n } else {\n logger.debug(\n `No moderation fixture matched for input \"${inputText.slice(0, 80)}\" — returning unflagged`,\n );\n }\n\n // Strict mode turns the lenient default (unflagged) into a no-match error,\n // like every other fixture-matching handler. These fixtures carry no\n // sequence/turn state, so the skipped count is always 0.\n if (!matchedFixture && resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {\n const strictMessage = strictNoMatchMessage(0);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", req.url ?? \"/v1/moderations\", 0));\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n res.writeHead(503, { \"Content-Type\": \"application/json\" });\n res.end(\n renderError(\"openai\", \"strict-miss\", {\n message: strictMessage,\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n }).body,\n );\n return;\n }\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/moderations\",\n headers: flattenHeaders(req.headers),\n body: null,\n service: \"moderation\",\n response: {\n status: 200,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n id: generateId(\"modr\"),\n model: requestedModel,\n results: [matchedResult],\n }),\n );\n}\n"],"mappings":"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"}
|
package/dist/ndjson-writer.cjs
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
const
|
|
1
|
+
const require_frame_loop = require('./response/frame-loop.cjs');
|
|
2
2
|
|
|
3
3
|
//#region src/ndjson-writer.ts
|
|
4
4
|
async function writeNDJSONStream(res, chunks, options) {
|
|
5
5
|
const opts = options ?? {};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (!res.writableEnded) res.end();
|
|
27
|
-
return true;
|
|
6
|
+
return require_frame_loop.runFrameLoop(require_frame_loop.httpSink(res), chunks, ndjsonCodec(), {
|
|
7
|
+
latency: opts.latency,
|
|
8
|
+
streamingProfile: opts.streamingProfile,
|
|
9
|
+
recordedTimings: opts.recordedTimings,
|
|
10
|
+
replaySpeed: opts.replaySpeed
|
|
11
|
+
}, {
|
|
12
|
+
signal: chunks.length > 0 ? opts.signal : void 0,
|
|
13
|
+
onChunkSent: opts.onChunkSent
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function ndjsonCodec() {
|
|
17
|
+
return {
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/x-ndjson",
|
|
20
|
+
"Cache-Control": "no-cache",
|
|
21
|
+
Connection: "keep-alive"
|
|
22
|
+
},
|
|
23
|
+
encode: (frame) => JSON.stringify(frame) + "\n"
|
|
24
|
+
};
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
//#endregion
|
|
28
|
+
exports.ndjsonCodec = ndjsonCodec;
|
|
31
29
|
exports.writeNDJSONStream = writeNDJSONStream;
|
|
32
30
|
//# sourceMappingURL=ndjson-writer.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ndjson-writer.cjs","names":["
|
|
1
|
+
{"version":3,"file":"ndjson-writer.cjs","names":["runFrameLoop","httpSink"],"sources":["../src/ndjson-writer.ts"],"sourcesContent":["/**\n * NDJSON streaming writer for Ollama endpoints.\n *\n * Mirrors writeSSEStream from sse-writer.ts but writes newline-delimited JSON\n * (one JSON object per line) instead of SSE events.\n */\n\nimport type * as http from \"node:http\";\nimport type { StreamingProfile, RecordedTimings } from \"./types.js\";\nimport { httpSink, runFrameLoop, type FrameCodec } from \"./response/frame-loop.js\";\n\nexport interface NDJSONStreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n}\n\nexport async function writeNDJSONStream(\n res: http.ServerResponse,\n chunks: object[],\n options?: NDJSONStreamOptions,\n): Promise<boolean> {\n const opts = options ?? {};\n // Base contract (public API): with no frames, an aborted signal does not\n // stop the writer; it still finishes the stream (any trailer, then end())\n // and returns true. The frame loop returns false there, so pass it no\n // signal when there are no frames (it reads the signal only per frame and\n // after the loop).\n return runFrameLoop(\n httpSink(res),\n chunks,\n ndjsonCodec(),\n {\n latency: opts.latency,\n streamingProfile: opts.streamingProfile,\n recordedTimings: opts.recordedTimings,\n replaySpeed: opts.replaySpeed,\n },\n { signal: chunks.length > 0 ? opts.signal : undefined, onChunkSent: opts.onChunkSent },\n );\n}\n\n// Internal (not part of the public API): newline-delimited JSON, one object\n// per line, no trailer (Ollama). Line comments, not JSDoc, so the bundled\n// .d.ts does not attach this comment to a public declaration.\nexport function ndjsonCodec<F = object>(): FrameCodec<F> {\n return {\n headers: {\n \"Content-Type\": \"application/x-ndjson\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n encode: (frame) => JSON.stringify(frame) + \"\\n\",\n };\n}\n"],"mappings":";;;AAoBA,eAAsB,kBACpB,KACA,QACA,SACkB;CAClB,MAAM,OAAO,WAAW,EAAE;AAM1B,QAAOA,gCACLC,4BAAS,IAAI,EACb,QACA,aAAa,EACb;EACE,SAAS,KAAK;EACd,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,aAAa,KAAK;EACnB,EACD;EAAE,QAAQ,OAAO,SAAS,IAAI,KAAK,SAAS;EAAW,aAAa,KAAK;EAAa,CACvF;;AAMH,SAAgB,cAAyC;AACvD,QAAO;EACL,SAAS;GACP,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACb;EACD,SAAS,UAAU,KAAK,UAAU,MAAM,GAAG;EAC5C"}
|
package/dist/ndjson-writer.d.cts
CHANGED
|
@@ -12,7 +12,6 @@ interface NDJSONStreamOptions {
|
|
|
12
12
|
onChunkSent?: () => void;
|
|
13
13
|
}
|
|
14
14
|
declare function writeNDJSONStream(res: http$1.ServerResponse, chunks: object[], options?: NDJSONStreamOptions): Promise<boolean>;
|
|
15
|
-
//# sourceMappingURL=ndjson-writer.d.ts.map
|
|
16
15
|
//#endregion
|
|
17
16
|
export { NDJSONStreamOptions, writeNDJSONStream };
|
|
18
17
|
//# sourceMappingURL=ndjson-writer.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ndjson-writer.d.cts","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"ndjson-writer.d.cts","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":[],"mappings":";;;;;AAgBsB,UALL,mBAAA,CAKK;EAIA,OAAA,CAAA,EAAA,MAAA;EAAiB,gBAAA,CAAA,EAPlB,gBAOkB;iBAC3B,CAAA,EAPQ,eAOR;aAEA,CAAA,EAAA,MAAA;QACT,CAAA,EARQ,WAQR;EAAO,WAAA,CAAA,EAAA,GAAA,GAAA,IAAA;;iBAJY,iBAAA,MACf,MAAA,CAAK,4CAEA,sBACT"}
|
package/dist/ndjson-writer.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ interface NDJSONStreamOptions {
|
|
|
12
12
|
onChunkSent?: () => void;
|
|
13
13
|
}
|
|
14
14
|
declare function writeNDJSONStream(res: http$1.ServerResponse, chunks: object[], options?: NDJSONStreamOptions): Promise<boolean>;
|
|
15
|
-
//# sourceMappingURL=ndjson-writer.d.ts.map
|
|
16
15
|
//#endregion
|
|
17
16
|
export { NDJSONStreamOptions, writeNDJSONStream };
|
|
18
17
|
//# sourceMappingURL=ndjson-writer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ndjson-writer.d.ts","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"ndjson-writer.d.ts","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":[],"mappings":";;;;;AAgBsB,UALL,mBAAA,CAKK;EAIA,OAAA,CAAA,EAAA,MAAA;EAAiB,gBAAA,CAAA,EAPlB,gBAOkB;iBAC3B,CAAA,EAPQ,eAOR;aAEA,CAAA,EAAA,MAAA;QACT,CAAA,EARQ,WAQR;EAAO,WAAA,CAAA,EAAA,GAAA,GAAA,IAAA;;iBAJY,iBAAA,MACf,MAAA,CAAK,4CAEA,sBACT"}
|
package/dist/ndjson-writer.js
CHANGED
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { httpSink, runFrameLoop } from "./response/frame-loop.js";
|
|
2
2
|
|
|
3
3
|
//#region src/ndjson-writer.ts
|
|
4
4
|
async function writeNDJSONStream(res, chunks, options) {
|
|
5
5
|
const opts = options ?? {};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
if (!res.writableEnded) res.end();
|
|
27
|
-
return true;
|
|
6
|
+
return runFrameLoop(httpSink(res), chunks, ndjsonCodec(), {
|
|
7
|
+
latency: opts.latency,
|
|
8
|
+
streamingProfile: opts.streamingProfile,
|
|
9
|
+
recordedTimings: opts.recordedTimings,
|
|
10
|
+
replaySpeed: opts.replaySpeed
|
|
11
|
+
}, {
|
|
12
|
+
signal: chunks.length > 0 ? opts.signal : void 0,
|
|
13
|
+
onChunkSent: opts.onChunkSent
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function ndjsonCodec() {
|
|
17
|
+
return {
|
|
18
|
+
headers: {
|
|
19
|
+
"Content-Type": "application/x-ndjson",
|
|
20
|
+
"Cache-Control": "no-cache",
|
|
21
|
+
Connection: "keep-alive"
|
|
22
|
+
},
|
|
23
|
+
encode: (frame) => JSON.stringify(frame) + "\n"
|
|
24
|
+
};
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
//#endregion
|
|
31
|
-
export { writeNDJSONStream };
|
|
28
|
+
export { ndjsonCodec, writeNDJSONStream };
|
|
32
29
|
//# sourceMappingURL=ndjson-writer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ndjson-writer.js","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":["/**\n * NDJSON streaming writer for Ollama endpoints.\n *\n * Mirrors writeSSEStream from sse-writer.ts but writes newline-delimited JSON\n * (one JSON object per line) instead of SSE events.\n */\n\nimport type * as http from \"node:http\";\nimport type { StreamingProfile, RecordedTimings } from \"./types.js\";\nimport {
|
|
1
|
+
{"version":3,"file":"ndjson-writer.js","names":[],"sources":["../src/ndjson-writer.ts"],"sourcesContent":["/**\n * NDJSON streaming writer for Ollama endpoints.\n *\n * Mirrors writeSSEStream from sse-writer.ts but writes newline-delimited JSON\n * (one JSON object per line) instead of SSE events.\n */\n\nimport type * as http from \"node:http\";\nimport type { StreamingProfile, RecordedTimings } from \"./types.js\";\nimport { httpSink, runFrameLoop, type FrameCodec } from \"./response/frame-loop.js\";\n\nexport interface NDJSONStreamOptions {\n latency?: number;\n streamingProfile?: StreamingProfile;\n recordedTimings?: RecordedTimings;\n replaySpeed?: number;\n signal?: AbortSignal;\n onChunkSent?: () => void;\n}\n\nexport async function writeNDJSONStream(\n res: http.ServerResponse,\n chunks: object[],\n options?: NDJSONStreamOptions,\n): Promise<boolean> {\n const opts = options ?? {};\n // Base contract (public API): with no frames, an aborted signal does not\n // stop the writer; it still finishes the stream (any trailer, then end())\n // and returns true. The frame loop returns false there, so pass it no\n // signal when there are no frames (it reads the signal only per frame and\n // after the loop).\n return runFrameLoop(\n httpSink(res),\n chunks,\n ndjsonCodec(),\n {\n latency: opts.latency,\n streamingProfile: opts.streamingProfile,\n recordedTimings: opts.recordedTimings,\n replaySpeed: opts.replaySpeed,\n },\n { signal: chunks.length > 0 ? opts.signal : undefined, onChunkSent: opts.onChunkSent },\n );\n}\n\n// Internal (not part of the public API): newline-delimited JSON, one object\n// per line, no trailer (Ollama). Line comments, not JSDoc, so the bundled\n// .d.ts does not attach this comment to a public declaration.\nexport function ndjsonCodec<F = object>(): FrameCodec<F> {\n return {\n headers: {\n \"Content-Type\": \"application/x-ndjson\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n },\n encode: (frame) => JSON.stringify(frame) + \"\\n\",\n };\n}\n"],"mappings":";;;AAoBA,eAAsB,kBACpB,KACA,QACA,SACkB;CAClB,MAAM,OAAO,WAAW,EAAE;AAM1B,QAAO,aACL,SAAS,IAAI,EACb,QACA,aAAa,EACb;EACE,SAAS,KAAK;EACd,kBAAkB,KAAK;EACvB,iBAAiB,KAAK;EACtB,aAAa,KAAK;EACnB,EACD;EAAE,QAAQ,OAAO,SAAS,IAAI,KAAK,SAAS;EAAW,aAAa,KAAK;EAAa,CACvF;;AAMH,SAAgB,cAAyC;AACvD,QAAO;EACL,SAAS;GACP,gBAAgB;GAChB,iBAAiB;GACjB,YAAY;GACb;EACD,SAAS,UAAU,KAAK,UAAU,MAAM,GAAG;EAC5C"}
|