@copilotkit/aimock 1.45.0 → 1.46.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 +24 -0
- package/README.md +7 -1
- 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.ts.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/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 +61 -80
- 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 +58 -77
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +65 -97
- 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 +62 -94
- 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 +49 -74
- 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 +47 -72
- 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 +37 -42
- 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 +37 -42
- 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 +50 -35
- 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 +49 -34
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +57 -67
- 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 +56 -67
- 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 +11 -68
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +12 -67
- 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.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- 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 +14 -4
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +14 -4
- 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 +111 -96
- 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 +109 -95
- 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/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/ollama.cjs +128 -68
- 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 +124 -64
- 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 +76 -71
- 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 +63 -58
- package/dist/openrouter-video.js.map +1 -1
- 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/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/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/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 +45 -58
- 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 +44 -57
- 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 +4 -0
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3 -0
- 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 +222 -146
- 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 +223 -147
- 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 +7 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +7 -1
- 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 +12 -10
- 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 +11 -9
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +62 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +62 -3
- 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-gemini-live.cjs +11 -3
- 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 +10 -2
- 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 +28 -54
- 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 +25 -51
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +8 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +7 -5
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +25 -16
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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"}
|