@copilotkit/aimock 1.42.1 → 1.44.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 +65 -0
- package/README.md +108 -16
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-mock.cjs +1 -1
- 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 +1 -1
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +33 -11
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +10 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +10 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +33 -11
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts +1 -0
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +1 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +38 -10
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts +5 -0
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts +5 -0
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +38 -10
- package/dist/aimock-cli.js.map +1 -1
- package/dist/batches.cjs +458 -0
- package/dist/batches.cjs.map +1 -0
- package/dist/batches.d.cts +57 -0
- package/dist/batches.d.cts.map +1 -0
- package/dist/batches.d.ts +57 -0
- package/dist/batches.d.ts.map +1 -0
- package/dist/batches.js +453 -0
- package/dist/batches.js.map +1 -0
- package/dist/bedrock-converse.cjs +44 -12
- 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 +46 -14
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +10 -8
- 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 +12 -10
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +3 -3
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.js +4 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +625 -72
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +164 -7
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +164 -7
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +616 -74
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +131 -30
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +132 -31
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +37 -9
- 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 +39 -11
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +1 -1
- package/dist/config-loader.js +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +6 -2
- package/dist/constants.d.ts +6 -2
- package/dist/constants.js +8 -1
- package/dist/constants.js.map +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.cjs.map +1 -1
- package/dist/convert.js +1 -1
- package/dist/convert.js.map +1 -1
- package/dist/echo-text.cjs +65 -0
- package/dist/echo-text.cjs.map +1 -0
- package/dist/echo-text.d.cts +22 -0
- package/dist/echo-text.d.cts.map +1 -0
- package/dist/echo-text.d.ts +22 -0
- package/dist/echo-text.d.ts.map +1 -0
- package/dist/echo-text.js +65 -0
- package/dist/echo-text.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +3 -3
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.js +4 -4
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +1053 -0
- package/dist/elevenlabs-voice.cjs.map +1 -0
- package/dist/elevenlabs-voice.d.cts +72 -0
- package/dist/elevenlabs-voice.d.cts.map +1 -0
- package/dist/elevenlabs-voice.d.ts +72 -0
- package/dist/elevenlabs-voice.d.ts.map +1 -0
- package/dist/elevenlabs-voice.js +1048 -0
- package/dist/elevenlabs-voice.js.map +1 -0
- package/dist/embeddings.cjs +26 -5
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +27 -6
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +3 -3
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.js +4 -4
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +28 -4
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts +16 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts +16 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -5
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +1069 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +68 -0
- package/dist/files.d.cts.map +1 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +1056 -0
- package/dist/files.js.map +1 -0
- package/dist/fine-tuning.cjs +1033 -0
- package/dist/fine-tuning.cjs.map +1 -0
- package/dist/fine-tuning.d.cts +96 -0
- package/dist/fine-tuning.d.cts.map +1 -0
- package/dist/fine-tuning.d.ts +96 -0
- package/dist/fine-tuning.d.ts.map +1 -0
- package/dist/fine-tuning.js +1026 -0
- package/dist/fine-tuning.js.map +1 -0
- package/dist/fixture-loader-services.cjs +116 -0
- package/dist/fixture-loader-services.cjs.map +1 -0
- package/dist/fixture-loader-services.d.cts +48 -0
- package/dist/fixture-loader-services.d.cts.map +1 -0
- package/dist/fixture-loader-services.d.ts +48 -0
- package/dist/fixture-loader-services.d.ts.map +1 -0
- package/dist/fixture-loader-services.js +114 -0
- package/dist/fixture-loader-services.js.map +1 -0
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +131 -5
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +131 -5
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +44 -4
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +46 -6
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +51 -7
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +53 -9
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +19 -8
- 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 +21 -10
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -14
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +15 -15
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +497 -10
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +38 -2
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +38 -2
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +487 -12
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +57 -125
- 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 +58 -126
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +71 -9
- package/dist/index.d.cts +17 -7
- package/dist/index.d.ts +17 -7
- package/dist/index.js +21 -11
- package/dist/jest.cjs +24 -12
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +6 -3
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +6 -3
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +25 -13
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +61 -4
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +60 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +60 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +61 -5
- package/dist/journal.js.map +1 -1
- package/dist/live-fixture.cjs +1192 -0
- package/dist/live-fixture.cjs.map +1 -0
- package/dist/live-fixture.js +1187 -0
- package/dist/live-fixture.js.map +1 -0
- package/dist/live-recorder.cjs +256 -0
- package/dist/live-recorder.cjs.map +1 -0
- package/dist/live-recorder.js +254 -0
- package/dist/live-recorder.js.map +1 -0
- package/dist/live-replay.cjs +286 -0
- package/dist/live-replay.cjs.map +1 -0
- package/dist/live-replay.js +285 -0
- package/dist/live-replay.js.map +1 -0
- package/dist/live-sanitize.cjs +196 -0
- package/dist/live-sanitize.cjs.map +1 -0
- package/dist/live-sanitize.js +196 -0
- package/dist/live-sanitize.js.map +1 -0
- package/dist/live-types.d.cts +71 -0
- package/dist/live-types.d.cts.map +1 -0
- package/dist/live-types.d.ts +71 -0
- package/dist/live-types.d.ts.map +1 -0
- package/dist/llmock.cjs +132 -49
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +51 -5
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +51 -5
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +133 -50
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +216 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -0
- package/dist/mcp-fakes-mount.js +210 -0
- package/dist/mcp-fakes-mount.js.map +1 -0
- package/dist/mcp-fakes.cjs +1619 -0
- package/dist/mcp-fakes.cjs.map +1 -0
- package/dist/mcp-fakes.d.cts +101 -0
- package/dist/mcp-fakes.d.cts.map +1 -0
- package/dist/mcp-fakes.d.ts +101 -0
- package/dist/mcp-fakes.d.ts.map +1 -0
- package/dist/mcp-fakes.js +1614 -0
- package/dist/mcp-fakes.js.map +1 -0
- package/dist/mcp-handler.cjs +331 -17
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +330 -18
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +217 -49
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +66 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +66 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +218 -50
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-stub.cjs +5 -1
- package/dist/mcp-stub.d.cts +3 -1
- package/dist/mcp-stub.d.ts +3 -1
- package/dist/mcp-stub.js +3 -1
- package/dist/mcp-types.d.cts +6 -0
- package/dist/mcp-types.d.ts +6 -0
- package/dist/message-text.cjs +192 -0
- package/dist/message-text.cjs.map +1 -0
- package/dist/message-text.d.cts +31 -0
- package/dist/message-text.d.cts.map +1 -0
- package/dist/message-text.d.ts +31 -0
- package/dist/message-text.d.ts.map +1 -0
- package/dist/message-text.js +184 -0
- package/dist/message-text.js.map +1 -0
- package/dist/messages.cjs +49 -7
- 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 +51 -9
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +278 -45
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts +17 -7
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts +17 -7
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +270 -29
- package/dist/metrics.js.map +1 -1
- package/dist/moderation.cjs +10 -1
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts.map +1 -1
- package/dist/moderation.js +10 -1
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +10 -6
- 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 +11 -7
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +3405 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.d.cts +30 -0
- package/dist/openapi.d.cts.map +1 -0
- package/dist/openapi.d.ts +30 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +3404 -0
- package/dist/openapi.js.map +1 -0
- package/dist/openrouter-video.cjs +9 -9
- 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 +10 -10
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +54 -10
- 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 +52 -12
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +47 -7
- 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 +48 -9
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +951 -0
- package/dist/route-registry.cjs.map +1 -0
- package/dist/route-registry.d.cts +33 -0
- package/dist/route-registry.d.cts.map +1 -0
- package/dist/route-registry.d.ts +33 -0
- package/dist/route-registry.d.ts.map +1 -0
- package/dist/route-registry.js +866 -0
- package/dist/route-registry.js.map +1 -0
- package/dist/router.cjs +21 -34
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts +4 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +21 -35
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +1252 -777
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1185 -710
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +6 -4
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.js +7 -5
- package/dist/speech.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +6 -6
- 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 +7 -7
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs +18 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.cts +320 -17
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +320 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -0
- package/dist/validate-cli.cjs +983 -0
- package/dist/validate-cli.cjs.map +1 -0
- package/dist/validate-cli.js +982 -0
- package/dist/validate-cli.js.map +1 -0
- package/dist/vector-stores.cjs +1180 -0
- package/dist/vector-stores.cjs.map +1 -0
- package/dist/vector-stores.d.cts +106 -0
- package/dist/vector-stores.d.cts.map +1 -0
- package/dist/vector-stores.d.ts +106 -0
- package/dist/vector-stores.d.ts.map +1 -0
- package/dist/vector-stores.js +1164 -0
- package/dist/vector-stores.js.map +1 -0
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +13 -13
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +14 -14
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +23 -4
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +24 -5
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +23 -11
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +5 -2
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +5 -2
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +25 -13
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +2 -1
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +2 -1
- package/dist/watcher.js.map +1 -1
- package/dist/ws-framing.cjs +150 -35
- package/dist/ws-framing.cjs.map +1 -1
- package/dist/ws-framing.d.cts +18 -3
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +18 -3
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-framing.js +151 -36
- package/dist/ws-framing.js.map +1 -1
- package/dist/ws-gemini-live.cjs +2 -2
- package/dist/ws-gemini-live.js +2 -2
- package/dist/ws-live.cjs +401 -0
- package/dist/ws-live.cjs.map +1 -0
- package/dist/ws-live.js +400 -0
- package/dist/ws-live.js.map +1 -0
- package/dist/ws-realtime.cjs +46 -2
- 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 +46 -2
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +16 -4
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +16 -4
- package/dist/ws-responses.js.map +1 -1
- package/dist/ws-upstream.cjs +118 -0
- package/dist/ws-upstream.cjs.map +1 -0
- package/dist/ws-upstream.js +115 -0
- package/dist/ws-upstream.js.map +1 -0
- package/fixtures/openai-live-client.json +390 -0
- package/fixtures/openai-live-managed.json +709 -0
- package/package.json +4 -1
package/dist/server.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { DEFAULT_TEST_ID } from "./constants.js";
|
|
2
|
+
import { build, msg, quote } from "./message-text.js";
|
|
2
3
|
import { markAuthenticatedRequest, resolveInboundAuth, validateRequestApiKey, writeApiKeyHttpRejection, writeApiKeyUpgradeRejection } from "./api-key-auth.js";
|
|
3
|
-
import { buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, readBody,
|
|
4
|
+
import { RequestBodyTooLargeError, buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, describeMatch, extractOverrides, flattenHeaders, getContext, getTestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, markMintedRequestId, readBody, readBodyBufferBounded, resolveFixtureBlockOutcome, resolveReasoningForModel, resolveRequestId, resolveResponse, resolveStrictMode, resolveTestId, resolveUsage, serializeErrorResponse, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, validateToolsField, wouldProxyMiss } from "./helpers.js";
|
|
5
|
+
import { normalizeLiveOptions } from "./live-fixture.js";
|
|
6
|
+
import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
|
|
7
|
+
import { CHAOS_FIELDS, CHAOS_FIELD_NAMES, applyChaosAction, awaitChaosLatency, describeUnwritableReason, evaluateChaos, isChaosScope, parseChaosField, resetChaosWarnings, resolveChaosConfig, responseGoneReason } from "./chaos.js";
|
|
8
|
+
import { FixtureLoadError, INJECTED_STATUS_RANGE, claimOneShotError, clearFixtureQueue, entryToFixture, isInjectableStatus, isOneShotError, queueOneShotError, releaseOneShotError, validateFixtures } from "./fixture-loader.js";
|
|
9
|
+
import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
|
|
10
|
+
import { proxyAndRecord } from "./recorder.js";
|
|
4
11
|
import { Logger } from "./logger.js";
|
|
5
12
|
import { Journal } from "./journal.js";
|
|
6
|
-
import { matchFixtureDiagnostic, recordMatchOptions } from "./router.js";
|
|
7
|
-
import { INJECTED_STATUS_RANGE, entryToFixture, isInjectableStatus, validateFixtures } from "./fixture-loader.js";
|
|
8
|
-
import { writeErrorResponse, writeSSEStream } from "./sse-writer.js";
|
|
9
13
|
import { createInterruptionSignal } from "./interruption.js";
|
|
10
14
|
import { buildOpenRouterCandidates, handleOpenRouterCredits, handleOpenRouterKey, handleOpenRouterModels, isOpenRouterPath, resolveOpenRouterShaping, serializeOpenRouterError, shapeOpenRouterChunks, shapeOpenRouterCompletion } from "./openrouter-chat.js";
|
|
11
|
-
import { applyChaosAction, evaluateChaos, isChaosScope } from "./chaos.js";
|
|
12
|
-
import { proxyAndRecord } from "./recorder.js";
|
|
13
15
|
import { handleResponses } from "./responses.js";
|
|
14
16
|
import { handleMessages } from "./messages.js";
|
|
15
17
|
import { handleGemini } from "./gemini.js";
|
|
@@ -23,51 +25,38 @@ import { handleImageEdit, handleImageVariations, handleImages } from "./images.j
|
|
|
23
25
|
import { handleSpeech } from "./speech.js";
|
|
24
26
|
import { VideoStateMap, handleVideoCreate } from "./video.js";
|
|
25
27
|
import { falJobs, handleFalQueue } from "./fal-audio.js";
|
|
26
|
-
import { falQueueStates, handleFal } from "./fal.js";
|
|
28
|
+
import { falQueueStates, falWillHandle, handleFal } from "./fal.js";
|
|
27
29
|
import { OPENROUTER_VIDEO_DEFAULT_MAX_CONTENT_BYTES, OpenRouterVideoJobMap, handleOpenRouterVideoContent, handleOpenRouterVideoCreate, handleOpenRouterVideoModels, handleOpenRouterVideoStatus } from "./openrouter-video.js";
|
|
28
30
|
import { VeoVideoJobMap, handleVeoVideoCreate, handleVeoVideoStatus } from "./veo-video.js";
|
|
29
31
|
import { GrokVideoJobMap, handleGrokVideoCreate, handleGrokVideoStatus } from "./grok-video.js";
|
|
30
32
|
import { BytePlusVideoJobMap, handleBytePlusVideoCreate, handleBytePlusVideoStatus } from "./byteplus-video.js";
|
|
31
33
|
import { handleElevenLabsAudio, handleElevenLabsTTS } from "./elevenlabs-audio.js";
|
|
34
|
+
import { clearElevenLabsVoices, handleElevenLabsVoiceCreate, handleElevenLabsVoiceDelete, handleElevenLabsVoiceDesign, handleElevenLabsVoiceGet } from "./elevenlabs-voice.js";
|
|
32
35
|
import { handleOllama, handleOllamaEmbeddings, handleOllamaGenerate } from "./ollama.js";
|
|
33
36
|
import { handleCohere, handleCohereEmbed } from "./cohere.js";
|
|
34
37
|
import { handleSearch } from "./search.js";
|
|
35
38
|
import { handleRerank } from "./rerank.js";
|
|
36
39
|
import { handleModeration } from "./moderation.js";
|
|
40
|
+
import { clearFineTuningStore, handleFineTuningCancel, handleFineTuningCreate, handleFineTuningEvents, handleFineTuningList, handleFineTuningRetrieve } from "./fine-tuning.js";
|
|
41
|
+
import { clearVectorStoreStore, handleVectorFileBatchesCancel, handleVectorFileBatchesCreate, handleVectorFileBatchesFiles, handleVectorFileBatchesRetrieve, handleVectorStoreFilesCreate, handleVectorStoreFilesDelete, handleVectorStoreFilesList, handleVectorStoreFilesRetrieve, handleVectorStoresCreate, handleVectorStoresDelete, handleVectorStoresList, handleVectorStoresModify, handleVectorStoresRetrieve, handleVectorStoresSearch } from "./vector-stores.js";
|
|
42
|
+
import { FILES_BODY_DRAIN_MAX_BYTES, FILES_BODY_MAX_BYTES, FILES_BODY_OVERSIZED, clearFileStore, handleFilesContent, handleFilesCreate, handleFilesDelete, handleFilesList, handleFilesRetrieve } from "./files.js";
|
|
43
|
+
import { clearBatchStore, handleBatchesCancel, handleBatchesCreate, handleBatchesList, handleBatchesRetrieve } from "./batches.js";
|
|
37
44
|
import { upgradeToWebSocket } from "./ws-framing.js";
|
|
45
|
+
import { handleLiveSession } from "./ws-live.js";
|
|
38
46
|
import { handleWebSocketResponses } from "./ws-responses.js";
|
|
39
47
|
import { handleWebSocketRealtime } from "./ws-realtime.js";
|
|
40
48
|
import { handleWebSocketGeminiLive } from "./ws-gemini-live.js";
|
|
41
|
-
import { BYTEPLUS_VIDEO_STATUS_RE, BYTEPLUS_VIDEO_SUBMIT_RE, GROK_VIDEO_STATUS_RE, GROK_VIDEO_SUBMIT_PATH, OPENROUTER_VIDEO_CONTENT_RE, OPENROUTER_VIDEO_STATUS_RE, VEO_OPERATION_RE, VEO_PREDICT_LRO_RE,
|
|
49
|
+
import { AZURE_DEPLOYMENT_RE, BATCHES_CANCEL_RE, BATCHES_ID_RE, BATCHES_PATH, BEDROCK_CONVERSE_RE, BEDROCK_CONVERSE_STREAM_RE, BEDROCK_INVOKE_RE, BEDROCK_STREAM_RE, BYTEPLUS_VIDEO_STATUS_RE, BYTEPLUS_VIDEO_SUBMIT_RE, COHERE_CHAT_PATH, COHERE_EMBED_PATH, COMPLETIONS_PATH, CONTROL_PREFIX, ELEVENLABS_MUSIC_RE, ELEVENLABS_SOUND_GENERATION_PATH, ELEVENLABS_TTS_RE, ELEVENLABS_VOICE_CREATE_PATH, ELEVENLABS_VOICE_DESIGN_PATH, ELEVENLABS_VOICE_RE, EMBEDDINGS_PATH, FAL_QUEUE_REQUESTS_RE, FAL_QUEUE_SUBMIT_RE, FAL_ROUTE_RE, FAL_RUN_RE, FILES_CONTENT_RE, FILES_ID_RE, FILES_PATH, FINE_TUNING_CANCEL_RE, FINE_TUNING_EVENTS_RE, FINE_TUNING_ID_RE, FINE_TUNING_JOBS_PATH, GEMINI_EMBED_RE, GEMINI_INTERACTIONS_PATH, GEMINI_LIVE_PATH, GEMINI_PATH_RE, GEMINI_PREDICT_RE, GROK_VIDEO_STATUS_RE, GROK_VIDEO_SUBMIT_PATH, HEALTH_PATH, IMAGES_EDIT_PATH, IMAGES_PATH, IMAGES_VARIATIONS_PATH, LIVE_PATH, MESSAGES_PATH, METRICS_PATH, MODELS_PATH, MODERATIONS_PATH, OLLAMA_CHAT_PATH, OLLAMA_EMBEDDINGS_PATH, OLLAMA_EMBED_PATH, OLLAMA_GENERATE_PATH, OLLAMA_TAGS_PATH, OPENROUTER_CREDITS_PATH, OPENROUTER_KEY_PATH, OPENROUTER_MODELS_PATH, OPENROUTER_VIDEOS_PATH, OPENROUTER_VIDEO_CONTENT_RE, OPENROUTER_VIDEO_MODELS_PATH, OPENROUTER_VIDEO_STATUS_RE, READY_PATH, REALTIME_PATH, REQUESTS_PATH, RERANK_PATH, RESPONSES_PATH, SEARCH_PATH, SPEECH_PATH, TRANSCRIPTIONS_PATH, TRANSLATIONS_PATH, VECTOR_STORES_BATCH_CANCEL_RE, VECTOR_STORES_BATCH_FILES_RE, VECTOR_STORES_BATCH_RE, VECTOR_STORES_FILES_RE, VECTOR_STORES_FILE_BATCHES_RE, VECTOR_STORES_FILE_RE, VECTOR_STORES_ID_RE, VECTOR_STORES_PATH, VECTOR_STORES_SEARCH_RE, VEO_OPERATION_RE, VEO_PREDICT_LRO_RE, VERTEX_AI_RE, VIDEOS_PATH, matchRouteDefinition } from "./route-registry.js";
|
|
50
|
+
import { CATALOG_ROUTES, buildOpenApiDocument } from "./openapi.js";
|
|
51
|
+
import { DESTROYED_STATUS_LABEL, createMetricsRegistry, normalizePathLabel } from "./metrics.js";
|
|
52
|
+
import { MCP_FAKES_DEFAULT_MOUNT, McpFakeStore, McpFakesAddError, blockIdOf } from "./mcp-fakes.js";
|
|
53
|
+
import { MCPMock } from "./mcp-mock.js";
|
|
54
|
+
import { ensureFakeMount, findFakeMount, handOffMcpFakes, isFakeMount, mountConflictDetail } from "./mcp-fakes-mount.js";
|
|
42
55
|
import * as http$1 from "node:http";
|
|
56
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
43
57
|
|
|
44
58
|
//#region src/server.ts
|
|
45
|
-
const
|
|
46
|
-
const RESPONSES_PATH = "/v1/responses";
|
|
47
|
-
const REALTIME_PATH = "/v1/realtime";
|
|
48
|
-
const GEMINI_LIVE_PATH = "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
|
|
49
|
-
const MESSAGES_PATH = "/v1/messages";
|
|
50
|
-
const EMBEDDINGS_PATH = "/v1/embeddings";
|
|
51
|
-
const COHERE_CHAT_PATH = "/v2/chat";
|
|
52
|
-
const COHERE_EMBED_PATH = "/v2/embed";
|
|
53
|
-
const SEARCH_PATH = "/search";
|
|
54
|
-
const RERANK_PATH = "/v2/rerank";
|
|
55
|
-
const MODERATIONS_PATH = "/v1/moderations";
|
|
56
|
-
const IMAGES_PATH = "/v1/images/generations";
|
|
57
|
-
const IMAGES_EDIT_PATH = "/v1/images/edits";
|
|
58
|
-
const IMAGES_VARIATIONS_PATH = "/v1/images/variations";
|
|
59
|
-
const SPEECH_PATH = "/v1/audio/speech";
|
|
60
|
-
const TRANSCRIPTIONS_PATH = "/v1/audio/transcriptions";
|
|
61
|
-
const TRANSLATIONS_PATH = "/v1/audio/translations";
|
|
62
|
-
const VIDEOS_PATH = "/v1/videos";
|
|
63
|
-
const GEMINI_PREDICT_RE = /^\/v1beta\/models\/([^:]+):predict$/;
|
|
64
|
-
const ELEVENLABS_SOUND_GENERATION_PATH = "/v1/sound-generation";
|
|
65
|
-
const ELEVENLABS_TTS_RE = /^\/v1\/text-to-speech\/([^/]+)$/;
|
|
66
|
-
const ELEVENLABS_MUSIC_RE = /^\/v1\/music(?:\/(.+))?$/;
|
|
67
|
-
const FAL_QUEUE_SUBMIT_RE = /^\/fal\/queue\/submit\/(.+)$/;
|
|
68
|
-
const FAL_QUEUE_REQUESTS_RE = /^\/fal\/queue\/requests\/(.+)$/;
|
|
69
|
-
const FAL_RUN_RE = /^\/fal\/run\/(.+)$/;
|
|
70
|
-
const FAL_PREFIX_RE = /^\/fal(?:\/.*)?$/;
|
|
59
|
+
const liveClosers = /* @__PURE__ */ new WeakMap();
|
|
71
60
|
const DEFAULT_CHUNK_SIZE = 20;
|
|
72
61
|
const COMPAT_SUFFIXES = [
|
|
73
62
|
"/chat/completions",
|
|
@@ -96,26 +85,6 @@ function normalizeCompatPath(pathname, logger) {
|
|
|
96
85
|
}
|
|
97
86
|
return pathname;
|
|
98
87
|
}
|
|
99
|
-
const GEMINI_INTERACTIONS_PATH = "/v1beta/interactions";
|
|
100
|
-
const GEMINI_PATH_RE = /^\/v1beta\/models\/([^:]+):(generateContent|streamGenerateContent)$/;
|
|
101
|
-
const GEMINI_EMBED_RE = /^\/v1beta\/models\/([^:]+):embedContent$/;
|
|
102
|
-
const AZURE_DEPLOYMENT_RE = /^\/openai\/deployments\/([^/]+)\/(chat\/completions|embeddings)$/;
|
|
103
|
-
const BEDROCK_INVOKE_RE = /^\/model\/([^/]+)\/invoke$/;
|
|
104
|
-
const BEDROCK_STREAM_RE = /^\/model\/([^/]+)\/invoke-with-response-stream$/;
|
|
105
|
-
const BEDROCK_CONVERSE_RE = /^\/model\/([^/]+)\/converse$/;
|
|
106
|
-
const BEDROCK_CONVERSE_STREAM_RE = /^\/model\/([^/]+)\/converse-stream$/;
|
|
107
|
-
const VERTEX_AI_RE = /^\/v1\/projects\/[^/]+\/locations\/[^/]+\/publishers\/google\/models\/([^/:]+):(generateContent|streamGenerateContent)$/;
|
|
108
|
-
const OLLAMA_CHAT_PATH = "/api/chat";
|
|
109
|
-
const OLLAMA_GENERATE_PATH = "/api/generate";
|
|
110
|
-
const OLLAMA_EMBEDDINGS_PATH = "/api/embeddings";
|
|
111
|
-
const OLLAMA_EMBED_PATH = "/api/embed";
|
|
112
|
-
const OLLAMA_TAGS_PATH = "/api/tags";
|
|
113
|
-
const OPENROUTER_VIDEOS_PATH = "/api/v1/videos";
|
|
114
|
-
const OPENROUTER_VIDEO_MODELS_PATH = "/api/v1/videos/models";
|
|
115
|
-
const HEALTH_PATH = "/health";
|
|
116
|
-
const READY_PATH = "/ready";
|
|
117
|
-
const MODELS_PATH = "/v1/models";
|
|
118
|
-
const REQUESTS_PATH = "/v1/_requests";
|
|
119
88
|
const DEFAULT_MODELS = [
|
|
120
89
|
"gpt-4",
|
|
121
90
|
"gpt-4o",
|
|
@@ -125,9 +94,9 @@ const DEFAULT_MODELS = [
|
|
|
125
94
|
];
|
|
126
95
|
const CORS_HEADERS = {
|
|
127
96
|
"Access-Control-Allow-Origin": "*",
|
|
128
|
-
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
97
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
129
98
|
"Access-Control-Allow-Headers": "*",
|
|
130
|
-
"Access-Control-Expose-Headers": "X-Total-Count"
|
|
99
|
+
"Access-Control-Expose-Headers": "X-Total-Count, X-Request-Id"
|
|
131
100
|
};
|
|
132
101
|
function setCorsHeaders(res) {
|
|
133
102
|
for (const [key, value] of Object.entries(CORS_HEADERS)) res.setHeader(key, value);
|
|
@@ -144,7 +113,6 @@ function handleNotFound(res, message) {
|
|
|
144
113
|
type: "not_found"
|
|
145
114
|
} }));
|
|
146
115
|
}
|
|
147
|
-
const CONTROL_PREFIX = "/__aimock";
|
|
148
116
|
/** The complete `GET /__aimock/fixtures` query-param vocabulary; anything else 400s. */
|
|
149
117
|
const FIXTURES_PARAMS = new Set(["include"]);
|
|
150
118
|
/** The complete `GET /__aimock/journal` query-param vocabulary; anything else 400s. */
|
|
@@ -155,14 +123,18 @@ const JOURNAL_PARAMS = new Set([
|
|
|
155
123
|
"method",
|
|
156
124
|
"status",
|
|
157
125
|
"service",
|
|
158
|
-
"testId"
|
|
126
|
+
"testId",
|
|
127
|
+
"requestId"
|
|
159
128
|
]);
|
|
160
129
|
/**
|
|
161
130
|
* Perform a full reset: clear the fixtures array, the journal (entries *and*
|
|
162
131
|
* per-test fixture match-counts, i.e. sequence position), the video and fal.ai
|
|
163
|
-
* job/queue state, the
|
|
164
|
-
*
|
|
165
|
-
*
|
|
132
|
+
* job/queue state, the fine-tuning store (jobs, their append-only event logs,
|
|
133
|
+
* their poll counts, the create-time suffixes their model names are built from,
|
|
134
|
+
* and the module's monotonic clock), the ElevenLabs Voice Design store, the
|
|
135
|
+
* Gemini interaction/event-id counters, and any runtime chaos override
|
|
136
|
+
* (reverting to the construction-time chaos config), then re-zero the
|
|
137
|
+
* `aimock_fixtures_loaded` gauge.
|
|
166
138
|
*
|
|
167
139
|
* `targets` is `null` when no server is running (an in-process `reset()` before
|
|
168
140
|
* `start()`). The process-global generation state is reset either way, since it
|
|
@@ -172,19 +144,27 @@ const JOURNAL_PARAMS = new Set([
|
|
|
172
144
|
* `POST /__aimock/reset/fixtures` alias, and `LLMock.reset()`.
|
|
173
145
|
*/
|
|
174
146
|
function performFullReset(fixtures, targets) {
|
|
175
|
-
fixtures
|
|
147
|
+
clearFixtureQueue(fixtures);
|
|
176
148
|
falJobs.clear();
|
|
177
149
|
falQueueStates.clear();
|
|
150
|
+
clearBatchStore();
|
|
151
|
+
clearElevenLabsVoices();
|
|
152
|
+
clearFileStore();
|
|
153
|
+
clearFineTuningStore();
|
|
154
|
+
clearVectorStoreStore();
|
|
178
155
|
resetInteractionCounter();
|
|
179
156
|
resetEventIdCounter();
|
|
180
157
|
if (!targets) return;
|
|
158
|
+
liveClosers.get(targets.defaults)?.();
|
|
181
159
|
targets.journal.clear();
|
|
160
|
+
resetChaosWarnings(targets.defaults.logger);
|
|
182
161
|
targets.defaults.chaos = void 0;
|
|
183
162
|
targets.videoStates.clear();
|
|
184
163
|
targets.openRouterVideoJobs.clear();
|
|
185
164
|
targets.veoVideoJobs.clear();
|
|
186
165
|
targets.grokVideoJobs.clear();
|
|
187
166
|
targets.bytePlusVideoJobs.clear();
|
|
167
|
+
for (const { handler } of targets.mounts ?? []) handler.clearMcpFakes?.();
|
|
188
168
|
if (targets.defaults.registry) targets.defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
|
|
189
169
|
}
|
|
190
170
|
/**
|
|
@@ -230,22 +210,23 @@ function fixtureResponseKind(response) {
|
|
|
230
210
|
return "unknown";
|
|
231
211
|
}
|
|
232
212
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
213
|
+
* The testId a journal entry was scoped under — `resolveTestId` applied to the
|
|
214
|
+
* journaled headers and path, i.e. the SAME function that scoped the request
|
|
215
|
+
* (fixture match-counts, chaos) when it arrived. Node folds a repeated
|
|
216
|
+
* `X-Test-Id` into one string ("a, a") before either side sees it, and the
|
|
217
|
+
* request is counted and chaos-evaluated under that folded string, so the
|
|
218
|
+
* `?testId=` filter matches it verbatim too: no token-splitting here that the
|
|
219
|
+
* scoping path does not also do. (Splitting on "," additionally broke every
|
|
220
|
+
* legitimately comma-bearing id.) A raw `path.includes("testId=t1")` both
|
|
221
|
+
* prefix-collides with `t10` and matches unrelated params like `notTestId`.
|
|
222
|
+
*
|
|
223
|
+
* An MCP mount records the test id it resolved on the entry (I9, B3): a
|
|
224
|
+
* decoded header or query value, or the one bound to the session at
|
|
225
|
+
* `initialize`. That value wins; LLM entries carry none and are attributed as
|
|
226
|
+
* before.
|
|
239
227
|
*/
|
|
240
228
|
function journalEntryTestId(entry) {
|
|
241
|
-
|
|
242
|
-
if (header) return header;
|
|
243
|
-
const qIdx = entry.path.indexOf("?");
|
|
244
|
-
if (qIdx !== -1) {
|
|
245
|
-
const queryValue = new URLSearchParams(entry.path.slice(qIdx + 1)).get("testId");
|
|
246
|
-
if (queryValue) return queryValue;
|
|
247
|
-
}
|
|
248
|
-
return DEFAULT_TEST_ID;
|
|
229
|
+
return entry.testId ?? resolveTestId(entry.headers, entry.path);
|
|
249
230
|
}
|
|
250
231
|
/**
|
|
251
232
|
* The testId a chaos override is stored under, and read back with — resolved by
|
|
@@ -272,27 +253,198 @@ function writeBlankTestId(res) {
|
|
|
272
253
|
res.end(JSON.stringify({ error: "Invalid 'X-Test-Id': header is present but empty" }));
|
|
273
254
|
return true;
|
|
274
255
|
}
|
|
275
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* The chaos config in effect for one testId: its override, else the baseline —
|
|
258
|
+
* one or the other, never the two merged. Mirrors `resolveScopedDefaults` in
|
|
259
|
+
* chaos.ts, so what `GET /__aimock/chaos` reports for a scope is exactly what
|
|
260
|
+
* that scope's traffic is evaluated against.
|
|
261
|
+
*/
|
|
276
262
|
function effectiveChaos(defaults, scopeId) {
|
|
277
|
-
|
|
278
|
-
if (scoped) return scoped;
|
|
263
|
+
if (defaults.chaosByTestId?.has(scopeId)) return defaults.chaosByTestId.get(scopeId) ?? {};
|
|
279
264
|
const current = defaults.chaos;
|
|
280
265
|
if (!current) return {};
|
|
281
266
|
return isChaosScope(current) ? current.base ?? {} : current;
|
|
282
267
|
}
|
|
268
|
+
/** The `GET /__aimock/mcp/fakes` query-param vocabulary; anything else 400s. */
|
|
269
|
+
const MCP_FAKES_PARAMS = new Set([
|
|
270
|
+
"testId",
|
|
271
|
+
"context",
|
|
272
|
+
"mount"
|
|
273
|
+
]);
|
|
274
|
+
/** A block's `mount` path, read without running getters; the add validates it. */
|
|
275
|
+
function controlFakeMountOf(raw) {
|
|
276
|
+
if (typeof raw !== "object" || raw === null) return MCP_FAKES_DEFAULT_MOUNT;
|
|
277
|
+
const value = Object.getOwnPropertyDescriptor(raw, "mount")?.value;
|
|
278
|
+
return typeof value === "string" ? value : MCP_FAKES_DEFAULT_MOUNT;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Check one `POST /__aimock/fixtures` `mcpFakes` value against the live
|
|
282
|
+
* mounts (F10, W6) without changing anything. Returns every error in input
|
|
283
|
+
* order (an empty list when the value can be added), and `apply`, which adds
|
|
284
|
+
* the blocks (auto-mounting an MCPMock where no mount serves a block's path)
|
|
285
|
+
* and returns the number of blocks added.
|
|
286
|
+
*
|
|
287
|
+
* The whole value is checked before any mount is touched, and the check
|
|
288
|
+
* never adds to a real mount: each group is added to a throwaway store,
|
|
289
|
+
* whose errors are the ones reported (it numbers the add `control-api#1`, as
|
|
290
|
+
* a new mount would; for an existing MCPMock the rejected group is re-run on
|
|
291
|
+
* its atomic add, so the errors name that mount's next `<n>`, I7). A group
|
|
292
|
+
* whose path no request to it can reach (a mount that is not an MCPMock
|
|
293
|
+
* serves it, an MCPMock answers it as its root, or it is under the control
|
|
294
|
+
* prefix) is a mount conflict (one error per block).
|
|
295
|
+
*/
|
|
296
|
+
function planControlApiFakes(raw, mounts) {
|
|
297
|
+
const items = Array.isArray(raw) ? raw.map((block, position) => ({
|
|
298
|
+
position,
|
|
299
|
+
source: {
|
|
300
|
+
source: "control-api",
|
|
301
|
+
blockIndex: position,
|
|
302
|
+
raw: block
|
|
303
|
+
}
|
|
304
|
+
})) : [{
|
|
305
|
+
position: 0,
|
|
306
|
+
source: {
|
|
307
|
+
source: "control-api",
|
|
308
|
+
blockIndex: null,
|
|
309
|
+
raw
|
|
310
|
+
}
|
|
311
|
+
}];
|
|
312
|
+
const groups = /* @__PURE__ */ new Map();
|
|
313
|
+
if (items.length === 0) groups.set(MCP_FAKES_DEFAULT_MOUNT, {
|
|
314
|
+
path: MCP_FAKES_DEFAULT_MOUNT,
|
|
315
|
+
positions: [],
|
|
316
|
+
sources: []
|
|
317
|
+
});
|
|
318
|
+
for (const { position, source } of items) {
|
|
319
|
+
const path = controlFakeMountOf(source.raw);
|
|
320
|
+
const group = groups.get(path) ?? {
|
|
321
|
+
path,
|
|
322
|
+
positions: [],
|
|
323
|
+
sources: []
|
|
324
|
+
};
|
|
325
|
+
group.positions.push(position);
|
|
326
|
+
group.sources.push(source);
|
|
327
|
+
groups.set(path, group);
|
|
328
|
+
}
|
|
329
|
+
const found = [];
|
|
330
|
+
/** The paths `apply` auto-mounts, in order. */
|
|
331
|
+
const pending = [];
|
|
332
|
+
for (const group of groups.values()) {
|
|
333
|
+
const target = findFakeMount(mounts, group.path, pending);
|
|
334
|
+
if (target.kind === "conflict") {
|
|
335
|
+
group.positions.forEach((position) => {
|
|
336
|
+
const where = Array.isArray(raw) ? msg`mcpFakes[${position}]` : msg`mcpFakes`;
|
|
337
|
+
found.push({
|
|
338
|
+
position,
|
|
339
|
+
error: new FixtureLoadError({
|
|
340
|
+
rule: "mcp-fakes/mount-conflict",
|
|
341
|
+
file: null,
|
|
342
|
+
blockId: null,
|
|
343
|
+
entryId: null,
|
|
344
|
+
detail: msg`${where}: ${mountConflictDetail(group.path, target)}`
|
|
345
|
+
})
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
let failure = addFailure(() => new McpFakeStore().add(group.sources, { kind: "control-api" }));
|
|
351
|
+
if (target.kind === "none") pending.push(group.path);
|
|
352
|
+
if (!failure) continue;
|
|
353
|
+
if (target.kind === "mcp" && target.handler.addMcpFakes === MCPMock.prototype.addMcpFakes) {
|
|
354
|
+
const handler = target.handler;
|
|
355
|
+
failure = addFailure(() => handler.addMcpFakes?.(group.sources, { kind: "control-api" })) ?? failure;
|
|
356
|
+
}
|
|
357
|
+
for (const error of failure.errors) found.push({
|
|
358
|
+
position: positionOf(error, group),
|
|
359
|
+
error
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
found.sort((a, b) => a.position - b.position);
|
|
363
|
+
return {
|
|
364
|
+
errors: found.map((f) => f.error),
|
|
365
|
+
apply(wiring) {
|
|
366
|
+
let added = 0;
|
|
367
|
+
for (const group of groups.values()) {
|
|
368
|
+
const target = ensureFakeMount(mounts, group.path, {
|
|
369
|
+
journal: wiring.journal,
|
|
370
|
+
registry: wiring.defaults.registry,
|
|
371
|
+
logger: wiring.defaults.logger
|
|
372
|
+
}, wiring.defaults.logger);
|
|
373
|
+
if ("conflict" in target) continue;
|
|
374
|
+
const result = target.handler.addMcpFakes?.(group.sources, { kind: "control-api" });
|
|
375
|
+
for (const warning of result?.warnings ?? []) wiring.defaults.logger.warn(warning.message);
|
|
376
|
+
added += group.sources.length;
|
|
377
|
+
}
|
|
378
|
+
return added;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** The `McpFakesAddError` that `add` throws, or `null` when it adds. */
|
|
383
|
+
function addFailure(add) {
|
|
384
|
+
try {
|
|
385
|
+
add();
|
|
386
|
+
return null;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (err instanceof McpFakesAddError) return err;
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The request position of the block an add error names. The add numbers a
|
|
394
|
+
* run-time group's blocks by their place in the group (`[<j>]`), or gives no
|
|
395
|
+
* index to a single-object value; an error tied to no block sorts with the
|
|
396
|
+
* group's first block.
|
|
397
|
+
*/
|
|
398
|
+
function positionOf(error, group) {
|
|
399
|
+
const first = group.positions[0] ?? 0;
|
|
400
|
+
if (error.blockId == null || error.file === null) return first;
|
|
401
|
+
const at = group.positions.findIndex((_, j) => error.blockId === blockIdOf(error.file ?? "", "", j));
|
|
402
|
+
return at === -1 ? first : group.positions[at];
|
|
403
|
+
}
|
|
404
|
+
function hasFakesSnapshot(handler) {
|
|
405
|
+
return "fakesSnapshot" in handler && typeof handler.fakesSnapshot === "function";
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* `GET /__aimock/mcp/fakes` (spec 6.5, I9): for each MCP mount (or the one
|
|
409
|
+
* named by `mount`), the blocks that apply to the requested test id and
|
|
410
|
+
* context, with every entry id and its consumed state for that test id.
|
|
411
|
+
* A mount is listed when it takes fakes (it implements `addMcpFakes`, the
|
|
412
|
+
* check every other fakes path uses); one with no `fakesSnapshot` lists no
|
|
413
|
+
* blocks.
|
|
414
|
+
*/
|
|
415
|
+
function listControlApiFakes(mounts, testId, context, mount) {
|
|
416
|
+
const listed = mounts.flatMap(({ path, handler }) => isFakeMount(handler) && (mount === null || path === mount) ? [{
|
|
417
|
+
mount: path,
|
|
418
|
+
blocks: hasFakesSnapshot(handler) ? handler.fakesSnapshot(testId, context) : []
|
|
419
|
+
}] : []);
|
|
420
|
+
return mount !== null && listed.length === 0 ? null : listed;
|
|
421
|
+
}
|
|
283
422
|
/**
|
|
284
423
|
* Handle requests under `/__aimock/`. Returns `true` if the request was
|
|
285
424
|
* handled, `false` if the path doesn't match the control prefix.
|
|
286
425
|
*/
|
|
287
|
-
async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults) {
|
|
426
|
+
async function handleControlAPI(req, res, pathname, searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults, mounts) {
|
|
288
427
|
if (!pathname.startsWith(CONTROL_PREFIX)) return false;
|
|
289
|
-
const subPath = pathname.slice(
|
|
428
|
+
const subPath = pathname.slice(CONTROL_PREFIX.length);
|
|
290
429
|
setCorsHeaders(res);
|
|
291
430
|
if (subPath === "/health" && req.method === "GET") {
|
|
292
431
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
293
432
|
res.end(JSON.stringify({ status: "ok" }));
|
|
294
433
|
return true;
|
|
295
434
|
}
|
|
435
|
+
if (subPath === "/openapi.json" && req.method === "GET") {
|
|
436
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
437
|
+
res.end(JSON.stringify(buildOpenApiDocument()));
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (subPath === "/routes" && req.method === "GET") {
|
|
441
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
442
|
+
res.end(JSON.stringify({
|
|
443
|
+
count: CATALOG_ROUTES.length,
|
|
444
|
+
routes: CATALOG_ROUTES
|
|
445
|
+
}));
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
296
448
|
if (subPath === "/journal" && req.method === "GET") {
|
|
297
449
|
const bad = (message) => {
|
|
298
450
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -320,6 +472,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
320
472
|
const methodFilter = searchParams.get("method");
|
|
321
473
|
const serviceFilter = searchParams.get("service");
|
|
322
474
|
const testIdFilter = searchParams.get("testId");
|
|
475
|
+
const requestIdFilter = searchParams.get("requestId");
|
|
323
476
|
let entries = journal.getAll();
|
|
324
477
|
if (methodFilter !== null) {
|
|
325
478
|
const want = methodFilter.toUpperCase();
|
|
@@ -329,6 +482,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
329
482
|
if (statusFilter !== void 0) entries = entries.filter((e) => e.response.status === statusFilter);
|
|
330
483
|
if (serviceFilter !== null) entries = entries.filter((e) => e.service === serviceFilter);
|
|
331
484
|
if (testIdFilter !== null) entries = entries.filter((e) => journalEntryTestId(e) === testIdFilter);
|
|
485
|
+
if (requestIdFilter !== null) entries = entries.filter((e) => e.headers["x-request-id"] === requestIdFilter);
|
|
332
486
|
const total = entries.length;
|
|
333
487
|
if (offset > 0) entries = entries.slice(offset);
|
|
334
488
|
if (limit >= 0) entries = entries.slice(0, limit);
|
|
@@ -362,7 +516,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
362
516
|
fixtures: fixtures.map((fixture, index) => ({
|
|
363
517
|
index,
|
|
364
518
|
match: redactForInspection(fixture.match),
|
|
365
|
-
responseKind: fixtureResponseKind(fixture.response),
|
|
519
|
+
responseKind: isOneShotError(fixture) ? "error" : fixtureResponseKind(fixture.response),
|
|
366
520
|
...fixture.latency !== void 0 ? { latency: fixture.latency } : {},
|
|
367
521
|
...fixture.chaos !== void 0 ? { chaos: fixture.chaos } : {}
|
|
368
522
|
}))
|
|
@@ -412,32 +566,53 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
412
566
|
res.end(JSON.stringify({ error: "Invalid body: expected a JSON object" }));
|
|
413
567
|
return true;
|
|
414
568
|
}
|
|
415
|
-
const
|
|
416
|
-
"dropRate",
|
|
417
|
-
"malformedRate",
|
|
418
|
-
"disconnectRate"
|
|
419
|
-
];
|
|
420
|
-
for (const key of Object.keys(parsed)) if (!allowed.includes(key)) {
|
|
569
|
+
for (const key of Object.keys(parsed)) if (!CHAOS_FIELD_NAMES.includes(key)) {
|
|
421
570
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
422
571
|
res.end(JSON.stringify({ error: `Unknown chaos field: '${key}'` }));
|
|
423
572
|
return true;
|
|
424
573
|
}
|
|
425
574
|
const next = {};
|
|
426
|
-
for (const key of
|
|
575
|
+
for (const key of CHAOS_FIELD_NAMES) {
|
|
427
576
|
const value = parsed[key];
|
|
428
577
|
if (value === void 0) continue;
|
|
429
|
-
|
|
578
|
+
const accepted = typeof value === "number" ? parseChaosField(key, value) : void 0;
|
|
579
|
+
if (accepted === void 0) {
|
|
580
|
+
const shape = CHAOS_FIELDS[key].integer ? "a whole number of ms" : "a number";
|
|
430
581
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
431
|
-
res.end(JSON.stringify({ error: `Invalid '${key}': must be
|
|
582
|
+
res.end(JSON.stringify({ error: `Invalid '${key}': must be ${shape} between 0 and ${CHAOS_FIELDS[key].max}` }));
|
|
432
583
|
return true;
|
|
433
584
|
}
|
|
434
|
-
next[key] =
|
|
585
|
+
next[key] = accepted;
|
|
586
|
+
}
|
|
587
|
+
const shadowed = effectiveChaos(defaults, scopeId);
|
|
588
|
+
const droppedFields = Object.keys(shadowed).filter((key) => shadowed[key] !== void 0 && next[key] === void 0);
|
|
589
|
+
if (droppedFields.length > 0) {
|
|
590
|
+
const lost = droppedFields.map((key) => `${key}=${shadowed[key]}`).join(", ");
|
|
591
|
+
defaults.logger.warn(`[chaos] POST /__aimock/chaos (testId '${scopeId}') replaces the chaos config for this scope wholesale, it is not merged: ${lost} no longer applies here. Restate the field in the body to keep it.`);
|
|
435
592
|
}
|
|
436
593
|
defaults.chaosByTestId?.set(scopeId, next);
|
|
437
594
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
438
595
|
res.end(JSON.stringify({ chaos: effectiveChaos(defaults, scopeId) }));
|
|
439
596
|
return true;
|
|
440
597
|
}
|
|
598
|
+
if (subPath === "/mcp/fakes" && req.method === "GET") {
|
|
599
|
+
const reply = (status, body) => {
|
|
600
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
601
|
+
res.end(JSON.stringify(body));
|
|
602
|
+
return true;
|
|
603
|
+
};
|
|
604
|
+
for (const key of searchParams.keys()) if (!MCP_FAKES_PARAMS.has(key)) return reply(400, { error: `Unknown query parameter: '${key}'. Supported: ${[...MCP_FAKES_PARAMS].join(", ")}` });
|
|
605
|
+
const testId = searchParams.get("testId") || null;
|
|
606
|
+
const context = searchParams.get("context") || null;
|
|
607
|
+
const mount = searchParams.get("mount") || null;
|
|
608
|
+
const listed = listControlApiFakes(mounts, testId, context, mount);
|
|
609
|
+
if (listed === null) return reply(404, { error: build(msg`No MCP mount at ${quote(mount ?? "")}`) });
|
|
610
|
+
return reply(200, {
|
|
611
|
+
testId,
|
|
612
|
+
context,
|
|
613
|
+
mounts: listed
|
|
614
|
+
});
|
|
615
|
+
}
|
|
441
616
|
if (subPath === "/fixtures" && req.method === "POST") {
|
|
442
617
|
let raw;
|
|
443
618
|
try {
|
|
@@ -459,29 +634,60 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
459
634
|
res.end(JSON.stringify({ error: `Invalid JSON: ${msg}` }));
|
|
460
635
|
return true;
|
|
461
636
|
}
|
|
462
|
-
|
|
637
|
+
const withFakes = typeof parsed === "object" && parsed !== null && Object.hasOwn(parsed, "mcpFakes");
|
|
638
|
+
if (parsed === null || !Array.isArray(parsed.fixtures) && !(withFakes && parsed.fixtures === void 0)) {
|
|
463
639
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
464
640
|
res.end(JSON.stringify({ error: "Missing or invalid \"fixtures\" array" }));
|
|
465
641
|
return true;
|
|
466
642
|
}
|
|
467
|
-
const
|
|
643
|
+
const entries = Array.isArray(parsed.fixtures) ? parsed.fixtures : [];
|
|
644
|
+
const missingMatchIndex = entries.findIndex((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && entry.match == null);
|
|
645
|
+
if (missingMatchIndex !== -1) {
|
|
646
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
647
|
+
res.end(JSON.stringify({ error: `Fixture at index ${missingMatchIndex} is missing match` }));
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
const invalidMatchIndex = entries.findIndex((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) && entry.match != null && (typeof entry.match !== "object" || Array.isArray(entry.match)));
|
|
651
|
+
if (invalidMatchIndex !== -1) {
|
|
652
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
653
|
+
res.end(JSON.stringify({ error: `Fixture at index ${invalidMatchIndex}: match must be an object` }));
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
const converted = entries.map((e) => entryToFixture(e));
|
|
468
657
|
const errors = validateFixtures(converted).filter((i) => i.severity === "error");
|
|
469
|
-
|
|
658
|
+
const fakes = withFakes ? planControlApiFakes(parsed.mcpFakes, mounts) : null;
|
|
659
|
+
const fakeErrors = fakes?.errors ?? [];
|
|
660
|
+
const validationFailed = (details) => {
|
|
470
661
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
471
662
|
res.end(JSON.stringify({
|
|
472
663
|
error: "Validation failed",
|
|
473
|
-
details
|
|
664
|
+
details
|
|
474
665
|
}));
|
|
475
666
|
return true;
|
|
667
|
+
};
|
|
668
|
+
if (errors.length > 0 || fakeErrors.length > 0) return validationFailed([...errors, ...fakeErrors.map((e) => e.toJSON())]);
|
|
669
|
+
let mcpFakesAdded = 0;
|
|
670
|
+
if (fakes) try {
|
|
671
|
+
mcpFakesAdded = fakes.apply({
|
|
672
|
+
journal,
|
|
673
|
+
defaults
|
|
674
|
+
});
|
|
675
|
+
} catch (err) {
|
|
676
|
+
if (!(err instanceof McpFakesAddError)) throw err;
|
|
677
|
+
return validationFailed(err.errors.map((e) => e.toJSON()));
|
|
476
678
|
}
|
|
477
679
|
fixtures.push(...converted);
|
|
478
680
|
if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
|
|
479
681
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
480
|
-
res.end(JSON.stringify(
|
|
682
|
+
res.end(JSON.stringify(withFakes ? {
|
|
683
|
+
added: converted.length,
|
|
684
|
+
mcpFakesAdded
|
|
685
|
+
} : { added: converted.length }));
|
|
481
686
|
return true;
|
|
482
687
|
}
|
|
483
688
|
if (subPath === "/fixtures" && req.method === "DELETE") {
|
|
484
|
-
fixtures
|
|
689
|
+
clearFixtureQueue(fixtures);
|
|
690
|
+
for (const { handler } of mounts) handler.clearMcpFakes?.();
|
|
485
691
|
if (defaults.registry) defaults.registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
|
|
486
692
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
487
693
|
res.end(JSON.stringify({ cleared: true }));
|
|
@@ -494,7 +700,8 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
494
700
|
veoVideoJobs,
|
|
495
701
|
grokVideoJobs,
|
|
496
702
|
bytePlusVideoJobs,
|
|
497
|
-
defaults
|
|
703
|
+
defaults,
|
|
704
|
+
mounts
|
|
498
705
|
});
|
|
499
706
|
if (subPath === "/reset" && req.method === "POST") {
|
|
500
707
|
performFullReset(fixtures, resetTargets());
|
|
@@ -575,32 +782,7 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
575
782
|
}
|
|
576
783
|
}
|
|
577
784
|
}
|
|
578
|
-
|
|
579
|
-
match: { predicate: () => true },
|
|
580
|
-
response: {
|
|
581
|
-
error: {
|
|
582
|
-
message: errorBody?.message ?? "Injected error",
|
|
583
|
-
type: errorBody?.type ?? "server_error",
|
|
584
|
-
code: errorBody?.code
|
|
585
|
-
},
|
|
586
|
-
status
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
fixtures.unshift(errorFixture);
|
|
590
|
-
let consumed = false;
|
|
591
|
-
const original = errorFixture.match.predicate;
|
|
592
|
-
errorFixture.match.predicate = (req) => {
|
|
593
|
-
if (consumed) return false;
|
|
594
|
-
const result = original(req);
|
|
595
|
-
if (result) {
|
|
596
|
-
consumed = true;
|
|
597
|
-
queueMicrotask(() => {
|
|
598
|
-
const idx = fixtures.indexOf(errorFixture);
|
|
599
|
-
if (idx !== -1) fixtures.splice(idx, 1);
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
return result;
|
|
603
|
-
};
|
|
785
|
+
queueOneShotError(fixtures, status, errorBody ?? void 0);
|
|
604
786
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
605
787
|
res.end(JSON.stringify({ queued: true }));
|
|
606
788
|
return true;
|
|
@@ -608,6 +790,19 @@ async function handleControlAPI(req, res, pathname, searchParams, fixtures, jour
|
|
|
608
790
|
handleNotFound(res, `Unknown control endpoint: ${pathname}`);
|
|
609
791
|
return true;
|
|
610
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* The client tore the connection down before its request body had fully
|
|
795
|
+
* arrived. Node rejects the pending body read with `aborted` (ECONNRESET) and
|
|
796
|
+
* destroys the response, so the rejection surfaces in the handler's read arm
|
|
797
|
+
* looking like any other thrown error. It is caller behaviour, not a server
|
|
798
|
+
* fault: no status can reach the client, so none must be journaled as
|
|
799
|
+
* delivered. A body that blew its size cap also leaves the request destroyed
|
|
800
|
+
* and incomplete, but that one is aimock's own decision, not a peer abort.
|
|
801
|
+
*/
|
|
802
|
+
function clientAbortedMidBody(req, res, err) {
|
|
803
|
+
if (err instanceof RequestBodyTooLargeError) return false;
|
|
804
|
+
return res.destroyed && !req.complete;
|
|
805
|
+
}
|
|
611
806
|
async function handleCompletions(req, res, fixtures, journal, defaults, modelFallback, providerKey, openRouter = false) {
|
|
612
807
|
setCorsHeaders(res);
|
|
613
808
|
let raw;
|
|
@@ -615,19 +810,43 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
615
810
|
raw = await readBody(req);
|
|
616
811
|
} catch (err) {
|
|
617
812
|
const msg = err instanceof Error ? err.message : "Failed to read request body";
|
|
813
|
+
const clientFault = err instanceof RequestBodyTooLargeError;
|
|
814
|
+
const route = `${req.method ?? "POST"} ${req.url ?? COMPLETIONS_PATH}`;
|
|
815
|
+
const clientAbort = clientAbortedMidBody(req, res, err);
|
|
816
|
+
const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
|
|
817
|
+
if (clientAbort || bodyLimitDisconnect) {
|
|
818
|
+
const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
|
|
819
|
+
defaults.logger.warn(bodyLimitDisconnect ? `${route}: ${msg}` : `${route}: client aborted mid-body (${msg})`);
|
|
820
|
+
journal.add({
|
|
821
|
+
method: req.method ?? "POST",
|
|
822
|
+
path: req.url ?? COMPLETIONS_PATH,
|
|
823
|
+
headers: flattenHeaders(req.headers),
|
|
824
|
+
body: null,
|
|
825
|
+
response: {
|
|
826
|
+
status: 0,
|
|
827
|
+
fixture: null,
|
|
828
|
+
interrupted: true,
|
|
829
|
+
interruptReason
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const status = clientFault ? 400 : 500;
|
|
835
|
+
if (clientFault) defaults.logger.warn(`${route}: ${msg}`);
|
|
836
|
+
else defaults.logger.error(`${route}: ${msg}`);
|
|
618
837
|
journal.add({
|
|
619
838
|
method: req.method ?? "POST",
|
|
620
839
|
path: req.url ?? COMPLETIONS_PATH,
|
|
621
840
|
headers: flattenHeaders(req.headers),
|
|
622
841
|
body: null,
|
|
623
842
|
response: {
|
|
624
|
-
status
|
|
843
|
+
status,
|
|
625
844
|
fixture: null
|
|
626
845
|
}
|
|
627
846
|
});
|
|
628
|
-
writeErrorResponse(res,
|
|
847
|
+
writeErrorResponse(res, status, openRouter ? serializeOpenRouterError(status, `Request body read failed: ${msg}`) : JSON.stringify({ error: {
|
|
629
848
|
message: `Request body read failed: ${msg}`,
|
|
630
|
-
type: "server_error"
|
|
849
|
+
type: clientFault ? "invalid_request_error" : "server_error"
|
|
631
850
|
} }));
|
|
632
851
|
return;
|
|
633
852
|
}
|
|
@@ -691,6 +910,51 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
691
910
|
} }));
|
|
692
911
|
return;
|
|
693
912
|
}
|
|
913
|
+
const nullMessageIndex = body.messages.findIndex((message) => message === null);
|
|
914
|
+
if (nullMessageIndex !== -1) {
|
|
915
|
+
const message = `Invalid request: messages[${nullMessageIndex}] must be an object`;
|
|
916
|
+
journal.add({
|
|
917
|
+
method: req.method ?? "POST",
|
|
918
|
+
path: req.url ?? COMPLETIONS_PATH,
|
|
919
|
+
headers: flattenHeaders(req.headers),
|
|
920
|
+
body: null,
|
|
921
|
+
response: {
|
|
922
|
+
status: 400,
|
|
923
|
+
fixture: null
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
writeErrorResponse(res, 400, openRouter ? serializeOpenRouterError(400, message) : JSON.stringify({ error: {
|
|
927
|
+
message,
|
|
928
|
+
type: "invalid_request_error",
|
|
929
|
+
param: null,
|
|
930
|
+
code: null
|
|
931
|
+
} }));
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (new URL(req.url ?? COMPLETIONS_PATH, "http://localhost").pathname === COMPLETIONS_PATH) {
|
|
935
|
+
let toolsError = validateToolsField(body.tools);
|
|
936
|
+
if (!toolsError && Array.isArray(body.tools)) {
|
|
937
|
+
const nullFunctionIndex = body.tools.findIndex((tool) => tool?.function === null);
|
|
938
|
+
if (nullFunctionIndex !== -1) toolsError = `tools[${nullFunctionIndex}].function must not be null`;
|
|
939
|
+
}
|
|
940
|
+
if (toolsError) {
|
|
941
|
+
journal.add({
|
|
942
|
+
method: req.method ?? "POST",
|
|
943
|
+
path: req.url ?? COMPLETIONS_PATH,
|
|
944
|
+
headers: flattenHeaders(req.headers),
|
|
945
|
+
body: null,
|
|
946
|
+
response: {
|
|
947
|
+
status: 400,
|
|
948
|
+
fixture: null
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
952
|
+
message: toolsError,
|
|
953
|
+
type: "invalid_request_error"
|
|
954
|
+
} }));
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
694
958
|
const method = req.method ?? "POST";
|
|
695
959
|
const path = req.url ?? COMPLETIONS_PATH;
|
|
696
960
|
const flatHeaders = flattenHeaders(req.headers);
|
|
@@ -703,7 +967,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
703
967
|
let preResolvedResponse = null;
|
|
704
968
|
let responseModel = body.model;
|
|
705
969
|
let fixtureCountIncremented = false;
|
|
706
|
-
|
|
970
|
+
const openRouterFallback = openRouter && Array.isArray(body.models) && body.models.length > 0;
|
|
971
|
+
const selectFixture = (probe, counts) => {
|
|
972
|
+
for (;;) {
|
|
973
|
+
const attempt = matchFixtureDiagnostic(fixtures, probe, counts, defaults.requestTransform, matchOptions);
|
|
974
|
+
if (attempt.fixture && isOneShotError(attempt.fixture) && !claimOneShotError(fixtures, attempt.fixture)) continue;
|
|
975
|
+
return attempt;
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
if (openRouterFallback) {
|
|
707
979
|
const candidates = buildOpenRouterCandidates(body);
|
|
708
980
|
let matchCounts = journal.getFixtureMatchCountsForTest(testId);
|
|
709
981
|
const allowFallbacks = body.provider?.allow_fallbacks !== false;
|
|
@@ -714,7 +986,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
714
986
|
...body,
|
|
715
987
|
model: candidate
|
|
716
988
|
};
|
|
717
|
-
const attempt =
|
|
989
|
+
const attempt = selectFixture(probe, matchCounts);
|
|
718
990
|
skippedBySequenceOrTurn = Math.max(skippedBySequenceOrTurn, attempt.skippedBySequenceOrTurn);
|
|
719
991
|
if (!attempt.fixture) return null;
|
|
720
992
|
const response = await resolveResponse(attempt.fixture, probe);
|
|
@@ -751,35 +1023,48 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
751
1023
|
preResolvedResponse = lastErrorResponse;
|
|
752
1024
|
}
|
|
753
1025
|
} else {
|
|
754
|
-
const single =
|
|
1026
|
+
const single = selectFixture(body, journal.getFixtureMatchCountsForTest(testId));
|
|
755
1027
|
fixture = single.fixture;
|
|
756
1028
|
skippedBySequenceOrTurn = single.skippedBySequenceOrTurn;
|
|
757
1029
|
}
|
|
758
1030
|
if (fixture) {
|
|
759
1031
|
if (!fixtureCountIncremented) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
760
|
-
defaults.logger.debug(`Fixture matched: ${
|
|
1032
|
+
defaults.logger.debug(`Fixture matched: ${describeMatch(fixture.match, fixtures.indexOf(fixture))}`);
|
|
761
1033
|
} else {
|
|
762
1034
|
const lastUserMsg = body.messages.filter((m) => m.role === "user").pop();
|
|
763
1035
|
const snippet = typeof lastUserMsg?.content === "string" ? lastUserMsg.content.slice(0, 80) : "";
|
|
764
1036
|
defaults.logger.debug(`No fixture matched for request (model=${body.model ?? "?"}, msg="${snippet}")`);
|
|
765
1037
|
}
|
|
766
|
-
const
|
|
1038
|
+
const chaosConfig = resolveChaosConfig(fixture, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
1039
|
+
await awaitChaosLatency(fixture, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
|
|
1040
|
+
const goneAfterLatency = responseGoneReason(res);
|
|
1041
|
+
if (goneAfterLatency !== null) {
|
|
1042
|
+
releaseOneShotError(fixtures, fixture);
|
|
1043
|
+
defaults.logger.debug(`[chaos] ${method} ${path}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const chaosAction = evaluateChaos(fixture, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
|
|
767
1047
|
const chaosContext = {
|
|
768
1048
|
method,
|
|
769
1049
|
path,
|
|
770
1050
|
headers: flatHeaders,
|
|
771
1051
|
body
|
|
772
1052
|
};
|
|
773
|
-
|
|
774
|
-
|
|
1053
|
+
const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
|
|
1054
|
+
const missWouldProxy = wouldProxyMiss(effectiveStrict, defaults.record, providerKey);
|
|
1055
|
+
const noFixtureSource = missWouldProxy ? "proxy" : "internal";
|
|
1056
|
+
if (chaosAction === "drop" || chaosAction === "disconnect" || chaosAction === "rateLimit") {
|
|
1057
|
+
applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : noFixtureSource, defaults.registry, defaults.logger);
|
|
1058
|
+
releaseOneShotError(fixtures, fixture);
|
|
775
1059
|
return;
|
|
776
1060
|
}
|
|
777
|
-
if (
|
|
778
|
-
applyChaosAction(chaosAction, res, fixture, journal, chaosContext, "fixture", defaults.registry);
|
|
1061
|
+
if (chaosAction === "malformed" && (fixture || !missWouldProxy)) {
|
|
1062
|
+
applyChaosAction(chaosAction, res, fixture, journal, chaosContext, fixture ? "fixture" : "internal", defaults.registry, defaults.logger);
|
|
1063
|
+
releaseOneShotError(fixtures, fixture);
|
|
779
1064
|
return;
|
|
780
1065
|
}
|
|
781
1066
|
if (!fixture) {
|
|
782
|
-
if (
|
|
1067
|
+
if (effectiveStrict) {
|
|
783
1068
|
const strictStatus = 503;
|
|
784
1069
|
const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
785
1070
|
defaults.logger.error(strictNoMatchLogLine(req.method ?? "POST", req.url ?? COMPLETIONS_PATH, skippedBySequenceOrTurn));
|
|
@@ -805,7 +1090,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
805
1090
|
if (defaults.record && providerKey) {
|
|
806
1091
|
const hookOptions = chaosAction === "malformed" ? {
|
|
807
1092
|
beforeWriteResponse: () => {
|
|
808
|
-
applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry);
|
|
1093
|
+
applyChaosAction(chaosAction, res, null, journal, chaosContext, "proxy", defaults.registry, defaults.logger);
|
|
809
1094
|
return true;
|
|
810
1095
|
},
|
|
811
1096
|
onHookBypassed: (reason) => {
|
|
@@ -817,9 +1102,22 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
817
1102
|
});
|
|
818
1103
|
}
|
|
819
1104
|
} : void 0;
|
|
820
|
-
|
|
1105
|
+
let clientHungUp = false;
|
|
1106
|
+
const noteClientHangUp = () => {
|
|
1107
|
+
if (!res.writableEnded) clientHungUp = true;
|
|
1108
|
+
};
|
|
1109
|
+
req.socket?.on("end", noteClientHangUp);
|
|
1110
|
+
req.socket?.on("error", noteClientHangUp);
|
|
1111
|
+
let outcome;
|
|
1112
|
+
try {
|
|
1113
|
+
outcome = await proxyAndRecord(req, res, body, providerKey, req.url ?? COMPLETIONS_PATH, fixtures, defaults, raw, hookOptions);
|
|
1114
|
+
} finally {
|
|
1115
|
+
req.socket?.off("end", noteClientHangUp);
|
|
1116
|
+
req.socket?.off("error", noteClientHangUp);
|
|
1117
|
+
}
|
|
821
1118
|
if (outcome === "handled_by_hook") return;
|
|
822
1119
|
if (outcome !== "not_configured") {
|
|
1120
|
+
const destroyedMidStream = res.destroyed && !res.writableEnded;
|
|
823
1121
|
journal.add({
|
|
824
1122
|
method: req.method ?? "POST",
|
|
825
1123
|
path: req.url ?? COMPLETIONS_PATH,
|
|
@@ -828,11 +1126,19 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
828
1126
|
response: {
|
|
829
1127
|
status: res.statusCode ?? 200,
|
|
830
1128
|
fixture: null,
|
|
831
|
-
source: "proxy"
|
|
1129
|
+
source: "proxy",
|
|
1130
|
+
...destroyedMidStream ? {
|
|
1131
|
+
interrupted: true,
|
|
1132
|
+
interruptReason: clientHungUp ? "client aborted" : "proxy stream destroyed"
|
|
1133
|
+
} : {}
|
|
832
1134
|
}
|
|
833
1135
|
});
|
|
834
1136
|
return;
|
|
835
1137
|
}
|
|
1138
|
+
if (chaosAction === "malformed") {
|
|
1139
|
+
applyChaosAction(chaosAction, res, null, journal, chaosContext, "internal", defaults.registry, defaults.logger);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
836
1142
|
}
|
|
837
1143
|
journal.add({
|
|
838
1144
|
method: req.method ?? "POST",
|
|
@@ -908,7 +1214,7 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
908
1214
|
const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);
|
|
909
1215
|
const effReasoning = resolveReasoningForModel(response.reasoning, responseModel, effectiveStrict, defaults.logger);
|
|
910
1216
|
const streaming = body.stream === true;
|
|
911
|
-
const
|
|
1217
|
+
const blockOutcome = response.blocks && response.blocks.length > 0 ? resolveFixtureBlockOutcome(response.blocks) : void 0;
|
|
912
1218
|
const journalEntry = journal.add({
|
|
913
1219
|
method: req.method ?? "POST",
|
|
914
1220
|
path: req.url ?? COMPLETIONS_PATH,
|
|
@@ -920,12 +1226,15 @@ async function handleCompletions(req, res, fixtures, journal, defaults, modelFal
|
|
|
920
1226
|
}
|
|
921
1227
|
});
|
|
922
1228
|
if (!streaming) {
|
|
923
|
-
const completion = buildContentWithToolCallsCompletion(response.content ?? "", response.toolCalls ?? [], responseModel, effReasoning,
|
|
1229
|
+
const completion = buildContentWithToolCallsCompletion(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, effReasoning, blockOutcome ? {
|
|
1230
|
+
...overrides,
|
|
1231
|
+
finishReason: overrides?.finishReason ?? (blockOutcome.hasToolCalls ? "tool_calls" : "stop")
|
|
1232
|
+
} : overrides, body.messages);
|
|
924
1233
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
925
1234
|
res.end(JSON.stringify(shapeORCompletion(completion, overrides)));
|
|
926
1235
|
} else {
|
|
927
|
-
const chunks = buildContentWithToolCallsChunks(response.content ?? "", response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides,
|
|
928
|
-
const completionText =
|
|
1236
|
+
const chunks = buildContentWithToolCallsChunks(blockOutcome?.content ?? response.content ?? "", blockOutcome?.toolCalls ?? response.toolCalls ?? [], responseModel, chunkSize, effReasoning, overrides, blockOutcome?.ordered);
|
|
1237
|
+
const completionText = blockOutcome ? blockOutcome.content + blockOutcome.toolCalls.map((tc) => tc.name + tc.arguments).join("") : (response.content ?? "") + (response.toolCalls ?? []).map((tc) => tc.name + tc.arguments).join("");
|
|
929
1238
|
const usageChunk = emitStreamingUsage ? buildUsageChunk(chunks[0]?.id ?? "chatcmpl-unknown", overrides?.model ?? responseModel, chunks[0]?.created ?? Math.floor(Date.now() / 1e3), resolveStreamingUsageTokens(overrides, completionText), overrides?.systemFingerprint) : void 0;
|
|
930
1239
|
shapeORChunks(chunks, usageChunk, overrides);
|
|
931
1240
|
const interruption = createInterruptionSignal(fixture);
|
|
@@ -1055,9 +1364,24 @@ async function createServer(fixtures, options, mounts, serviceFixtures) {
|
|
|
1055
1364
|
}
|
|
1056
1365
|
/** @internal Config startup uses this to avoid parsing a selected auth source twice. */
|
|
1057
1366
|
async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mounts, serviceFixtures) {
|
|
1367
|
+
const logger = new Logger(options?.logLevel ?? "silent");
|
|
1368
|
+
const mountList = mounts ?? [];
|
|
1369
|
+
const handOff = serviceFixtures?.mcpFakes && serviceFixtures.mcpFakes.length > 0 ? handOffMcpFakes(mountList, serviceFixtures.mcpFakes, logger) : null;
|
|
1370
|
+
try {
|
|
1371
|
+
return await startServer(fixtures, options, resolvedAuth, mountList, logger, serviceFixtures, () => handOff?.commit());
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
handOff?.undo();
|
|
1374
|
+
throw err;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* The server of `createServerWithResolvedAuth`, after the MCP fakes hand-off.
|
|
1379
|
+
* `commitHandOff` runs once the server listens; if it throws, the server is
|
|
1380
|
+
* closed and start rejects.
|
|
1381
|
+
*/
|
|
1382
|
+
async function startServer(fixtures, options, resolvedAuth, mountList, logger, serviceFixtures, commitHandOff) {
|
|
1058
1383
|
const host = options?.host ?? "127.0.0.1";
|
|
1059
1384
|
const port = options?.port ?? 0;
|
|
1060
|
-
const logger = new Logger(options?.logLevel ?? "silent");
|
|
1061
1385
|
const registry = options?.metrics ? createMetricsRegistry() : void 0;
|
|
1062
1386
|
const serverOptions = options ?? {};
|
|
1063
1387
|
const chaosByTestId = /* @__PURE__ */ new Map();
|
|
@@ -1109,21 +1433,14 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1109
1433
|
}
|
|
1110
1434
|
};
|
|
1111
1435
|
if (options?.chaos) {
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
},
|
|
1121
|
-
{
|
|
1122
|
-
name: "disconnectRate",
|
|
1123
|
-
value: options.chaos.disconnectRate
|
|
1124
|
-
}
|
|
1125
|
-
];
|
|
1126
|
-
for (const { name, value } of chaosRates) if (value !== void 0 && (value < 0 || value > 1)) logger.warn(`Chaos ${name} (${value}) is outside 0-1 range — will be clamped at runtime`);
|
|
1436
|
+
const chaosDefaults = options.chaos;
|
|
1437
|
+
for (const field of CHAOS_FIELD_NAMES) {
|
|
1438
|
+
const value = chaosDefaults[field];
|
|
1439
|
+
if (value === void 0) continue;
|
|
1440
|
+
if (typeof value === "number" && parseChaosField(field, value) !== void 0) continue;
|
|
1441
|
+
const shape = CHAOS_FIELDS[field].integer ? "a whole number of ms" : "a number";
|
|
1442
|
+
logger.warn(`Chaos default ${field} value ${JSON.stringify(value)} — must be ${shape} in [0,${CHAOS_FIELDS[field].max}]; rejected at runtime, never clamped: this default is ignored`);
|
|
1443
|
+
}
|
|
1127
1444
|
}
|
|
1128
1445
|
for (const { name, config } of [
|
|
1129
1446
|
{
|
|
@@ -1169,67 +1486,276 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1169
1486
|
if (basePath !== void 0 && /(^|\/)api\/v3\/?$/.test(basePath)) throw new Error(`record.providers.byteplus (${bytePlusBase}) ends in /api/v3, but aimock appends the /api/v3 prefix itself — every BytePlus request would go to /api/v3/api/v3/... and fail upstream. Configure the ORIGIN only (e.g. https://ark.ap-southeast.bytepluses.com).`);
|
|
1170
1487
|
}
|
|
1171
1488
|
}
|
|
1489
|
+
/**
|
|
1490
|
+
* The in-flight request, scoped to the async execution that serves it.
|
|
1491
|
+
* Entered once in the `http.createServer` callback, so every handler —
|
|
1492
|
+
* including the ones in other modules that only ever see the shared
|
|
1493
|
+
* `journal` — runs inside it.
|
|
1494
|
+
*/
|
|
1495
|
+
const requestScope = new AsyncLocalStorage();
|
|
1496
|
+
/** The journal entry each request produced, keyed by the request object. */
|
|
1497
|
+
const ownJournalEntry = /* @__PURE__ */ new WeakMap();
|
|
1172
1498
|
const journal = new Journal({
|
|
1173
1499
|
maxEntries: options?.journalMaxEntries ?? 1e3,
|
|
1174
|
-
fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500
|
|
1500
|
+
fixtureCountsMaxTestIds: options?.fixtureCountsMaxTestIds ?? 500,
|
|
1501
|
+
onAdd: (entry) => {
|
|
1502
|
+
const req = requestScope.getStore();
|
|
1503
|
+
if (req) ownJournalEntry.set(req, entry);
|
|
1504
|
+
}
|
|
1175
1505
|
});
|
|
1176
1506
|
const videoStates = new VideoStateMap();
|
|
1177
1507
|
const openRouterVideoJobs = new OpenRouterVideoJobMap();
|
|
1178
1508
|
const veoVideoJobs = new VeoVideoJobMap();
|
|
1179
1509
|
const grokVideoJobs = new GrokVideoJobMap();
|
|
1180
1510
|
const bytePlusVideoJobs = new BytePlusVideoJobMap();
|
|
1181
|
-
|
|
1511
|
+
/**
|
|
1512
|
+
* The OpenAI error `type` that goes with a status: 4xx is the caller's
|
|
1513
|
+
* fault, everything else is ours. Shared by `routeError` and the envelopes
|
|
1514
|
+
* it calls so the body can never disagree with the status line.
|
|
1515
|
+
*/
|
|
1516
|
+
function errorTypeForStatus(status) {
|
|
1517
|
+
return status >= 400 && status < 500 ? "invalid_request_error" : "server_error";
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* The newest journal entry this request already produced, or null.
|
|
1521
|
+
*
|
|
1522
|
+
* Attribution is by the identity of the `IncomingMessage` the entry was
|
|
1523
|
+
* written under — never by `x-request-id`. That header is CALLER-supplied
|
|
1524
|
+
* (`resolveRequestId` echoes a well-formed one) and nothing makes it
|
|
1525
|
+
* unique: a retry, a harness that pins one id, or a load generator sends
|
|
1526
|
+
* the same id on many requests, and keying off it made a crash rewrite an
|
|
1527
|
+
* EARLIER request's entry while leaving the crashing request untraced. The
|
|
1528
|
+
* request object cannot be collided by a caller, so one entry belongs to
|
|
1529
|
+
* exactly one request. A handler writes at most one entry; if it writes
|
|
1530
|
+
* more, the newest wins, which is the entry the old scan would have found.
|
|
1531
|
+
*/
|
|
1532
|
+
function lastJournalEntryFor(req) {
|
|
1533
|
+
return ownJournalEntry.get(req) ?? null;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Uniform terminal arm for a route handler that threw. Every route error
|
|
1537
|
+
* path logs the route, journals the failure, sets CORS and answers the error
|
|
1538
|
+
* envelope — so a handler crash is never invisible in the logs or in
|
|
1539
|
+
* `/__aimock/journal`.
|
|
1540
|
+
*
|
|
1541
|
+
* ONE request produces ONE entry. `res.headersSent` is not the test for that:
|
|
1542
|
+
* a handler that journaled its 200 and then threw before `writeHead` has
|
|
1543
|
+
* sent no headers, and journaling again would leave two contradicting
|
|
1544
|
+
* entries for one request. So the existing entry is looked up by request
|
|
1545
|
+
* identity ({@link lastJournalEntryFor}) and AMENDED — status to what the
|
|
1546
|
+
* client actually got when the response is still changeable, or interrupted
|
|
1547
|
+
* when it isn't — and a fresh entry is
|
|
1548
|
+
* added only when the handler journaled nothing. That holds after the
|
|
1549
|
+
* response too: a handler that answered and then crashed before its own
|
|
1550
|
+
* journal write gets an entry carrying the status already sent, so a
|
|
1551
|
+
* post-response crash is never invisible.
|
|
1552
|
+
*
|
|
1553
|
+
* Once headers are on the wire the status can't change, so the socket is
|
|
1554
|
+
* destroyed: a clean `end()` reads as EOF and the client treats a truncated
|
|
1555
|
+
* stream as a complete response. The chat-style SSE routes whose clients
|
|
1556
|
+
* surface an error frame pass `streamEvent`; that frame is written first,
|
|
1557
|
+
* but the tear-down still follows it.
|
|
1558
|
+
*
|
|
1559
|
+
* A body that blew its size cap is the CALLER's fault, not a server fault:
|
|
1560
|
+
* it logs at warn, but if the reader destroyed the socket it journals an
|
|
1561
|
+
* interruption with status 0 because no 4xx reached the client. A client
|
|
1562
|
+
* that hung up before its body arrived
|
|
1563
|
+
* ({@link clientAbortedMidBody}) is not a fault at all: it logs at warn and
|
|
1564
|
+
* journals a torn socket (`interrupted`, status 0), never a 500 the client
|
|
1565
|
+
* could not have received.
|
|
1566
|
+
*/
|
|
1567
|
+
function routeError(req, res, err, pathname, opts) {
|
|
1568
|
+
const route = `${req.method ?? "?"} ${pathname}`;
|
|
1569
|
+
const msg = err instanceof Error ? err.message : "Internal error";
|
|
1570
|
+
const clientFault = err instanceof RequestBodyTooLargeError;
|
|
1571
|
+
const clientAbort = clientAbortedMidBody(req, res, err);
|
|
1572
|
+
const status = clientFault ? 400 : 500;
|
|
1573
|
+
if (clientAbort) logger.warn(`${route}: client aborted mid-body (${msg})`);
|
|
1574
|
+
else if (clientFault) logger.warn(`${route}: ${msg}`);
|
|
1575
|
+
else logger.error(`${route}: ${msg}`);
|
|
1576
|
+
if (err instanceof Error && err.stack) logger.debug(err.stack);
|
|
1577
|
+
const existing = lastJournalEntryFor(req);
|
|
1578
|
+
const bodyLimitDisconnect = clientFault && (req.destroyed || res.destroyed);
|
|
1579
|
+
if ((clientAbort || bodyLimitDisconnect) && !res.headersSent) {
|
|
1580
|
+
const interruptReason = bodyLimitDisconnect ? "request body exceeded size limit" : "client aborted";
|
|
1581
|
+
if (existing) {
|
|
1582
|
+
if (!existing.response.interrupted) {
|
|
1583
|
+
existing.response.status = 0;
|
|
1584
|
+
existing.response.interrupted = true;
|
|
1585
|
+
existing.response.interruptReason = interruptReason;
|
|
1586
|
+
}
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
try {
|
|
1590
|
+
journal.add({
|
|
1591
|
+
method: req.method ?? "?",
|
|
1592
|
+
path: req.url ?? "?",
|
|
1593
|
+
headers: flattenHeaders(req.headers),
|
|
1594
|
+
body: null,
|
|
1595
|
+
...opts?.service ? { service: opts.service } : {},
|
|
1596
|
+
response: {
|
|
1597
|
+
status: 0,
|
|
1598
|
+
fixture: null,
|
|
1599
|
+
source: "internal",
|
|
1600
|
+
interrupted: true,
|
|
1601
|
+
interruptReason
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
} catch (jErr) {
|
|
1605
|
+
logger.warn(`${route}: journal write failed after body-read interruption: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1606
|
+
}
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
if (!res.headersSent) {
|
|
1610
|
+
if (existing) existing.response.status = status;
|
|
1611
|
+
else try {
|
|
1612
|
+
journal.add({
|
|
1613
|
+
method: req.method ?? "?",
|
|
1614
|
+
path: req.url ?? "?",
|
|
1615
|
+
headers: flattenHeaders(req.headers),
|
|
1616
|
+
body: null,
|
|
1617
|
+
...opts?.service ? { service: opts.service } : {},
|
|
1618
|
+
response: {
|
|
1619
|
+
status,
|
|
1620
|
+
fixture: null,
|
|
1621
|
+
source: "internal"
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
} catch (jErr) {
|
|
1625
|
+
logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1626
|
+
}
|
|
1627
|
+
setCorsHeaders(res);
|
|
1628
|
+
writeErrorResponse(res, status, opts?.envelope ? opts.envelope(msg, status) : JSON.stringify({ error: {
|
|
1629
|
+
message: msg,
|
|
1630
|
+
type: errorTypeForStatus(status)
|
|
1631
|
+
} }));
|
|
1632
|
+
} else {
|
|
1633
|
+
const midStreamReason = clientAbort ? "client aborted" : "handler crashed mid-stream";
|
|
1634
|
+
if (existing) {
|
|
1635
|
+
if (res.writableEnded) {
|
|
1636
|
+
if (existing.response.error === void 0) existing.response.error = msg;
|
|
1637
|
+
} else if (!existing.response.interrupted) {
|
|
1638
|
+
existing.response.interrupted = true;
|
|
1639
|
+
existing.response.interruptReason = midStreamReason;
|
|
1640
|
+
}
|
|
1641
|
+
} else try {
|
|
1642
|
+
journal.add({
|
|
1643
|
+
method: req.method ?? "?",
|
|
1644
|
+
path: req.url ?? "?",
|
|
1645
|
+
headers: flattenHeaders(req.headers),
|
|
1646
|
+
body: null,
|
|
1647
|
+
...opts?.service ? { service: opts.service } : {},
|
|
1648
|
+
response: {
|
|
1649
|
+
status: res.statusCode,
|
|
1650
|
+
fixture: null,
|
|
1651
|
+
source: "internal",
|
|
1652
|
+
...res.writableEnded ? { error: msg } : {
|
|
1653
|
+
interrupted: true,
|
|
1654
|
+
interruptReason: midStreamReason
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
} catch (jErr) {
|
|
1659
|
+
logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1660
|
+
}
|
|
1661
|
+
if (res.writableEnded) return;
|
|
1662
|
+
if (opts?.streamEvent) try {
|
|
1663
|
+
res.write(opts.streamEvent(msg, status), () => {
|
|
1664
|
+
if (!res.destroyed) res.destroy();
|
|
1665
|
+
});
|
|
1666
|
+
} catch (writeErr) {
|
|
1667
|
+
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1668
|
+
res.destroy();
|
|
1669
|
+
}
|
|
1670
|
+
else res.destroy();
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
for (const { handler } of mountList) {
|
|
1182
1674
|
if (handler.setJournal) handler.setJournal(journal);
|
|
1183
1675
|
if (registry && handler.setRegistry) handler.setRegistry(registry);
|
|
1676
|
+
if (handler.setLogger) handler.setLogger(logger);
|
|
1184
1677
|
}
|
|
1185
1678
|
if (registry) registry.setGauge("aimock_fixtures_loaded", {}, fixtures.length);
|
|
1186
1679
|
const server = http$1.createServer((req, res) => {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
const url = req.url ?? "?";
|
|
1192
|
-
defaults.logger.warn(`Unhandled request error on ${method} ${url}: ${msg}\n${stack}`);
|
|
1193
|
-
if (!res.headersSent) {
|
|
1194
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1195
|
-
res.end(JSON.stringify({ error: {
|
|
1196
|
-
message: msg,
|
|
1197
|
-
type: "server_error"
|
|
1198
|
-
} }));
|
|
1199
|
-
} else if (!res.writableEnded) res.end();
|
|
1680
|
+
requestScope.run(req, () => {
|
|
1681
|
+
handleHttpRequest(req, res).catch((err) => {
|
|
1682
|
+
routeError(req, res, err, req.url ?? "?");
|
|
1683
|
+
});
|
|
1200
1684
|
});
|
|
1201
1685
|
});
|
|
1202
1686
|
async function handleHttpRequest(req, res) {
|
|
1203
1687
|
const startTime = registry ? process.hrtime.bigint() : 0n;
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1688
|
+
const { id: requestId, generated: requestIdMinted } = resolveRequestId(req.headers);
|
|
1689
|
+
req.headers["x-request-id"] = requestId;
|
|
1690
|
+
markMintedRequestId(req, requestIdMinted);
|
|
1691
|
+
res.setHeader("X-Request-Id", requestId);
|
|
1692
|
+
let pathname = "";
|
|
1693
|
+
if (registry) {
|
|
1694
|
+
let recorded = false;
|
|
1695
|
+
const recordOutcome = (status) => {
|
|
1696
|
+
if (recorded) return;
|
|
1697
|
+
recorded = true;
|
|
1698
|
+
try {
|
|
1699
|
+
const normalizedPath = normalizePathLabel(pathname, mountList.map((m) => m.path));
|
|
1700
|
+
const method = req.method ?? "UNKNOWN";
|
|
1701
|
+
registry.incrementCounter("aimock_requests_total", {
|
|
1702
|
+
method,
|
|
1703
|
+
path: normalizedPath,
|
|
1704
|
+
status
|
|
1705
|
+
});
|
|
1706
|
+
const elapsed = Number(process.hrtime.bigint() - startTime) / 1e9;
|
|
1707
|
+
registry.observeHistogram("aimock_request_duration_seconds", {
|
|
1708
|
+
method,
|
|
1709
|
+
path: normalizedPath
|
|
1710
|
+
}, elapsed);
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
defaults.logger.warn("metrics instrumentation error", err);
|
|
1713
|
+
}
|
|
1714
|
+
};
|
|
1715
|
+
res.on("finish", () => recordOutcome(String(res.statusCode)));
|
|
1716
|
+
res.on("close", () => {
|
|
1717
|
+
if (!res.writableFinished) recordOutcome(DESTROYED_STATUS_LABEL);
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
let parsedUrl;
|
|
1721
|
+
try {
|
|
1722
|
+
parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1723
|
+
} catch (err) {
|
|
1724
|
+
const route = `${req.method ?? "?"} ${req.url ?? "?"}`;
|
|
1725
|
+
const msg = err instanceof Error ? err.message : "Internal error";
|
|
1726
|
+
logger.error(`${route}: ${msg}`);
|
|
1727
|
+
if (err instanceof Error && err.stack) logger.debug(err.stack);
|
|
1728
|
+
try {
|
|
1729
|
+
journal.add({
|
|
1730
|
+
method: req.method ?? "?",
|
|
1731
|
+
path: req.url ?? "?",
|
|
1732
|
+
headers: flattenHeaders(req.headers),
|
|
1733
|
+
body: null,
|
|
1734
|
+
response: {
|
|
1735
|
+
status: 400,
|
|
1736
|
+
fixture: null,
|
|
1737
|
+
source: "internal"
|
|
1738
|
+
}
|
|
1739
|
+
});
|
|
1740
|
+
} catch (jErr) {
|
|
1741
|
+
logger.warn(`${route}: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1742
|
+
}
|
|
1743
|
+
setCorsHeaders(res);
|
|
1744
|
+
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
1745
|
+
message: "Invalid request target",
|
|
1746
|
+
type: "invalid_request_error"
|
|
1747
|
+
} }));
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
pathname = parsedUrl.pathname;
|
|
1206
1751
|
const originalPathname = pathname;
|
|
1207
1752
|
const isOpenRouter = isOpenRouterPath(originalPathname);
|
|
1208
1753
|
const isBytePlusArk = defaults.record?.providers.byteplus !== void 0 && originalPathname.startsWith("/api/v3/");
|
|
1209
|
-
if (registry) res.on("finish", () => {
|
|
1210
|
-
try {
|
|
1211
|
-
const normalizedPath = normalizePathLabel(pathname);
|
|
1212
|
-
const method = req.method ?? "UNKNOWN";
|
|
1213
|
-
const status = String(res.statusCode);
|
|
1214
|
-
registry.incrementCounter("aimock_requests_total", {
|
|
1215
|
-
method,
|
|
1216
|
-
path: normalizedPath,
|
|
1217
|
-
status
|
|
1218
|
-
});
|
|
1219
|
-
const elapsed = Number(process.hrtime.bigint() - startTime) / 1e9;
|
|
1220
|
-
registry.observeHistogram("aimock_request_duration_seconds", {
|
|
1221
|
-
method,
|
|
1222
|
-
path: normalizedPath
|
|
1223
|
-
}, elapsed);
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
defaults.logger.warn("metrics instrumentation error", err);
|
|
1226
|
-
}
|
|
1227
|
-
});
|
|
1228
1754
|
if (req.method === "OPTIONS" && (!resolvedAuth.policy.enabled || req.headers.origin !== void 0 && req.headers["access-control-request-method"] !== void 0)) {
|
|
1229
1755
|
handleOptions(res);
|
|
1230
1756
|
return;
|
|
1231
1757
|
}
|
|
1232
|
-
if (!(req.method === "GET" && (pathname === HEALTH_PATH || pathname === READY_PATH || pathname ===
|
|
1758
|
+
if (!(req.method === "GET" && (pathname === HEALTH_PATH || pathname === READY_PATH || pathname === METRICS_PATH))) {
|
|
1233
1759
|
if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
|
|
1234
1760
|
writeApiKeyHttpRejection(res, setCorsHeaders);
|
|
1235
1761
|
return;
|
|
@@ -1237,25 +1763,18 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1237
1763
|
markAuthenticatedRequest(req, resolvedAuth.policy);
|
|
1238
1764
|
}
|
|
1239
1765
|
if (pathname.startsWith(CONTROL_PREFIX)) {
|
|
1240
|
-
await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults);
|
|
1766
|
+
await handleControlAPI(req, res, pathname, parsedUrl.searchParams, fixtures, journal, videoStates, openRouterVideoJobs, veoVideoJobs, grokVideoJobs, bytePlusVideoJobs, defaults, mountList);
|
|
1241
1767
|
return;
|
|
1242
1768
|
}
|
|
1243
|
-
if (
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
if (await handler.handleRequest(req, res, subPath)) return;
|
|
1247
|
-
}
|
|
1769
|
+
for (const { path: mountPath, handler } of mountList) if (pathname === mountPath || pathname.startsWith(mountPath + "/")) {
|
|
1770
|
+
const subPath = pathname.slice(mountPath.length) || "/";
|
|
1771
|
+
if (await handler.handleRequest(req, res, subPath)) return;
|
|
1248
1772
|
}
|
|
1249
1773
|
if (pathname === OLLAMA_CHAT_PATH && req.method === "POST") {
|
|
1250
1774
|
try {
|
|
1251
1775
|
await handleOllama(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1252
1776
|
} catch (err) {
|
|
1253
|
-
|
|
1254
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1255
|
-
message: msg,
|
|
1256
|
-
type: "server_error"
|
|
1257
|
-
} }));
|
|
1258
|
-
else if (!res.writableEnded) res.destroy();
|
|
1777
|
+
routeError(req, res, err, pathname);
|
|
1259
1778
|
}
|
|
1260
1779
|
return;
|
|
1261
1780
|
}
|
|
@@ -1263,12 +1782,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1263
1782
|
try {
|
|
1264
1783
|
await handleOllamaGenerate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1265
1784
|
} catch (err) {
|
|
1266
|
-
|
|
1267
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1268
|
-
message: msg,
|
|
1269
|
-
type: "server_error"
|
|
1270
|
-
} }));
|
|
1271
|
-
else if (!res.writableEnded) res.destroy();
|
|
1785
|
+
routeError(req, res, err, pathname);
|
|
1272
1786
|
}
|
|
1273
1787
|
return;
|
|
1274
1788
|
}
|
|
@@ -1276,12 +1790,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1276
1790
|
try {
|
|
1277
1791
|
await handleOllamaEmbeddings(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1278
1792
|
} catch (err) {
|
|
1279
|
-
|
|
1280
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1281
|
-
message: msg,
|
|
1282
|
-
type: "server_error"
|
|
1283
|
-
} }));
|
|
1284
|
-
else if (!res.writableEnded) res.destroy();
|
|
1793
|
+
routeError(req, res, err, pathname);
|
|
1285
1794
|
}
|
|
1286
1795
|
return;
|
|
1287
1796
|
}
|
|
@@ -1306,28 +1815,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1306
1815
|
try {
|
|
1307
1816
|
await handleOpenRouterVideoContent(req, res, openRouterVideoContentMatch[1], journal, defaults, setCorsHeaders, openRouterVideoJobs);
|
|
1308
1817
|
} catch (err) {
|
|
1309
|
-
|
|
1310
|
-
defaults.logger.error(`openrouter-video content: ${msg}`);
|
|
1311
|
-
if (!res.headersSent) {
|
|
1312
|
-
try {
|
|
1313
|
-
journal.add({
|
|
1314
|
-
method: req.method ?? "GET",
|
|
1315
|
-
path: req.url ?? pathname,
|
|
1316
|
-
headers: flattenHeaders(req.headers),
|
|
1317
|
-
body: null,
|
|
1318
|
-
response: {
|
|
1319
|
-
status: 500,
|
|
1320
|
-
fixture: null
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
} catch (jErr) {
|
|
1324
|
-
defaults.logger.warn(`openrouter-video content: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1325
|
-
}
|
|
1326
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1327
|
-
message: msg,
|
|
1328
|
-
type: "server_error"
|
|
1329
|
-
} }));
|
|
1330
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1818
|
+
routeError(req, res, err, pathname);
|
|
1331
1819
|
}
|
|
1332
1820
|
return;
|
|
1333
1821
|
}
|
|
@@ -1335,28 +1823,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1335
1823
|
try {
|
|
1336
1824
|
await handleOpenRouterVideoModels(req, res, fixtures, journal, defaults, setCorsHeaders);
|
|
1337
1825
|
} catch (err) {
|
|
1338
|
-
|
|
1339
|
-
defaults.logger.error(`openrouter-video models: ${msg}`);
|
|
1340
|
-
if (!res.headersSent) {
|
|
1341
|
-
try {
|
|
1342
|
-
journal.add({
|
|
1343
|
-
method: req.method ?? "GET",
|
|
1344
|
-
path: req.url ?? pathname,
|
|
1345
|
-
headers: flattenHeaders(req.headers),
|
|
1346
|
-
body: null,
|
|
1347
|
-
response: {
|
|
1348
|
-
status: 500,
|
|
1349
|
-
fixture: null
|
|
1350
|
-
}
|
|
1351
|
-
});
|
|
1352
|
-
} catch (jErr) {
|
|
1353
|
-
defaults.logger.warn(`openrouter-video models: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1354
|
-
}
|
|
1355
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1356
|
-
message: msg,
|
|
1357
|
-
type: "server_error"
|
|
1358
|
-
} }));
|
|
1359
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1826
|
+
routeError(req, res, err, pathname);
|
|
1360
1827
|
}
|
|
1361
1828
|
return;
|
|
1362
1829
|
}
|
|
@@ -1365,28 +1832,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1365
1832
|
try {
|
|
1366
1833
|
await handleOpenRouterVideoStatus(req, res, openRouterVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
|
|
1367
1834
|
} catch (err) {
|
|
1368
|
-
|
|
1369
|
-
defaults.logger.error(`openrouter-video status: ${msg}`);
|
|
1370
|
-
if (!res.headersSent) {
|
|
1371
|
-
try {
|
|
1372
|
-
journal.add({
|
|
1373
|
-
method: req.method ?? "GET",
|
|
1374
|
-
path: req.url ?? pathname,
|
|
1375
|
-
headers: flattenHeaders(req.headers),
|
|
1376
|
-
body: null,
|
|
1377
|
-
response: {
|
|
1378
|
-
status: 500,
|
|
1379
|
-
fixture: null
|
|
1380
|
-
}
|
|
1381
|
-
});
|
|
1382
|
-
} catch (jErr) {
|
|
1383
|
-
defaults.logger.warn(`openrouter-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1384
|
-
}
|
|
1385
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1386
|
-
message: msg,
|
|
1387
|
-
type: "server_error"
|
|
1388
|
-
} }));
|
|
1389
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1835
|
+
routeError(req, res, err, pathname);
|
|
1390
1836
|
}
|
|
1391
1837
|
return;
|
|
1392
1838
|
}
|
|
@@ -1395,28 +1841,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1395
1841
|
try {
|
|
1396
1842
|
await handleOpenRouterVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, openRouterVideoJobs);
|
|
1397
1843
|
} catch (err) {
|
|
1398
|
-
|
|
1399
|
-
defaults.logger.error(`openrouter-video submit: ${msg}`);
|
|
1400
|
-
if (!res.headersSent) {
|
|
1401
|
-
try {
|
|
1402
|
-
journal.add({
|
|
1403
|
-
method: req.method ?? "POST",
|
|
1404
|
-
path: req.url ?? pathname,
|
|
1405
|
-
headers: flattenHeaders(req.headers),
|
|
1406
|
-
body: null,
|
|
1407
|
-
response: {
|
|
1408
|
-
status: 500,
|
|
1409
|
-
fixture: null
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
} catch (jErr) {
|
|
1413
|
-
defaults.logger.warn(`openrouter-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1414
|
-
}
|
|
1415
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1416
|
-
message: msg,
|
|
1417
|
-
type: "server_error"
|
|
1418
|
-
} }));
|
|
1419
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1844
|
+
routeError(req, res, err, pathname);
|
|
1420
1845
|
}
|
|
1421
1846
|
return;
|
|
1422
1847
|
}
|
|
@@ -1425,28 +1850,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1425
1850
|
try {
|
|
1426
1851
|
await handleBytePlusVideoStatus(req, res, bytePlusVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
|
|
1427
1852
|
} catch (err) {
|
|
1428
|
-
|
|
1429
|
-
defaults.logger.error(`byteplus-video status: ${msg}`);
|
|
1430
|
-
if (!res.headersSent) {
|
|
1431
|
-
try {
|
|
1432
|
-
journal.add({
|
|
1433
|
-
method: req.method ?? "GET",
|
|
1434
|
-
path: req.url ?? pathname,
|
|
1435
|
-
headers: flattenHeaders(req.headers),
|
|
1436
|
-
body: null,
|
|
1437
|
-
response: {
|
|
1438
|
-
status: 500,
|
|
1439
|
-
fixture: null
|
|
1440
|
-
}
|
|
1441
|
-
});
|
|
1442
|
-
} catch (jErr) {
|
|
1443
|
-
defaults.logger.warn(`byteplus-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1444
|
-
}
|
|
1445
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1446
|
-
message: msg,
|
|
1447
|
-
type: "server_error"
|
|
1448
|
-
} }));
|
|
1449
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1853
|
+
routeError(req, res, err, pathname);
|
|
1450
1854
|
}
|
|
1451
1855
|
return;
|
|
1452
1856
|
}
|
|
@@ -1455,40 +1859,19 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1455
1859
|
try {
|
|
1456
1860
|
await handleBytePlusVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, bytePlusVideoJobs);
|
|
1457
1861
|
} catch (err) {
|
|
1458
|
-
|
|
1459
|
-
defaults.logger.error(`byteplus-video submit: ${msg}`);
|
|
1460
|
-
if (!res.headersSent) {
|
|
1461
|
-
try {
|
|
1462
|
-
journal.add({
|
|
1463
|
-
method: req.method ?? "POST",
|
|
1464
|
-
path: req.url ?? pathname,
|
|
1465
|
-
headers: flattenHeaders(req.headers),
|
|
1466
|
-
body: null,
|
|
1467
|
-
response: {
|
|
1468
|
-
status: 500,
|
|
1469
|
-
fixture: null
|
|
1470
|
-
}
|
|
1471
|
-
});
|
|
1472
|
-
} catch (jErr) {
|
|
1473
|
-
defaults.logger.warn(`byteplus-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1474
|
-
}
|
|
1475
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1476
|
-
message: msg,
|
|
1477
|
-
type: "server_error"
|
|
1478
|
-
} }));
|
|
1479
|
-
} else if (!res.writableEnded) res.destroy();
|
|
1862
|
+
routeError(req, res, err, pathname);
|
|
1480
1863
|
}
|
|
1481
1864
|
return;
|
|
1482
1865
|
}
|
|
1483
|
-
if (pathname ===
|
|
1866
|
+
if (pathname === OPENROUTER_MODELS_PATH && req.method === "GET") {
|
|
1484
1867
|
handleOpenRouterModels(req, res, fixtures, journal, defaults, setCorsHeaders);
|
|
1485
1868
|
return;
|
|
1486
1869
|
}
|
|
1487
|
-
if (pathname ===
|
|
1870
|
+
if (pathname === OPENROUTER_KEY_PATH && req.method === "GET") {
|
|
1488
1871
|
handleOpenRouterKey(req, res, journal, setCorsHeaders);
|
|
1489
1872
|
return;
|
|
1490
1873
|
}
|
|
1491
|
-
if (pathname ===
|
|
1874
|
+
if (pathname === OPENROUTER_CREDITS_PATH && req.method === "GET") {
|
|
1492
1875
|
handleOpenRouterCredits(req, res, journal, setCorsHeaders);
|
|
1493
1876
|
return;
|
|
1494
1877
|
}
|
|
@@ -1501,12 +1884,12 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1501
1884
|
if (!azureDeploymentId) pathname = normalizeCompatPath(pathname, logger);
|
|
1502
1885
|
if (pathname === HEALTH_PATH && req.method === "GET") {
|
|
1503
1886
|
setCorsHeaders(res);
|
|
1504
|
-
if (
|
|
1887
|
+
if (mountList.length > 0) {
|
|
1505
1888
|
const services = { llm: {
|
|
1506
1889
|
status: "ok",
|
|
1507
1890
|
fixtures: fixtures.length
|
|
1508
1891
|
} };
|
|
1509
|
-
for (const { path: mountPath, handler } of
|
|
1892
|
+
for (const { path: mountPath, handler } of mountList) if (handler.health) {
|
|
1510
1893
|
const name = mountPath.replace(/^\//, "");
|
|
1511
1894
|
services[name] = handler.health();
|
|
1512
1895
|
}
|
|
@@ -1527,7 +1910,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1527
1910
|
res.end(JSON.stringify({ status: "ready" }));
|
|
1528
1911
|
return;
|
|
1529
1912
|
}
|
|
1530
|
-
if (pathname ===
|
|
1913
|
+
if (pathname === METRICS_PATH && req.method === "GET") {
|
|
1531
1914
|
if (!registry) {
|
|
1532
1915
|
handleNotFound(res, "Not found");
|
|
1533
1916
|
return;
|
|
@@ -1554,14 +1937,261 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1554
1937
|
}));
|
|
1555
1938
|
return;
|
|
1556
1939
|
}
|
|
1940
|
+
const batchesCancelMatch = pathname.match(BATCHES_CANCEL_RE);
|
|
1941
|
+
if (batchesCancelMatch && req.method === "POST") {
|
|
1942
|
+
try {
|
|
1943
|
+
await readBody(req);
|
|
1944
|
+
await handleBatchesCancel(req, res, batchesCancelMatch[1], journal, defaults, setCorsHeaders);
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
routeError(req, res, err, pathname, { service: "batches" });
|
|
1947
|
+
}
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
const batchesIdMatch = pathname.match(BATCHES_ID_RE);
|
|
1951
|
+
if (batchesIdMatch && req.method === "GET") {
|
|
1952
|
+
try {
|
|
1953
|
+
await handleBatchesRetrieve(req, res, batchesIdMatch[1], journal, defaults, setCorsHeaders);
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
routeError(req, res, err, pathname, { service: "batches" });
|
|
1956
|
+
}
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (pathname === BATCHES_PATH && req.method === "GET") {
|
|
1960
|
+
try {
|
|
1961
|
+
await handleBatchesList(req, res, journal, defaults, setCorsHeaders);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
routeError(req, res, err, pathname, { service: "batches" });
|
|
1964
|
+
}
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (pathname === BATCHES_PATH && req.method === "POST") {
|
|
1968
|
+
try {
|
|
1969
|
+
await handleBatchesCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
routeError(req, res, err, pathname, { service: "batches" });
|
|
1972
|
+
}
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const filesContentMatch = pathname.match(FILES_CONTENT_RE);
|
|
1976
|
+
if (filesContentMatch && req.method === "GET") {
|
|
1977
|
+
try {
|
|
1978
|
+
await handleFilesContent(req, res, filesContentMatch[1], journal, defaults, setCorsHeaders);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
routeError(req, res, err, pathname, { service: "files" });
|
|
1981
|
+
}
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
const filesIdMatch = pathname.match(FILES_ID_RE);
|
|
1985
|
+
if (filesIdMatch && req.method === "GET") {
|
|
1986
|
+
try {
|
|
1987
|
+
await handleFilesRetrieve(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
|
|
1988
|
+
} catch (err) {
|
|
1989
|
+
routeError(req, res, err, pathname, { service: "files" });
|
|
1990
|
+
}
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
if (filesIdMatch && req.method === "DELETE") {
|
|
1994
|
+
try {
|
|
1995
|
+
await handleFilesDelete(req, res, filesIdMatch[1], journal, defaults, setCorsHeaders);
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
routeError(req, res, err, pathname, { service: "files" });
|
|
1998
|
+
}
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
if (pathname === FILES_PATH && req.method === "GET") {
|
|
2002
|
+
try {
|
|
2003
|
+
await handleFilesList(req, res, journal, defaults, setCorsHeaders);
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
routeError(req, res, err, pathname, { service: "files" });
|
|
2006
|
+
}
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
if (pathname === FILES_PATH && req.method === "POST") {
|
|
2010
|
+
try {
|
|
2011
|
+
await handleFilesCreate(req, res, (await readBodyBufferBounded(req, FILES_BODY_MAX_BYTES, FILES_BODY_DRAIN_MAX_BYTES)).buffer ?? FILES_BODY_OVERSIZED, journal, defaults, setCorsHeaders);
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
routeError(req, res, err, pathname, { service: "files" });
|
|
2014
|
+
}
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const ftCancelMatch = pathname.match(FINE_TUNING_CANCEL_RE);
|
|
2018
|
+
if (ftCancelMatch && req.method === "POST") {
|
|
2019
|
+
try {
|
|
2020
|
+
await readBody(req);
|
|
2021
|
+
await handleFineTuningCancel(req, res, ftCancelMatch[1], journal, defaults, setCorsHeaders);
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
routeError(req, res, err, pathname, { service: "fine-tuning" });
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const ftEventsMatch = pathname.match(FINE_TUNING_EVENTS_RE);
|
|
2028
|
+
if (ftEventsMatch && req.method === "GET") {
|
|
2029
|
+
try {
|
|
2030
|
+
await handleFineTuningEvents(req, res, ftEventsMatch[1], journal, defaults, setCorsHeaders);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
routeError(req, res, err, pathname, { service: "fine-tuning" });
|
|
2033
|
+
}
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
const ftIdMatch = pathname.match(FINE_TUNING_ID_RE);
|
|
2037
|
+
if (ftIdMatch && req.method === "GET") {
|
|
2038
|
+
try {
|
|
2039
|
+
await handleFineTuningRetrieve(req, res, ftIdMatch[1], journal, defaults, setCorsHeaders);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
routeError(req, res, err, pathname, { service: "fine-tuning" });
|
|
2042
|
+
}
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
if (pathname === FINE_TUNING_JOBS_PATH && req.method === "GET") {
|
|
2046
|
+
try {
|
|
2047
|
+
await handleFineTuningList(req, res, journal, defaults, setCorsHeaders);
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
routeError(req, res, err, pathname, { service: "fine-tuning" });
|
|
2050
|
+
}
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (pathname === FINE_TUNING_JOBS_PATH && req.method === "POST") {
|
|
2054
|
+
try {
|
|
2055
|
+
await handleFineTuningCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
routeError(req, res, err, pathname, { service: "fine-tuning" });
|
|
2058
|
+
}
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
const vsSearchMatch = pathname.match(VECTOR_STORES_SEARCH_RE);
|
|
2062
|
+
if (vsSearchMatch && req.method === "POST") {
|
|
2063
|
+
try {
|
|
2064
|
+
const raw = await readBody(req);
|
|
2065
|
+
await handleVectorStoresSearch(req, res, vsSearchMatch[1], raw, journal, defaults, setCorsHeaders);
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2068
|
+
}
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const vsBatchFilesMatch = pathname.match(VECTOR_STORES_BATCH_FILES_RE);
|
|
2072
|
+
if (vsBatchFilesMatch && req.method === "GET") {
|
|
2073
|
+
try {
|
|
2074
|
+
await handleVectorFileBatchesFiles(req, res, vsBatchFilesMatch[1], vsBatchFilesMatch[2], journal, defaults, setCorsHeaders);
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2077
|
+
}
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
const vsBatchCancelMatch = pathname.match(VECTOR_STORES_BATCH_CANCEL_RE);
|
|
2081
|
+
if (vsBatchCancelMatch && req.method === "POST") {
|
|
2082
|
+
try {
|
|
2083
|
+
await readBody(req);
|
|
2084
|
+
await handleVectorFileBatchesCancel(req, res, vsBatchCancelMatch[1], vsBatchCancelMatch[2], journal, defaults, setCorsHeaders);
|
|
2085
|
+
} catch (err) {
|
|
2086
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2087
|
+
}
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const vsBatchMatch = pathname.match(VECTOR_STORES_BATCH_RE);
|
|
2091
|
+
if (vsBatchMatch && req.method === "GET") {
|
|
2092
|
+
try {
|
|
2093
|
+
await handleVectorFileBatchesRetrieve(req, res, vsBatchMatch[1], vsBatchMatch[2], journal, defaults, setCorsHeaders);
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const vsFileMatch = pathname.match(VECTOR_STORES_FILE_RE);
|
|
2100
|
+
if (vsFileMatch && req.method === "GET") {
|
|
2101
|
+
try {
|
|
2102
|
+
await handleVectorStoreFilesRetrieve(req, res, vsFileMatch[1], vsFileMatch[2], journal, defaults, setCorsHeaders);
|
|
2103
|
+
} catch (err) {
|
|
2104
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2105
|
+
}
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
if (vsFileMatch && req.method === "DELETE") {
|
|
2109
|
+
try {
|
|
2110
|
+
await handleVectorStoreFilesDelete(req, res, vsFileMatch[1], vsFileMatch[2], journal, defaults, setCorsHeaders);
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2113
|
+
}
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const vsIdMatch = pathname.match(VECTOR_STORES_ID_RE);
|
|
2117
|
+
if (vsIdMatch && req.method === "GET") {
|
|
2118
|
+
try {
|
|
2119
|
+
await handleVectorStoresRetrieve(req, res, vsIdMatch[1], journal, defaults, setCorsHeaders);
|
|
2120
|
+
} catch (err) {
|
|
2121
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2122
|
+
}
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
if (vsIdMatch && req.method === "POST") {
|
|
2126
|
+
try {
|
|
2127
|
+
const raw = await readBody(req);
|
|
2128
|
+
await handleVectorStoresModify(req, res, vsIdMatch[1], raw, journal, defaults, setCorsHeaders);
|
|
2129
|
+
} catch (err) {
|
|
2130
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2131
|
+
}
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
if (vsIdMatch && req.method === "DELETE") {
|
|
2135
|
+
try {
|
|
2136
|
+
await handleVectorStoresDelete(req, res, vsIdMatch[1], journal, defaults, setCorsHeaders);
|
|
2137
|
+
} catch (err) {
|
|
2138
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2139
|
+
}
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (VECTOR_STORES_FILES_RE.test(pathname) && req.method === "POST") {
|
|
2143
|
+
try {
|
|
2144
|
+
const raw = await readBody(req);
|
|
2145
|
+
const storeId = pathname.split("/")[3];
|
|
2146
|
+
await handleVectorStoreFilesCreate(req, res, storeId, raw, journal, defaults, setCorsHeaders);
|
|
2147
|
+
} catch (err) {
|
|
2148
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2149
|
+
}
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
if (VECTOR_STORES_FILES_RE.test(pathname) && req.method === "GET") {
|
|
2153
|
+
try {
|
|
2154
|
+
const storeId = pathname.split("/")[3];
|
|
2155
|
+
await handleVectorStoreFilesList(req, res, storeId, journal, defaults, setCorsHeaders);
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2158
|
+
}
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (VECTOR_STORES_FILE_BATCHES_RE.test(pathname) && req.method === "POST") {
|
|
2162
|
+
try {
|
|
2163
|
+
const raw = await readBody(req);
|
|
2164
|
+
const storeId = pathname.split("/")[3];
|
|
2165
|
+
await handleVectorFileBatchesCreate(req, res, storeId, raw, journal, defaults, setCorsHeaders);
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2168
|
+
}
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
if (pathname === VECTOR_STORES_PATH && req.method === "GET") {
|
|
2172
|
+
try {
|
|
2173
|
+
await handleVectorStoresList(req, res, journal, defaults, setCorsHeaders);
|
|
2174
|
+
} catch (err) {
|
|
2175
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2176
|
+
}
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (pathname === VECTOR_STORES_PATH && req.method === "POST") {
|
|
2180
|
+
try {
|
|
2181
|
+
await handleVectorStoresCreate(req, res, await readBody(req), journal, defaults, setCorsHeaders);
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
routeError(req, res, err, pathname, { service: "vector-stores" });
|
|
2184
|
+
}
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
1557
2187
|
if (pathname === REQUESTS_PATH) {
|
|
1558
2188
|
setCorsHeaders(res);
|
|
1559
2189
|
if (req.method === "GET") {
|
|
1560
2190
|
const limitParam = parsedUrl.searchParams.get("limit");
|
|
1561
2191
|
let opts;
|
|
1562
2192
|
if (limitParam) {
|
|
1563
|
-
const limit =
|
|
1564
|
-
if (Number.
|
|
2193
|
+
const limit = Number(limitParam);
|
|
2194
|
+
if (!/^\+?\d+$/.test(limitParam.trim()) || !Number.isSafeInteger(limit) || limit <= 0) {
|
|
1565
2195
|
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
1566
2196
|
message: `Invalid limit parameter: "${limitParam}"`,
|
|
1567
2197
|
type: "invalid_request_error"
|
|
@@ -1588,17 +2218,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1588
2218
|
try {
|
|
1589
2219
|
await handleResponses(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1590
2220
|
} catch (err) {
|
|
1591
|
-
|
|
1592
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1593
|
-
message: msg,
|
|
1594
|
-
type: "server_error"
|
|
1595
|
-
} }));
|
|
1596
|
-
else if (!res.writableEnded) try {
|
|
1597
|
-
res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1598
|
-
res.end();
|
|
1599
|
-
} catch (writeErr) {
|
|
1600
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1601
|
-
}
|
|
2221
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1602
2222
|
}
|
|
1603
2223
|
return;
|
|
1604
2224
|
}
|
|
@@ -1606,17 +2226,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1606
2226
|
try {
|
|
1607
2227
|
await handleMessages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1608
2228
|
} catch (err) {
|
|
1609
|
-
|
|
1610
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1611
|
-
message: msg,
|
|
1612
|
-
type: "server_error"
|
|
1613
|
-
} }));
|
|
1614
|
-
else if (!res.writableEnded) try {
|
|
1615
|
-
res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1616
|
-
res.end();
|
|
1617
|
-
} catch (writeErr) {
|
|
1618
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1619
|
-
}
|
|
2229
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1620
2230
|
}
|
|
1621
2231
|
return;
|
|
1622
2232
|
}
|
|
@@ -1624,17 +2234,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1624
2234
|
try {
|
|
1625
2235
|
await handleCohere(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1626
2236
|
} catch (err) {
|
|
1627
|
-
|
|
1628
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1629
|
-
message: msg,
|
|
1630
|
-
type: "server_error"
|
|
1631
|
-
} }));
|
|
1632
|
-
else if (!res.writableEnded) try {
|
|
1633
|
-
res.write(`event: error\ndata: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1634
|
-
res.end();
|
|
1635
|
-
} catch (writeErr) {
|
|
1636
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1637
|
-
}
|
|
2237
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `event: error\ndata: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1638
2238
|
}
|
|
1639
2239
|
return;
|
|
1640
2240
|
}
|
|
@@ -1642,12 +2242,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1642
2242
|
try {
|
|
1643
2243
|
await handleCohereEmbed(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1644
2244
|
} catch (err) {
|
|
1645
|
-
|
|
1646
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1647
|
-
message: msg,
|
|
1648
|
-
type: "server_error"
|
|
1649
|
-
} }));
|
|
1650
|
-
else if (!res.writableEnded) res.destroy();
|
|
2245
|
+
routeError(req, res, err, pathname);
|
|
1651
2246
|
}
|
|
1652
2247
|
return;
|
|
1653
2248
|
}
|
|
@@ -1665,14 +2260,9 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1665
2260
|
} catch (err) {
|
|
1666
2261
|
if (!(err instanceof SyntaxError)) defaults.logger.error(`Unexpected error in Azure model injection: ${err instanceof Error ? err.message : String(err)}`);
|
|
1667
2262
|
}
|
|
1668
|
-
await handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, embeddingsProvider);
|
|
2263
|
+
await handleEmbeddings(req, res, raw, fixtures, journal, defaults, setCorsHeaders, embeddingsProvider, originalPathname === EMBEDDINGS_PATH);
|
|
1669
2264
|
} catch (err) {
|
|
1670
|
-
|
|
1671
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1672
|
-
message: msg,
|
|
1673
|
-
type: "server_error"
|
|
1674
|
-
} }));
|
|
1675
|
-
else if (!res.writableEnded) res.destroy();
|
|
2265
|
+
routeError(req, res, err, pathname);
|
|
1676
2266
|
}
|
|
1677
2267
|
return;
|
|
1678
2268
|
}
|
|
@@ -1680,12 +2270,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1680
2270
|
try {
|
|
1681
2271
|
await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "openai", void 0, isBytePlusArk ? "byteplus" : void 0);
|
|
1682
2272
|
} catch (err) {
|
|
1683
|
-
|
|
1684
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1685
|
-
message: msg,
|
|
1686
|
-
type: "server_error"
|
|
1687
|
-
} }));
|
|
1688
|
-
else if (!res.writableEnded) res.destroy();
|
|
2273
|
+
routeError(req, res, err, pathname);
|
|
1689
2274
|
}
|
|
1690
2275
|
return;
|
|
1691
2276
|
}
|
|
@@ -1693,12 +2278,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1693
2278
|
try {
|
|
1694
2279
|
await handleImageEdit(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1695
2280
|
} catch (err) {
|
|
1696
|
-
|
|
1697
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1698
|
-
message: msg,
|
|
1699
|
-
type: "server_error"
|
|
1700
|
-
} }));
|
|
1701
|
-
else if (!res.writableEnded) res.destroy();
|
|
2281
|
+
routeError(req, res, err, pathname);
|
|
1702
2282
|
}
|
|
1703
2283
|
return;
|
|
1704
2284
|
}
|
|
@@ -1706,12 +2286,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1706
2286
|
try {
|
|
1707
2287
|
await handleImageVariations(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1708
2288
|
} catch (err) {
|
|
1709
|
-
|
|
1710
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1711
|
-
message: msg,
|
|
1712
|
-
type: "server_error"
|
|
1713
|
-
} }));
|
|
1714
|
-
else if (!res.writableEnded) res.destroy();
|
|
2289
|
+
routeError(req, res, err, pathname);
|
|
1715
2290
|
}
|
|
1716
2291
|
return;
|
|
1717
2292
|
}
|
|
@@ -1719,12 +2294,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1719
2294
|
try {
|
|
1720
2295
|
await handleSpeech(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1721
2296
|
} catch (err) {
|
|
1722
|
-
|
|
1723
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1724
|
-
message: msg,
|
|
1725
|
-
type: "server_error"
|
|
1726
|
-
} }));
|
|
1727
|
-
else if (!res.writableEnded) res.destroy();
|
|
2297
|
+
routeError(req, res, err, pathname);
|
|
1728
2298
|
}
|
|
1729
2299
|
return;
|
|
1730
2300
|
}
|
|
@@ -1732,12 +2302,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1732
2302
|
try {
|
|
1733
2303
|
await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1734
2304
|
} catch (err) {
|
|
1735
|
-
|
|
1736
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1737
|
-
message: msg,
|
|
1738
|
-
type: "server_error"
|
|
1739
|
-
} }));
|
|
1740
|
-
else if (!res.writableEnded) res.destroy();
|
|
2305
|
+
routeError(req, res, err, pathname);
|
|
1741
2306
|
}
|
|
1742
2307
|
return;
|
|
1743
2308
|
}
|
|
@@ -1745,12 +2310,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1745
2310
|
try {
|
|
1746
2311
|
await handleTranscription(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "translation");
|
|
1747
2312
|
} catch (err) {
|
|
1748
|
-
|
|
1749
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1750
|
-
message: msg,
|
|
1751
|
-
type: "server_error"
|
|
1752
|
-
} }));
|
|
1753
|
-
else if (!res.writableEnded) res.destroy();
|
|
2313
|
+
routeError(req, res, err, pathname);
|
|
1754
2314
|
}
|
|
1755
2315
|
return;
|
|
1756
2316
|
}
|
|
@@ -1759,28 +2319,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1759
2319
|
try {
|
|
1760
2320
|
await handleGrokVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, grokVideoJobs);
|
|
1761
2321
|
} catch (err) {
|
|
1762
|
-
|
|
1763
|
-
defaults.logger.error(`grok-video submit: ${msg}`);
|
|
1764
|
-
if (!res.headersSent) {
|
|
1765
|
-
try {
|
|
1766
|
-
journal.add({
|
|
1767
|
-
method: req.method ?? "POST",
|
|
1768
|
-
path: req.url ?? pathname,
|
|
1769
|
-
headers: flattenHeaders(req.headers),
|
|
1770
|
-
body: null,
|
|
1771
|
-
response: {
|
|
1772
|
-
status: 500,
|
|
1773
|
-
fixture: null
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
1776
|
-
} catch (jErr) {
|
|
1777
|
-
defaults.logger.warn(`grok-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1778
|
-
}
|
|
1779
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1780
|
-
message: msg,
|
|
1781
|
-
type: "server_error"
|
|
1782
|
-
} }));
|
|
1783
|
-
} else if (!res.writableEnded) res.destroy();
|
|
2322
|
+
routeError(req, res, err, pathname);
|
|
1784
2323
|
}
|
|
1785
2324
|
return;
|
|
1786
2325
|
}
|
|
@@ -1788,12 +2327,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1788
2327
|
try {
|
|
1789
2328
|
await handleVideoCreate(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, videoStates);
|
|
1790
2329
|
} catch (err) {
|
|
1791
|
-
|
|
1792
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1793
|
-
message: msg,
|
|
1794
|
-
type: "server_error"
|
|
1795
|
-
} }));
|
|
1796
|
-
else if (!res.writableEnded) res.destroy();
|
|
2330
|
+
routeError(req, res, err, pathname);
|
|
1797
2331
|
}
|
|
1798
2332
|
return;
|
|
1799
2333
|
}
|
|
@@ -1802,28 +2336,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1802
2336
|
try {
|
|
1803
2337
|
await handleGrokVideoStatus(req, res, grokVideoStatusMatch[1], fixtures, journal, defaults, setCorsHeaders, grokVideoJobs, videoStates);
|
|
1804
2338
|
} catch (err) {
|
|
1805
|
-
|
|
1806
|
-
defaults.logger.error(`grok-video status: ${msg}`);
|
|
1807
|
-
if (!res.headersSent) {
|
|
1808
|
-
try {
|
|
1809
|
-
journal.add({
|
|
1810
|
-
method: req.method ?? "GET",
|
|
1811
|
-
path: req.url ?? pathname,
|
|
1812
|
-
headers: flattenHeaders(req.headers),
|
|
1813
|
-
body: null,
|
|
1814
|
-
response: {
|
|
1815
|
-
status: 500,
|
|
1816
|
-
fixture: null
|
|
1817
|
-
}
|
|
1818
|
-
});
|
|
1819
|
-
} catch (jErr) {
|
|
1820
|
-
defaults.logger.warn(`grok-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1821
|
-
}
|
|
1822
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1823
|
-
message: msg,
|
|
1824
|
-
type: "server_error"
|
|
1825
|
-
} }));
|
|
1826
|
-
} else if (!res.writableEnded) res.destroy();
|
|
2339
|
+
routeError(req, res, err, pathname);
|
|
1827
2340
|
}
|
|
1828
2341
|
return;
|
|
1829
2342
|
}
|
|
@@ -1833,28 +2346,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1833
2346
|
try {
|
|
1834
2347
|
await handleVeoVideoCreate(req, res, await readBody(req), veoPredictMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
|
|
1835
2348
|
} catch (err) {
|
|
1836
|
-
|
|
1837
|
-
defaults.logger.error(`veo-video submit: ${msg}`);
|
|
1838
|
-
if (!res.headersSent) {
|
|
1839
|
-
try {
|
|
1840
|
-
journal.add({
|
|
1841
|
-
method: req.method ?? "POST",
|
|
1842
|
-
path: req.url ?? pathname,
|
|
1843
|
-
headers: flattenHeaders(req.headers),
|
|
1844
|
-
body: null,
|
|
1845
|
-
response: {
|
|
1846
|
-
status: 500,
|
|
1847
|
-
fixture: null
|
|
1848
|
-
}
|
|
1849
|
-
});
|
|
1850
|
-
} catch (jErr) {
|
|
1851
|
-
defaults.logger.warn(`veo-video submit: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1852
|
-
}
|
|
1853
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1854
|
-
message: msg,
|
|
1855
|
-
type: "server_error"
|
|
1856
|
-
} }));
|
|
1857
|
-
} else if (!res.writableEnded) res.destroy();
|
|
2349
|
+
routeError(req, res, err, pathname);
|
|
1858
2350
|
}
|
|
1859
2351
|
return;
|
|
1860
2352
|
}
|
|
@@ -1863,28 +2355,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1863
2355
|
try {
|
|
1864
2356
|
await handleVeoVideoStatus(req, res, veoOperationMatch[1], fixtures, journal, defaults, setCorsHeaders, veoVideoJobs);
|
|
1865
2357
|
} catch (err) {
|
|
1866
|
-
|
|
1867
|
-
defaults.logger.error(`veo-video status: ${msg}`);
|
|
1868
|
-
if (!res.headersSent) {
|
|
1869
|
-
try {
|
|
1870
|
-
journal.add({
|
|
1871
|
-
method: req.method ?? "GET",
|
|
1872
|
-
path: req.url ?? pathname,
|
|
1873
|
-
headers: flattenHeaders(req.headers),
|
|
1874
|
-
body: null,
|
|
1875
|
-
response: {
|
|
1876
|
-
status: 500,
|
|
1877
|
-
fixture: null
|
|
1878
|
-
}
|
|
1879
|
-
});
|
|
1880
|
-
} catch (jErr) {
|
|
1881
|
-
defaults.logger.warn(`veo-video status: journal write failed after handler error: ${jErr instanceof Error ? jErr.message : String(jErr)}`);
|
|
1882
|
-
}
|
|
1883
|
-
writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1884
|
-
message: msg,
|
|
1885
|
-
type: "server_error"
|
|
1886
|
-
} }));
|
|
1887
|
-
} else if (!res.writableEnded) res.destroy();
|
|
2358
|
+
routeError(req, res, err, pathname);
|
|
1888
2359
|
}
|
|
1889
2360
|
return;
|
|
1890
2361
|
}
|
|
@@ -1894,12 +2365,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1894
2365
|
try {
|
|
1895
2366
|
await handleImages(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders, "gemini", predictModel);
|
|
1896
2367
|
} catch (err) {
|
|
1897
|
-
|
|
1898
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1899
|
-
message: msg,
|
|
1900
|
-
type: "server_error"
|
|
1901
|
-
} }));
|
|
1902
|
-
else if (!res.writableEnded) res.destroy();
|
|
2368
|
+
routeError(req, res, err, pathname);
|
|
1903
2369
|
}
|
|
1904
2370
|
return;
|
|
1905
2371
|
}
|
|
@@ -1907,17 +2373,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1907
2373
|
try {
|
|
1908
2374
|
await handleGeminiInteractions(req, res, await readBody(req), fixtures, journal, defaults, setCorsHeaders);
|
|
1909
2375
|
} catch (err) {
|
|
1910
|
-
|
|
1911
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1912
|
-
message: msg,
|
|
1913
|
-
type: "server_error"
|
|
1914
|
-
} }));
|
|
1915
|
-
else if (!res.writableEnded) try {
|
|
1916
|
-
res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1917
|
-
res.end();
|
|
1918
|
-
} catch (writeErr) {
|
|
1919
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1920
|
-
}
|
|
2376
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1921
2377
|
}
|
|
1922
2378
|
return;
|
|
1923
2379
|
}
|
|
@@ -1927,12 +2383,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1927
2383
|
try {
|
|
1928
2384
|
await handleGeminiEmbedContent(req, res, await readBody(req), embedModel, fixtures, journal, defaults, setCorsHeaders);
|
|
1929
2385
|
} catch (err) {
|
|
1930
|
-
|
|
1931
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1932
|
-
message: msg,
|
|
1933
|
-
type: "server_error"
|
|
1934
|
-
} }));
|
|
1935
|
-
else if (!res.writableEnded) res.destroy();
|
|
2386
|
+
routeError(req, res, err, pathname);
|
|
1936
2387
|
}
|
|
1937
2388
|
return;
|
|
1938
2389
|
}
|
|
@@ -1943,17 +2394,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1943
2394
|
try {
|
|
1944
2395
|
await handleGemini(req, res, await readBody(req), geminiModel, streaming, fixtures, journal, defaults, setCorsHeaders);
|
|
1945
2396
|
} catch (err) {
|
|
1946
|
-
|
|
1947
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1948
|
-
message: msg,
|
|
1949
|
-
type: "server_error"
|
|
1950
|
-
} }));
|
|
1951
|
-
else if (!res.writableEnded) try {
|
|
1952
|
-
res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1953
|
-
res.end();
|
|
1954
|
-
} catch (writeErr) {
|
|
1955
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1956
|
-
}
|
|
2397
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1957
2398
|
}
|
|
1958
2399
|
return;
|
|
1959
2400
|
}
|
|
@@ -1964,17 +2405,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1964
2405
|
try {
|
|
1965
2406
|
await handleGemini(req, res, await readBody(req), vertexModel, streaming, fixtures, journal, defaults, setCorsHeaders, "vertexai");
|
|
1966
2407
|
} catch (err) {
|
|
1967
|
-
|
|
1968
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1969
|
-
message: msg,
|
|
1970
|
-
type: "server_error"
|
|
1971
|
-
} }));
|
|
1972
|
-
else if (!res.writableEnded) try {
|
|
1973
|
-
res.write(`data: ${JSON.stringify({ error: { message: msg } })}\n\n`);
|
|
1974
|
-
res.end();
|
|
1975
|
-
} catch (writeErr) {
|
|
1976
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
1977
|
-
}
|
|
2408
|
+
routeError(req, res, err, pathname, { streamEvent: (m) => `data: ${JSON.stringify({ error: { message: m } })}\n\n` });
|
|
1978
2409
|
}
|
|
1979
2410
|
return;
|
|
1980
2411
|
}
|
|
@@ -1984,12 +2415,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1984
2415
|
try {
|
|
1985
2416
|
await handleBedrock(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
|
|
1986
2417
|
} catch (err) {
|
|
1987
|
-
|
|
1988
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
1989
|
-
message: msg,
|
|
1990
|
-
type: "server_error"
|
|
1991
|
-
} }));
|
|
1992
|
-
else if (!res.writableEnded) res.destroy();
|
|
2418
|
+
routeError(req, res, err, pathname);
|
|
1993
2419
|
}
|
|
1994
2420
|
return;
|
|
1995
2421
|
}
|
|
@@ -1999,12 +2425,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
1999
2425
|
try {
|
|
2000
2426
|
await handleBedrockStream(req, res, await readBody(req), bedrockModelId, fixtures, journal, defaults, setCorsHeaders);
|
|
2001
2427
|
} catch (err) {
|
|
2002
|
-
|
|
2003
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2004
|
-
message: msg,
|
|
2005
|
-
type: "server_error"
|
|
2006
|
-
} }));
|
|
2007
|
-
else if (!res.writableEnded) res.destroy();
|
|
2428
|
+
routeError(req, res, err, pathname);
|
|
2008
2429
|
}
|
|
2009
2430
|
return;
|
|
2010
2431
|
}
|
|
@@ -2014,12 +2435,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2014
2435
|
try {
|
|
2015
2436
|
await handleConverse(req, res, await readBody(req), converseModelId, fixtures, journal, defaults, setCorsHeaders);
|
|
2016
2437
|
} catch (err) {
|
|
2017
|
-
|
|
2018
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2019
|
-
message: msg,
|
|
2020
|
-
type: "server_error"
|
|
2021
|
-
} }));
|
|
2022
|
-
else if (!res.writableEnded) res.destroy();
|
|
2438
|
+
routeError(req, res, err, pathname);
|
|
2023
2439
|
}
|
|
2024
2440
|
return;
|
|
2025
2441
|
}
|
|
@@ -2029,12 +2445,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2029
2445
|
try {
|
|
2030
2446
|
await handleConverseStream(req, res, await readBody(req), converseStreamModelId, fixtures, journal, defaults, setCorsHeaders);
|
|
2031
2447
|
} catch (err) {
|
|
2032
|
-
|
|
2033
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2034
|
-
message: msg,
|
|
2035
|
-
type: "server_error"
|
|
2036
|
-
} }));
|
|
2037
|
-
else if (!res.writableEnded) res.destroy();
|
|
2448
|
+
routeError(req, res, err, pathname);
|
|
2038
2449
|
}
|
|
2039
2450
|
return;
|
|
2040
2451
|
}
|
|
@@ -2042,12 +2453,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2042
2453
|
try {
|
|
2043
2454
|
await handleSearch(req, res, await readBody(req), serviceFixtures?.search ?? [], journal, defaults, setCorsHeaders);
|
|
2044
2455
|
} catch (err) {
|
|
2045
|
-
|
|
2046
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2047
|
-
message: msg,
|
|
2048
|
-
type: "server_error"
|
|
2049
|
-
} }));
|
|
2050
|
-
else if (!res.writableEnded) res.destroy();
|
|
2456
|
+
routeError(req, res, err, pathname, { service: "search" });
|
|
2051
2457
|
}
|
|
2052
2458
|
return;
|
|
2053
2459
|
}
|
|
@@ -2055,12 +2461,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2055
2461
|
try {
|
|
2056
2462
|
await handleRerank(req, res, await readBody(req), serviceFixtures?.rerank ?? [], journal, defaults, setCorsHeaders);
|
|
2057
2463
|
} catch (err) {
|
|
2058
|
-
|
|
2059
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2060
|
-
message: msg,
|
|
2061
|
-
type: "server_error"
|
|
2062
|
-
} }));
|
|
2063
|
-
else if (!res.writableEnded) res.destroy();
|
|
2464
|
+
routeError(req, res, err, pathname, { service: "rerank" });
|
|
2064
2465
|
}
|
|
2065
2466
|
return;
|
|
2066
2467
|
}
|
|
@@ -2068,12 +2469,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2068
2469
|
try {
|
|
2069
2470
|
await handleModeration(req, res, await readBody(req), serviceFixtures?.moderation ?? [], journal, defaults, setCorsHeaders);
|
|
2070
2471
|
} catch (err) {
|
|
2071
|
-
|
|
2072
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2073
|
-
message: msg,
|
|
2074
|
-
type: "server_error"
|
|
2075
|
-
} }));
|
|
2076
|
-
else if (!res.writableEnded) res.destroy();
|
|
2472
|
+
routeError(req, res, err, pathname, { service: "moderation" });
|
|
2077
2473
|
}
|
|
2078
2474
|
return;
|
|
2079
2475
|
}
|
|
@@ -2082,12 +2478,57 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2082
2478
|
try {
|
|
2083
2479
|
await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, "sound-generation");
|
|
2084
2480
|
} catch (err) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2481
|
+
routeError(req, res, err, pathname);
|
|
2482
|
+
}
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
if (pathname === ELEVENLABS_VOICE_DESIGN_PATH && req.method === "POST") {
|
|
2486
|
+
setCorsHeaders(res);
|
|
2487
|
+
try {
|
|
2488
|
+
await handleElevenLabsVoiceDesign(req, res, await readBody(req), fixtures, defaults, journal);
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
|
|
2491
|
+
}
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
if (pathname === ELEVENLABS_VOICE_CREATE_PATH && req.method === "POST") {
|
|
2495
|
+
setCorsHeaders(res);
|
|
2496
|
+
try {
|
|
2497
|
+
await handleElevenLabsVoiceCreate(req, res, await readBody(req), fixtures, defaults, journal);
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
|
|
2500
|
+
}
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const elevenLabsVoiceMatch = pathname.match(ELEVENLABS_VOICE_RE);
|
|
2504
|
+
if (elevenLabsVoiceMatch && (req.method === "GET" || req.method === "DELETE")) {
|
|
2505
|
+
setCorsHeaders(res);
|
|
2506
|
+
let voiceId;
|
|
2507
|
+
try {
|
|
2508
|
+
voiceId = decodeURIComponent(elevenLabsVoiceMatch[1]);
|
|
2509
|
+
} catch {
|
|
2510
|
+
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
2511
|
+
message: `Invalid voice id '${elevenLabsVoiceMatch[1]}': malformed percent-encoding`,
|
|
2512
|
+
type: "invalid_request_error"
|
|
2089
2513
|
} }));
|
|
2090
|
-
|
|
2514
|
+
journal.add({
|
|
2515
|
+
method: req.method ?? "GET",
|
|
2516
|
+
path: req.url ?? pathname,
|
|
2517
|
+
headers: flattenHeaders(req.headers),
|
|
2518
|
+
service: "elevenlabs-voice",
|
|
2519
|
+
body: null,
|
|
2520
|
+
response: {
|
|
2521
|
+
status: 400,
|
|
2522
|
+
fixture: null
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
try {
|
|
2528
|
+
if (req.method === "GET") await handleElevenLabsVoiceGet(req, res, voiceId, fixtures, defaults, journal);
|
|
2529
|
+
else await handleElevenLabsVoiceDelete(req, res, voiceId, fixtures, defaults, journal);
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
routeError(req, res, err, pathname, { service: "elevenlabs-voice" });
|
|
2091
2532
|
}
|
|
2092
2533
|
return;
|
|
2093
2534
|
}
|
|
@@ -2098,12 +2539,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2098
2539
|
try {
|
|
2099
2540
|
await handleElevenLabsTTS(req, res, await readBody(req), fixtures, defaults, journal, voiceId);
|
|
2100
2541
|
} catch (err) {
|
|
2101
|
-
|
|
2102
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2103
|
-
message: msg,
|
|
2104
|
-
type: "server_error"
|
|
2105
|
-
} }));
|
|
2106
|
-
else if (!res.writableEnded) res.destroy();
|
|
2542
|
+
routeError(req, res, err, pathname);
|
|
2107
2543
|
}
|
|
2108
2544
|
return;
|
|
2109
2545
|
}
|
|
@@ -2114,42 +2550,38 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2114
2550
|
try {
|
|
2115
2551
|
await handleElevenLabsAudio(req, res, await readBody(req), fixtures, defaults, journal, musicSubType);
|
|
2116
2552
|
} catch (err) {
|
|
2117
|
-
|
|
2118
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2119
|
-
message: msg,
|
|
2120
|
-
type: "server_error"
|
|
2121
|
-
} }));
|
|
2122
|
-
else if (!res.writableEnded) res.destroy();
|
|
2553
|
+
routeError(req, res, err, pathname);
|
|
2123
2554
|
}
|
|
2124
2555
|
return;
|
|
2125
2556
|
}
|
|
2126
2557
|
let falBody;
|
|
2127
|
-
if (
|
|
2558
|
+
if (FAL_ROUTE_RE.test(pathname) && req.headers["x-fal-target-host"]) {
|
|
2128
2559
|
setCorsHeaders(res);
|
|
2129
2560
|
try {
|
|
2130
2561
|
falBody = req.method === "POST" || req.method === "PUT" ? await readBody(req) : "";
|
|
2131
2562
|
const raw = falBody;
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2563
|
+
if (falWillHandle(req, pathname)) {
|
|
2564
|
+
const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
2565
|
+
await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
|
|
2566
|
+
const goneAfterLatency = responseGoneReason(res);
|
|
2567
|
+
if (goneAfterLatency !== null) {
|
|
2568
|
+
defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
|
|
2572
|
+
if (chaosAction) {
|
|
2573
|
+
applyChaosAction(chaosAction, res, null, journal, {
|
|
2574
|
+
method: req.method ?? "GET",
|
|
2575
|
+
path: pathname,
|
|
2576
|
+
headers: flattenHeaders(req.headers),
|
|
2577
|
+
body: null
|
|
2578
|
+
}, "internal", defaults.registry, defaults.logger);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2144
2581
|
}
|
|
2145
2582
|
if (await handleFal(req, res, raw, pathname, fixtures, defaults, journal) === "handled") return;
|
|
2146
2583
|
} catch (err) {
|
|
2147
|
-
|
|
2148
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2149
|
-
message: msg,
|
|
2150
|
-
type: "server_error"
|
|
2151
|
-
} }));
|
|
2152
|
-
else if (!res.writableEnded) res.destroy();
|
|
2584
|
+
routeError(req, res, err, pathname);
|
|
2153
2585
|
return;
|
|
2154
2586
|
}
|
|
2155
2587
|
}
|
|
@@ -2158,12 +2590,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2158
2590
|
try {
|
|
2159
2591
|
await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
|
|
2160
2592
|
} catch (err) {
|
|
2161
|
-
|
|
2162
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2163
|
-
message: msg,
|
|
2164
|
-
type: "server_error"
|
|
2165
|
-
} }));
|
|
2166
|
-
else if (!res.writableEnded) res.destroy();
|
|
2593
|
+
routeError(req, res, err, pathname);
|
|
2167
2594
|
}
|
|
2168
2595
|
return;
|
|
2169
2596
|
}
|
|
@@ -2171,27 +2598,26 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2171
2598
|
setCorsHeaders(res);
|
|
2172
2599
|
try {
|
|
2173
2600
|
const raw = req.method === "POST" || req.method === "PUT" ? falBody ?? await readBody(req) : "{}";
|
|
2174
|
-
const
|
|
2601
|
+
const chaosConfig = resolveChaosConfig(null, defaults.chaos, req.headers, defaults.logger, req.url);
|
|
2602
|
+
await awaitChaosLatency(null, defaults.chaos, req.headers, defaults.logger, req.url, res, chaosConfig);
|
|
2603
|
+
const goneAfterLatency = responseGoneReason(res);
|
|
2604
|
+
if (goneAfterLatency !== null) {
|
|
2605
|
+
defaults.logger.debug(`[chaos] ${req.method ?? "GET"} ${pathname}: ${describeUnwritableReason(goneAfterLatency)} after the latency delay — not served, not journalled`);
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const chaosAction = evaluateChaos(null, defaults.chaos, req.headers, defaults.logger, req.url, chaosConfig);
|
|
2175
2609
|
if (chaosAction) {
|
|
2176
2610
|
applyChaosAction(chaosAction, res, null, journal, {
|
|
2177
2611
|
method: req.method ?? "GET",
|
|
2178
2612
|
path: pathname,
|
|
2179
2613
|
headers: flattenHeaders(req.headers),
|
|
2180
|
-
body:
|
|
2181
|
-
|
|
2182
|
-
messages: []
|
|
2183
|
-
}
|
|
2184
|
-
}, "fixture", defaults.registry);
|
|
2614
|
+
body: null
|
|
2615
|
+
}, "internal", defaults.registry, defaults.logger);
|
|
2185
2616
|
return;
|
|
2186
2617
|
}
|
|
2187
2618
|
await handleFalQueue(req, res, raw, pathname, fixtures, defaults, journal);
|
|
2188
2619
|
} catch (err) {
|
|
2189
|
-
|
|
2190
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2191
|
-
message: msg,
|
|
2192
|
-
type: "server_error"
|
|
2193
|
-
} }));
|
|
2194
|
-
else if (!res.writableEnded) res.destroy();
|
|
2620
|
+
routeError(req, res, err, pathname);
|
|
2195
2621
|
}
|
|
2196
2622
|
return;
|
|
2197
2623
|
}
|
|
@@ -2200,16 +2626,13 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2200
2626
|
try {
|
|
2201
2627
|
await handleFalQueue(req, res, falBody ?? await readBody(req), pathname, fixtures, defaults, journal);
|
|
2202
2628
|
} catch (err) {
|
|
2203
|
-
|
|
2204
|
-
if (!res.headersSent) writeErrorResponse(res, 500, JSON.stringify({ error: {
|
|
2205
|
-
message: msg,
|
|
2206
|
-
type: "server_error"
|
|
2207
|
-
} }));
|
|
2208
|
-
else if (!res.writableEnded) res.destroy();
|
|
2629
|
+
routeError(req, res, err, pathname);
|
|
2209
2630
|
}
|
|
2210
2631
|
return;
|
|
2211
2632
|
}
|
|
2212
2633
|
if (pathname !== COMPLETIONS_PATH) {
|
|
2634
|
+
const catalogMatch = matchRouteDefinition(req.method ?? "", pathname);
|
|
2635
|
+
if (catalogMatch && !(pathname === METRICS_PATH && !registry)) defaults.logger.warn(`Route drift: ${req.method ?? "?"} ${pathname} matches the route catalog (${catalogMatch.method} ${catalogMatch.path} — ${catalogMatch.description}) but no dispatcher branch served it. The catalog entry or the dispatcher is stale — see route-registry.ts.`);
|
|
2213
2636
|
handleNotFound(res, "Not found");
|
|
2214
2637
|
return;
|
|
2215
2638
|
}
|
|
@@ -2221,23 +2644,25 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2221
2644
|
try {
|
|
2222
2645
|
await handleCompletions(req, res, fixtures, journal, defaults, azureDeploymentId, completionsProvider, isOpenRouter);
|
|
2223
2646
|
} catch (err) {
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
res.end();
|
|
2235
|
-
} catch (writeErr) {
|
|
2236
|
-
logger.debug("Failed to write error recovery response:", writeErr);
|
|
2237
|
-
}
|
|
2647
|
+
routeError(req, res, err, pathname, {
|
|
2648
|
+
envelope: (m, status) => isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
|
|
2649
|
+
message: m,
|
|
2650
|
+
type: errorTypeForStatus(status)
|
|
2651
|
+
} }),
|
|
2652
|
+
streamEvent: (m, status) => `data: ${isOpenRouter ? serializeOpenRouterError(status, m) : JSON.stringify({ error: {
|
|
2653
|
+
message: m,
|
|
2654
|
+
type: errorTypeForStatus(status)
|
|
2655
|
+
} })}\n\n`
|
|
2656
|
+
});
|
|
2238
2657
|
}
|
|
2239
2658
|
}
|
|
2240
2659
|
const activeConnections = /* @__PURE__ */ new Set();
|
|
2660
|
+
const liveLimits = normalizeLiveOptions(options?.live);
|
|
2661
|
+
const liveSessions = /* @__PURE__ */ new Map();
|
|
2662
|
+
const closeLiveSessions = (testId) => {
|
|
2663
|
+
for (const session of [...liveSessions.values()]) if (testId === void 0 || session.testId === testId) session.dispose();
|
|
2664
|
+
};
|
|
2665
|
+
liveClosers.set(defaults, closeLiveSessions);
|
|
2241
2666
|
server.on("upgrade", (req, socket, head) => {
|
|
2242
2667
|
handleUpgradeRequest(req, socket, head).catch((err) => {
|
|
2243
2668
|
const msg = err instanceof Error ? err.message : "Internal error";
|
|
@@ -2287,30 +2712,57 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2287
2712
|
server.emit("connection", socket);
|
|
2288
2713
|
return;
|
|
2289
2714
|
}
|
|
2290
|
-
|
|
2715
|
+
let parsedUrl;
|
|
2716
|
+
try {
|
|
2717
|
+
parsedUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
2718
|
+
} catch (err) {
|
|
2719
|
+
const msg = err instanceof Error ? err.message : "Internal error";
|
|
2720
|
+
defaults.logger.warn(`Unhandled upgrade error: ${msg}`);
|
|
2721
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2291
2724
|
let pathname = parsedUrl.pathname;
|
|
2292
2725
|
if (!validateRequestApiKey(req, resolvedAuth.policy).ok) {
|
|
2293
2726
|
writeApiKeyUpgradeRejection(socket);
|
|
2294
2727
|
return;
|
|
2295
2728
|
}
|
|
2296
|
-
if (
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
if (await handler.handleUpgrade(socket, head, subPath)) return;
|
|
2300
|
-
}
|
|
2729
|
+
for (const { path: mountPath, handler } of mountList) if ((pathname === mountPath || pathname.startsWith(mountPath + "/")) && handler.handleUpgrade) {
|
|
2730
|
+
const subPath = pathname.slice(mountPath.length) || "/";
|
|
2731
|
+
if (await handler.handleUpgrade(socket, head, subPath)) return;
|
|
2301
2732
|
}
|
|
2302
2733
|
if (!pathname.match(AZURE_DEPLOYMENT_RE)) pathname = normalizeCompatPath(pathname, logger);
|
|
2303
|
-
if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH) {
|
|
2734
|
+
if (pathname !== RESPONSES_PATH && pathname !== REALTIME_PATH && pathname !== GEMINI_LIVE_PATH && pathname !== LIVE_PATH) {
|
|
2304
2735
|
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
2305
2736
|
socket.destroy();
|
|
2306
2737
|
return;
|
|
2307
2738
|
}
|
|
2739
|
+
let liveId;
|
|
2740
|
+
if (pathname === LIVE_PATH) {
|
|
2741
|
+
if (req.method !== "GET" || parsedUrl.searchParams.has("model")) {
|
|
2742
|
+
socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
if (liveSessions.size >= liveLimits.maxSessions) {
|
|
2746
|
+
socket.end("HTTP/1.1 429 Too Many Requests\r\nConnection: close\r\n\r\n");
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
liveId = Symbol("live-session");
|
|
2750
|
+
const reservedId = liveId;
|
|
2751
|
+
liveSessions.set(reservedId, {
|
|
2752
|
+
testId: getTestId(req),
|
|
2753
|
+
dispose: () => {
|
|
2754
|
+
socket.destroy();
|
|
2755
|
+
liveSessions.delete(reservedId);
|
|
2756
|
+
}
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2308
2759
|
if (head.length > 0) socket.unshift(head);
|
|
2309
2760
|
let ws;
|
|
2310
2761
|
try {
|
|
2311
|
-
ws = upgradeToWebSocket(req, socket);
|
|
2762
|
+
ws = upgradeToWebSocket(req, socket, pathname === LIVE_PATH ? liveLimits : void 0);
|
|
2312
2763
|
} catch (err) {
|
|
2313
2764
|
const msg = err instanceof Error ? err.message : "WebSocket upgrade failed";
|
|
2765
|
+
if (liveId !== void 0) liveSessions.delete(liveId);
|
|
2314
2766
|
logger.error(`WebSocket upgrade error: ${msg}`);
|
|
2315
2767
|
if (!socket.destroyed) socket.destroy();
|
|
2316
2768
|
return;
|
|
@@ -2324,7 +2776,24 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2324
2776
|
activeConnections.delete(ws);
|
|
2325
2777
|
});
|
|
2326
2778
|
const wsTestId = getTestId(req);
|
|
2327
|
-
if (pathname ===
|
|
2779
|
+
if (pathname === LIVE_PATH && liveId !== void 0) {
|
|
2780
|
+
const sessionId = liveId;
|
|
2781
|
+
const dispose = handleLiveSession(ws, fixtures, journal, {
|
|
2782
|
+
defaults,
|
|
2783
|
+
limits: liveLimits,
|
|
2784
|
+
localApiKeys: resolvedAuth.publicConfig?.apiKeys,
|
|
2785
|
+
testId: wsTestId,
|
|
2786
|
+
headers: req.headers,
|
|
2787
|
+
onDispose: () => {
|
|
2788
|
+
liveSessions.delete(sessionId);
|
|
2789
|
+
activeConnections.delete(ws);
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
liveSessions.set(sessionId, {
|
|
2793
|
+
testId: wsTestId,
|
|
2794
|
+
dispose
|
|
2795
|
+
});
|
|
2796
|
+
} else if (pathname === RESPONSES_PATH) handleWebSocketResponses(ws, fixtures, journal, {
|
|
2328
2797
|
...defaults,
|
|
2329
2798
|
model: "gpt-4",
|
|
2330
2799
|
testId: wsTestId,
|
|
@@ -2349,6 +2818,7 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2349
2818
|
}
|
|
2350
2819
|
const originalClose = server.close.bind(server);
|
|
2351
2820
|
server.close = function(callback) {
|
|
2821
|
+
closeLiveSessions();
|
|
2352
2822
|
for (const ws of activeConnections) ws.close(1001, "Server shutting down");
|
|
2353
2823
|
activeConnections.clear();
|
|
2354
2824
|
videoStates.clear();
|
|
@@ -2368,10 +2838,15 @@ async function createServerWithResolvedAuth(fixtures, options, resolvedAuth, mou
|
|
|
2368
2838
|
return;
|
|
2369
2839
|
}
|
|
2370
2840
|
const url = `http://${addr.address}:${addr.port}`;
|
|
2371
|
-
|
|
2372
|
-
|
|
2841
|
+
try {
|
|
2842
|
+
commitHandOff();
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
server.close(() => reject(err));
|
|
2845
|
+
return;
|
|
2373
2846
|
}
|
|
2847
|
+
for (const { path: mountPath, handler } of mountList) if (handler.setBaseUrl) handler.setBaseUrl(url + mountPath);
|
|
2374
2848
|
resolve({
|
|
2849
|
+
closeLiveSessions,
|
|
2375
2850
|
server,
|
|
2376
2851
|
journal,
|
|
2377
2852
|
url,
|