@copilotkit/aimock 1.45.0 → 1.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +7 -1
- package/dist/agui-mock.cjs +25 -25
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +25 -25
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +15 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +16 -2
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +10 -9
- package/dist/api-key-auth.cjs.map +1 -1
- package/dist/api-key-auth.d.cts.map +1 -1
- package/dist/api-key-auth.d.ts.map +1 -1
- package/dist/api-key-auth.js +11 -10
- package/dist/api-key-auth.js.map +1 -1
- package/dist/batches.cjs +2 -4
- package/dist/batches.cjs.map +1 -1
- package/dist/batches.d.cts.map +1 -1
- package/dist/batches.d.ts.map +1 -1
- package/dist/batches.js +2 -4
- package/dist/batches.js.map +1 -1
- package/dist/bedrock-converse.cjs +61 -80
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +58 -77
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +65 -97
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +62 -94
- package/dist/bedrock.js.map +1 -1
- package/dist/byteplus-video.cjs +11 -10
- package/dist/byteplus-video.cjs.map +1 -1
- package/dist/byteplus-video.d.cts.map +1 -1
- package/dist/byteplus-video.d.ts.map +1 -1
- package/dist/byteplus-video.js +5 -4
- package/dist/byteplus-video.js.map +1 -1
- package/dist/chaos.cjs +5 -4
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +5 -4
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +15 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +49 -74
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +47 -72
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +29 -1
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +7 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +7 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +29 -2
- package/dist/config-loader.js.map +1 -1
- package/dist/count-tokens.cjs +146 -0
- package/dist/count-tokens.cjs.map +1 -0
- package/dist/count-tokens.js +146 -0
- package/dist/count-tokens.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +36 -34
- package/dist/elevenlabs-audio.cjs.map +1 -1
- package/dist/elevenlabs-audio.d.cts.map +1 -1
- package/dist/elevenlabs-audio.d.ts.map +1 -1
- package/dist/elevenlabs-audio.js +33 -31
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/elevenlabs-voice.cjs +38 -37
- package/dist/elevenlabs-voice.cjs.map +1 -1
- package/dist/elevenlabs-voice.d.cts.map +1 -1
- package/dist/elevenlabs-voice.d.ts.map +1 -1
- package/dist/elevenlabs-voice.js +33 -32
- package/dist/elevenlabs-voice.js.map +1 -1
- package/dist/embedding-request.cjs +16 -0
- package/dist/embedding-request.cjs.map +1 -0
- package/dist/embedding-request.js +14 -0
- package/dist/embedding-request.js.map +1 -0
- package/dist/embeddings.cjs +30 -20
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +30 -20
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +45 -43
- package/dist/fal-audio.cjs.map +1 -1
- package/dist/fal-audio.d.cts.map +1 -1
- package/dist/fal-audio.d.ts.map +1 -1
- package/dist/fal-audio.js +42 -40
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +17 -15
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.js +16 -14
- package/dist/fal.js.map +1 -1
- package/dist/files.cjs +2 -4
- package/dist/files.cjs.map +1 -1
- package/dist/files.d.cts.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -4
- package/dist/files.js.map +1 -1
- package/dist/fine-tuning.cjs +2 -4
- package/dist/fine-tuning.cjs.map +1 -1
- package/dist/fine-tuning.d.cts.map +1 -1
- package/dist/fine-tuning.d.ts.map +1 -1
- package/dist/fine-tuning.js +2 -4
- package/dist/fine-tuning.js.map +1 -1
- package/dist/fixture-loader.cjs +37 -42
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +37 -42
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-embeddings.cjs +26 -24
- package/dist/gemini-embeddings.cjs.map +1 -1
- package/dist/gemini-embeddings.js +26 -24
- package/dist/gemini-embeddings.js.map +1 -1
- package/dist/gemini-interactions.cjs +50 -35
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts +0 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts +0 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +49 -34
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +57 -67
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +56 -67
- package/dist/gemini.js.map +1 -1
- package/dist/grok-video.cjs +14 -12
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.d.cts.map +1 -1
- package/dist/grok-video.d.ts.map +1 -1
- package/dist/grok-video.js +8 -6
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +11 -68
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +12 -67
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +30 -28
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +27 -25
- package/dist/images.js.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/journal.cjs +3 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +2 -1
- package/dist/journal.js.map +1 -1
- package/dist/llmock.cjs +3 -0
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +3 -0
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-handler.cjs +14 -4
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.js +14 -4
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-recorder.cjs +2 -1
- package/dist/mcp-recorder.cjs.map +1 -1
- package/dist/mcp-recorder.js +3 -2
- package/dist/mcp-recorder.js.map +1 -1
- package/dist/messages.cjs +111 -96
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +109 -95
- package/dist/messages.js.map +1 -1
- package/dist/metrics.cjs +5 -2
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.cts.map +1 -1
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +6 -3
- package/dist/metrics.js.map +1 -1
- package/dist/model-utils.cjs +1 -0
- package/dist/model-utils.cjs.map +1 -1
- package/dist/model-utils.js +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/moderation.cjs +15 -12
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +14 -11
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +128 -68
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +124 -64
- package/dist/ollama.js.map +1 -1
- package/dist/openapi.cjs +323 -3
- package/dist/openapi.cjs.map +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +324 -4
- package/dist/openapi.js.map +1 -1
- package/dist/openrouter-chat.cjs +11 -155
- package/dist/openrouter-chat.cjs.map +1 -1
- package/dist/openrouter-chat.js +11 -155
- package/dist/openrouter-chat.js.map +1 -1
- package/dist/openrouter-video.cjs +76 -71
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +63 -58
- package/dist/openrouter-video.js.map +1 -1
- package/dist/recorder.cjs +176 -95
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +176 -95
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +13 -11
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +12 -10
- package/dist/rerank.js.map +1 -1
- package/dist/response/catalog/anthropic-page.cjs +39 -0
- package/dist/response/catalog/anthropic-page.cjs.map +1 -0
- package/dist/response/catalog/anthropic-page.js +38 -0
- package/dist/response/catalog/anthropic-page.js.map +1 -0
- package/dist/response/catalog/collect.cjs +61 -0
- package/dist/response/catalog/collect.cjs.map +1 -0
- package/dist/response/catalog/collect.js +59 -0
- package/dist/response/catalog/collect.js.map +1 -0
- package/dist/response/catalog/gemini-page.cjs +25 -0
- package/dist/response/catalog/gemini-page.cjs.map +1 -0
- package/dist/response/catalog/gemini-page.js +24 -0
- package/dist/response/catalog/gemini-page.js.map +1 -0
- package/dist/response/catalog/render.cjs +182 -0
- package/dist/response/catalog/render.cjs.map +1 -0
- package/dist/response/catalog/render.js +171 -0
- package/dist/response/catalog/render.js.map +1 -0
- package/dist/response/errors.cjs +225 -0
- package/dist/response/errors.cjs.map +1 -0
- package/dist/response/errors.js +224 -0
- package/dist/response/errors.js.map +1 -0
- package/dist/response/finish.cjs +39 -0
- package/dist/response/finish.cjs.map +1 -0
- package/dist/response/finish.js +35 -0
- package/dist/response/finish.js.map +1 -0
- package/dist/response/policy.cjs +126 -0
- package/dist/response/policy.cjs.map +1 -0
- package/dist/response/policy.js +120 -0
- package/dist/response/policy.js.map +1 -0
- package/dist/response/profiles.cjs +69 -0
- package/dist/response/profiles.cjs.map +1 -0
- package/dist/response/profiles.js +65 -0
- package/dist/response/profiles.js.map +1 -0
- package/dist/response/usage/codecs/anthropic.cjs +52 -0
- package/dist/response/usage/codecs/anthropic.cjs.map +1 -0
- package/dist/response/usage/codecs/anthropic.js +50 -0
- package/dist/response/usage/codecs/anthropic.js.map +1 -0
- package/dist/response/usage/codecs/cohere.cjs +36 -0
- package/dist/response/usage/codecs/cohere.cjs.map +1 -0
- package/dist/response/usage/codecs/cohere.js +35 -0
- package/dist/response/usage/codecs/cohere.js.map +1 -0
- package/dist/response/usage/codecs/converse.cjs +16 -0
- package/dist/response/usage/codecs/converse.cjs.map +1 -0
- package/dist/response/usage/codecs/converse.js +15 -0
- package/dist/response/usage/codecs/converse.js.map +1 -0
- package/dist/response/usage/codecs/gemini.cjs +31 -0
- package/dist/response/usage/codecs/gemini.cjs.map +1 -0
- package/dist/response/usage/codecs/gemini.js +31 -0
- package/dist/response/usage/codecs/gemini.js.map +1 -0
- package/dist/response/usage/codecs/interactions.cjs +22 -0
- package/dist/response/usage/codecs/interactions.cjs.map +1 -0
- package/dist/response/usage/codecs/interactions.js +21 -0
- package/dist/response/usage/codecs/interactions.js.map +1 -0
- package/dist/response/usage/codecs/ollama.cjs +17 -0
- package/dist/response/usage/codecs/ollama.cjs.map +1 -0
- package/dist/response/usage/codecs/ollama.js +17 -0
- package/dist/response/usage/codecs/ollama.js.map +1 -0
- package/dist/response/usage/codecs/openai.cjs +52 -0
- package/dist/response/usage/codecs/openai.cjs.map +1 -0
- package/dist/response/usage/codecs/openai.js +50 -0
- package/dist/response/usage/codecs/openai.js.map +1 -0
- package/dist/response/usage/codecs/openrouter.cjs +68 -0
- package/dist/response/usage/codecs/openrouter.cjs.map +1 -0
- package/dist/response/usage/codecs/openrouter.js +67 -0
- package/dist/response/usage/codecs/openrouter.js.map +1 -0
- package/dist/response/usage/codecs/realtime.cjs +19 -0
- package/dist/response/usage/codecs/realtime.cjs.map +1 -0
- package/dist/response/usage/codecs/realtime.js +18 -0
- package/dist/response/usage/codecs/realtime.js.map +1 -0
- package/dist/response/usage/codecs/responses.cjs +21 -0
- package/dist/response/usage/codecs/responses.cjs.map +1 -0
- package/dist/response/usage/codecs/responses.js +20 -0
- package/dist/response/usage/codecs/responses.js.map +1 -0
- package/dist/response/usage/estimate.cjs +23 -0
- package/dist/response/usage/estimate.cjs.map +1 -0
- package/dist/response/usage/estimate.js +21 -0
- package/dist/response/usage/estimate.js.map +1 -0
- package/dist/response/usage/fields.cjs +72 -0
- package/dist/response/usage/fields.cjs.map +1 -0
- package/dist/response/usage/fields.js +70 -0
- package/dist/response/usage/fields.js.map +1 -0
- package/dist/response/usage/recording.cjs +52 -0
- package/dist/response/usage/recording.cjs.map +1 -0
- package/dist/response/usage/recording.js +52 -0
- package/dist/response/usage/recording.js.map +1 -0
- package/dist/responses.cjs +45 -58
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +44 -57
- package/dist/responses.js.map +1 -1
- package/dist/route-registry.cjs +45 -0
- package/dist/route-registry.cjs.map +1 -1
- package/dist/route-registry.d.cts.map +1 -1
- package/dist/route-registry.d.ts.map +1 -1
- package/dist/route-registry.js +41 -1
- package/dist/route-registry.js.map +1 -1
- package/dist/router.cjs +4 -0
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +3 -0
- package/dist/router.js.map +1 -1
- package/dist/search.cjs +13 -11
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +12 -10
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +222 -146
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +223 -147
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -18
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +19 -17
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.cjs +7 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +7 -1
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +118 -11
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +10 -9
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +10 -9
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +118 -11
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +12 -10
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +11 -9
- package/dist/transcription.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +62 -3
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +62 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vector-mock.cjs +3 -8
- package/dist/vector-mock.cjs.map +1 -1
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +3 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-stores.cjs +2 -4
- package/dist/vector-stores.cjs.map +1 -1
- package/dist/vector-stores.d.cts.map +1 -1
- package/dist/vector-stores.d.ts.map +1 -1
- package/dist/vector-stores.js +2 -4
- package/dist/vector-stores.js.map +1 -1
- package/dist/veo-video.cjs +16 -14
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -8
- package/dist/veo-video.js.map +1 -1
- package/dist/video.cjs +21 -19
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +20 -18
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +11 -3
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +0 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +0 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +10 -2
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-live.cjs +6 -5
- package/dist/ws-live.cjs.map +1 -1
- package/dist/ws-live.js +5 -4
- package/dist/ws-live.js.map +1 -1
- package/dist/ws-realtime.cjs +28 -54
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +25 -51
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +8 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.js +7 -5
- package/dist/ws-responses.js.map +1 -1
- package/package.json +4 -1
- package/skills/write-fixtures/SKILL.md +25 -16
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const require_errors = require('./response/errors.cjs');
|
|
2
|
+
const require_policy = require('./response/policy.cjs');
|
|
1
3
|
const require_helpers = require('./helpers.cjs');
|
|
2
4
|
const require_sse_writer = require('./sse-writer.cjs');
|
|
3
5
|
const require_chaos = require('./chaos.cjs');
|
|
@@ -24,11 +26,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
24
26
|
fixture: null
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
29
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("gemini", "validation", {
|
|
28
30
|
message: `Malformed JSON body: ${detail}`,
|
|
29
|
-
|
|
31
|
+
bodyCode: 400,
|
|
30
32
|
status: "INVALID_ARGUMENT"
|
|
31
|
-
}
|
|
33
|
+
}).body);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
if (!require_helpers.isJsonObject(embedReq)) {
|
|
@@ -42,11 +44,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
42
44
|
fixture: null
|
|
43
45
|
}
|
|
44
46
|
});
|
|
45
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
47
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("gemini", "validation", {
|
|
46
48
|
message: "Request body must be a JSON object",
|
|
47
|
-
|
|
49
|
+
bodyCode: 400,
|
|
48
50
|
status: "INVALID_ARGUMENT"
|
|
49
|
-
}
|
|
51
|
+
}).body);
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
const parts = embedReq.content?.parts ?? [];
|
|
@@ -62,11 +64,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
62
64
|
fixture: null
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
67
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("gemini", "validation", {
|
|
66
68
|
message: partsError,
|
|
67
|
-
|
|
69
|
+
bodyCode: 400,
|
|
68
70
|
status: "INVALID_ARGUMENT"
|
|
69
|
-
}
|
|
71
|
+
}).body);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
const inputText = parts.filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
|
|
@@ -104,12 +106,12 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
104
106
|
fixture
|
|
105
107
|
}
|
|
106
108
|
});
|
|
107
|
-
const geminiError =
|
|
108
|
-
|
|
109
|
+
const geminiError = require_errors.renderError("gemini", "fixture", {
|
|
110
|
+
bodyCode: status,
|
|
109
111
|
message: response.error.message,
|
|
110
112
|
status: response.error.type ?? "ERROR"
|
|
111
|
-
}
|
|
112
|
-
require_sse_writer.writeErrorResponse(res, status,
|
|
113
|
+
}).body;
|
|
114
|
+
require_sse_writer.writeErrorResponse(res, status, geminiError, { retryAfter: response.retryAfter });
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
if (require_helpers.isEmbeddingResponse(response)) {
|
|
@@ -138,14 +140,14 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
138
140
|
fixture
|
|
139
141
|
}
|
|
140
142
|
});
|
|
141
|
-
require_sse_writer.writeErrorResponse(res, 500,
|
|
143
|
+
require_sse_writer.writeErrorResponse(res, 500, require_errors.renderError("gemini", "unknown-response", {
|
|
142
144
|
message: "Fixture response did not match any known embedding type (must have embedding or error)",
|
|
143
|
-
|
|
145
|
+
bodyCode: 500,
|
|
144
146
|
status: "INTERNAL"
|
|
145
|
-
}
|
|
147
|
+
}).body);
|
|
146
148
|
return;
|
|
147
149
|
}
|
|
148
|
-
if (
|
|
150
|
+
if (require_policy.resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
149
151
|
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
150
152
|
logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", path, skippedBySequenceOrTurn));
|
|
151
153
|
journal.add({
|
|
@@ -156,14 +158,14 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
156
158
|
response: {
|
|
157
159
|
status: 503,
|
|
158
160
|
fixture: null,
|
|
159
|
-
...
|
|
161
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
160
162
|
}
|
|
161
163
|
});
|
|
162
|
-
require_sse_writer.writeErrorResponse(res, 503,
|
|
164
|
+
require_sse_writer.writeErrorResponse(res, 503, require_errors.renderError("gemini", "strict-miss", {
|
|
163
165
|
message: strictMessage,
|
|
164
|
-
|
|
166
|
+
bodyCode: 503,
|
|
165
167
|
status: "UNAVAILABLE"
|
|
166
|
-
}
|
|
168
|
+
}).body);
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
if (defaults.record) {
|
|
@@ -197,11 +199,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
197
199
|
fixture: null
|
|
198
200
|
}
|
|
199
201
|
});
|
|
200
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
202
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("gemini", "validation", {
|
|
201
203
|
message: exceedsFallbackBudget ? `outputDimensionality exceeds the fallback serialization budget of ${require_helpers.MAX_EMBEDDING_DIMENSIONS}` : "outputDimensionality cannot produce a numeric embedding",
|
|
202
|
-
|
|
204
|
+
bodyCode: 400,
|
|
203
205
|
status: "INVALID_ARGUMENT"
|
|
204
|
-
}
|
|
206
|
+
}).body);
|
|
205
207
|
return;
|
|
206
208
|
}
|
|
207
209
|
const embedding = require_helpers.generateDeterministicEmbedding(inputText, dimensions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveStrictMode","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","MAX_EMBEDDING_DIMENSIONS","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n MAX_EMBEDDING_DIMENSIONS,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n // Preserve nullish defaults and ignored non-null entries; guard only the\n // shapes that cannot be consumed by the text extraction below.\n const partsError = !Array.isArray(parts)\n ? \"content.parts must be an array\"\n : parts.some((part) => part === null)\n ? \"content.parts must not contain null entries\"\n : undefined;\n if (partsError) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: partsError, code: 400, status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n // Match the embedding loop's numeric coercion, including array dimensions.\n // Preserve supported coercion and zero within the local serialization budget.\n // Guard only the fallback allocation, not unused dimensions on earlier branches.\n // Replay, strict, chaos, and proxy paths above do not consume this field.\n const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;\n if (\n exceedsFallbackBudget ||\n (typeof dimensions === \"number\" && (!Number.isInteger(dimensions) || dimensions < 0)) ||\n (typeof dimensions === \"string\" && Number.isNaN(Number(dimensions)))\n ) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: exceedsFallbackBudget\n ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}`\n : \"outputDimensionality cannot produce a numeric embedding\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gemini-embeddings.cjs","names":["flattenHeaders","renderError","isJsonObject","getContext","getTestId","matchFixtureDiagnostic","applyChaosAsync","resolveResponse","isErrorResponse","isEmbeddingResponse","resolveRequestDecisions","strictNoMatchMessage","strictNoMatchLogLine","strictOverrideField","proxyAndRecord","MAX_EMBEDDING_DIMENSIONS","generateDeterministicEmbedding"],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport { renderError } from \"./response/errors.js\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n MAX_EMBEDDING_DIMENSIONS,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: `Malformed JSON body: ${detail}`,\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: \"Request body must be a JSON object\",\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n // Preserve nullish defaults and ignored non-null entries; guard only the\n // shapes that cannot be consumed by the text extraction below.\n const partsError = !Array.isArray(parts)\n ? \"content.parts must be an array\"\n : parts.some((part) => part === null)\n ? \"content.parts must not contain null entries\"\n : undefined;\n if (partsError) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: partsError,\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = renderError(\"gemini\", \"fixture\", {\n bodyCode: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n }).body;\n writeErrorResponse(res, status, geminiError, {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n renderError(\"gemini\", \"unknown-response\", {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n bodyCode: 500,\n status: \"INTERNAL\",\n }).body,\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss;\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n renderError(\"gemini\", \"strict-miss\", {\n message: strictMessage,\n bodyCode: 503,\n status: \"UNAVAILABLE\",\n }).body,\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n // Match the embedding loop's numeric coercion, including array dimensions.\n // Preserve supported coercion and zero within the local serialization budget.\n // Guard only the fallback allocation, not unused dimensions on earlier branches.\n // Replay, strict, chaos, and proxy paths above do not consume this field.\n const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;\n if (\n exceedsFallbackBudget ||\n (typeof dimensions === \"number\" && (!Number.isInteger(dimensions) || dimensions < 0)) ||\n (typeof dimensions === \"string\" && Number.isNaN(Number(dimensions)))\n ) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: exceedsFallbackBudget\n ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}`\n : \"outputDimensionality cannot produce a numeric embedding\",\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderError } from "./response/errors.js";
|
|
2
|
+
import { resolveRequestDecisions, strictOverrideField } from "./response/policy.js";
|
|
3
|
+
import { MAX_EMBEDDING_DIMENSIONS, flattenHeaders, generateDeterministicEmbedding, getContext, getTestId, isEmbeddingResponse, isErrorResponse, isJsonObject, resolveResponse, strictNoMatchLogLine, strictNoMatchMessage } from "./helpers.js";
|
|
2
4
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
3
5
|
import { applyChaosAsync } from "./chaos.js";
|
|
4
6
|
import { matchFixtureDiagnostic } from "./router.js";
|
|
@@ -24,11 +26,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
24
26
|
fixture: null
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
|
-
writeErrorResponse(res, 400,
|
|
29
|
+
writeErrorResponse(res, 400, renderError("gemini", "validation", {
|
|
28
30
|
message: `Malformed JSON body: ${detail}`,
|
|
29
|
-
|
|
31
|
+
bodyCode: 400,
|
|
30
32
|
status: "INVALID_ARGUMENT"
|
|
31
|
-
}
|
|
33
|
+
}).body);
|
|
32
34
|
return;
|
|
33
35
|
}
|
|
34
36
|
if (!isJsonObject(embedReq)) {
|
|
@@ -42,11 +44,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
42
44
|
fixture: null
|
|
43
45
|
}
|
|
44
46
|
});
|
|
45
|
-
writeErrorResponse(res, 400,
|
|
47
|
+
writeErrorResponse(res, 400, renderError("gemini", "validation", {
|
|
46
48
|
message: "Request body must be a JSON object",
|
|
47
|
-
|
|
49
|
+
bodyCode: 400,
|
|
48
50
|
status: "INVALID_ARGUMENT"
|
|
49
|
-
}
|
|
51
|
+
}).body);
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
const parts = embedReq.content?.parts ?? [];
|
|
@@ -62,11 +64,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
62
64
|
fixture: null
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
|
-
writeErrorResponse(res, 400,
|
|
67
|
+
writeErrorResponse(res, 400, renderError("gemini", "validation", {
|
|
66
68
|
message: partsError,
|
|
67
|
-
|
|
69
|
+
bodyCode: 400,
|
|
68
70
|
status: "INVALID_ARGUMENT"
|
|
69
|
-
}
|
|
71
|
+
}).body);
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
74
|
const inputText = parts.filter((p) => p.text !== void 0).map((p) => p.text).join(" ");
|
|
@@ -104,12 +106,12 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
104
106
|
fixture
|
|
105
107
|
}
|
|
106
108
|
});
|
|
107
|
-
const geminiError =
|
|
108
|
-
|
|
109
|
+
const geminiError = renderError("gemini", "fixture", {
|
|
110
|
+
bodyCode: status,
|
|
109
111
|
message: response.error.message,
|
|
110
112
|
status: response.error.type ?? "ERROR"
|
|
111
|
-
}
|
|
112
|
-
writeErrorResponse(res, status,
|
|
113
|
+
}).body;
|
|
114
|
+
writeErrorResponse(res, status, geminiError, { retryAfter: response.retryAfter });
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
if (isEmbeddingResponse(response)) {
|
|
@@ -138,14 +140,14 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
138
140
|
fixture
|
|
139
141
|
}
|
|
140
142
|
});
|
|
141
|
-
writeErrorResponse(res, 500,
|
|
143
|
+
writeErrorResponse(res, 500, renderError("gemini", "unknown-response", {
|
|
142
144
|
message: "Fixture response did not match any known embedding type (must have embedding or error)",
|
|
143
|
-
|
|
145
|
+
bodyCode: 500,
|
|
144
146
|
status: "INTERNAL"
|
|
145
|
-
}
|
|
147
|
+
}).body);
|
|
146
148
|
return;
|
|
147
149
|
}
|
|
148
|
-
if (
|
|
150
|
+
if (resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss) {
|
|
149
151
|
const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
150
152
|
logger.error(strictNoMatchLogLine(req.method ?? "POST", path, skippedBySequenceOrTurn));
|
|
151
153
|
journal.add({
|
|
@@ -159,11 +161,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
159
161
|
...strictOverrideField(defaults.strict, req.headers)
|
|
160
162
|
}
|
|
161
163
|
});
|
|
162
|
-
writeErrorResponse(res, 503,
|
|
164
|
+
writeErrorResponse(res, 503, renderError("gemini", "strict-miss", {
|
|
163
165
|
message: strictMessage,
|
|
164
|
-
|
|
166
|
+
bodyCode: 503,
|
|
165
167
|
status: "UNAVAILABLE"
|
|
166
|
-
}
|
|
168
|
+
}).body);
|
|
167
169
|
return;
|
|
168
170
|
}
|
|
169
171
|
if (defaults.record) {
|
|
@@ -197,11 +199,11 @@ async function handleGeminiEmbedContent(req, res, raw, model, fixtures, journal,
|
|
|
197
199
|
fixture: null
|
|
198
200
|
}
|
|
199
201
|
});
|
|
200
|
-
writeErrorResponse(res, 400,
|
|
202
|
+
writeErrorResponse(res, 400, renderError("gemini", "validation", {
|
|
201
203
|
message: exceedsFallbackBudget ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}` : "outputDimensionality cannot produce a numeric embedding",
|
|
202
|
-
|
|
204
|
+
bodyCode: 400,
|
|
203
205
|
status: "INVALID_ARGUMENT"
|
|
204
|
-
}
|
|
206
|
+
}).body);
|
|
205
207
|
return;
|
|
206
208
|
}
|
|
207
209
|
const embedding = generateDeterministicEmbedding(inputText, dimensions);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n MAX_EMBEDDING_DIMENSIONS,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Malformed JSON body: ${detail}`,\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: \"Request body must be a JSON object\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n // Preserve nullish defaults and ignored non-null entries; guard only the\n // shapes that cannot be consumed by the text extraction below.\n const partsError = !Array.isArray(parts)\n ? \"content.parts must be an array\"\n : parts.some((part) => part === null)\n ? \"content.parts must not contain null entries\"\n : undefined;\n if (partsError) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: { message: partsError, code: 400, status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = {\n error: {\n code: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n },\n };\n writeErrorResponse(res, status, JSON.stringify(geminiError), {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n code: 500,\n status: \"INTERNAL\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: strictMessage,\n code: 503,\n status: \"UNAVAILABLE\",\n },\n }),\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n // Match the embedding loop's numeric coercion, including array dimensions.\n // Preserve supported coercion and zero within the local serialization budget.\n // Guard only the fallback allocation, not unused dimensions on earlier branches.\n // Replay, strict, chaos, and proxy paths above do not consume this field.\n const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;\n if (\n exceedsFallbackBudget ||\n (typeof dimensions === \"number\" && (!Number.isInteger(dimensions) || dimensions < 0)) ||\n (typeof dimensions === \"string\" && Number.isNaN(Number(dimensions)))\n ) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: exceedsFallbackBudget\n ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}`\n : \"outputDimensionality cannot produce a numeric embedding\",\n code: 400,\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":";;;;;;;AAiEA,MAAM,sCAAsC;AAI5C,eAAsB,yBACpB,KACA,KACA,KACA,OACA,UACA,SACA,UACA,gBACA,cAAiC,UAClB;CACf,MAAM,EAAE,WAAW;AACnB,gBAAe,IAAI;CAEnB,IAAI;AACJ,KAAI;AACF,aAAW,KAAK,MAAM,IAAI;UACnB,UAAU;EACjB,MAAM,SAAS,oBAAoB,QAAQ,SAAS,UAAU;AAC9D,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO,kBAAkB,MAAM;GACzC,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS,wBAAwB;GACjC,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAMF,KAAI,CAAC,aAAa,SAAS,EAAE;AAC3B,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO,kBAAkB,MAAM;GACzC,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;CAIF,MAAM,QAAQ,SAAS,SAAS,SAAS,EAAE;CAG3C,MAAM,aAAa,CAAC,MAAM,QAAQ,MAAM,GACpC,mCACA,MAAM,MAAM,SAAS,SAAS,KAAK,GACjC,gDACA;AACN,KAAI,YAAY;AACd,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB,MAAM,IAAI,OAAO,kBAAkB,MAAM;GACzC,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GAAE,SAAS;GAAY,MAAM;GAAK,QAAQ;GAAoB,EACtE,CAAC,CACH;AACD;;CAEF,MAAM,YAAY,MACf,QAAQ,MAAM,EAAE,SAAS,OAAU,CACnC,KAAK,MAAM,EAAE,KAAM,CACnB,KAAK,IAAI;CAKZ,MAAM,eAAsC;EAC1C,OAAO,SAAS,SAAS;EACzB,UAAU,EAAE;EACZ,gBAAgB;EAChB,eAAe;EACf,UAAU,WAAW,IAAI;EAC1B;CAED,MAAM,SAAS,UAAU,IAAI;CAC7B,MAAM,EAAE,SAAS,4BAA4B,uBAC3C,UACA,cACA,QAAQ,6BAA6B,OAAO,EAC5C,SAAS,iBACV;CACD,MAAM,OAAO,IAAI,OAAO,kBAAkB,MAAM;AAEhD,KAAI,SAAS;AACX,UAAQ,2BAA2B,SAAS,UAAU,OAAO;AAC7D,SAAO,MAAM,oBAAoB,KAAK,UAAU,QAAQ,MAAM,CAAC,MAAM,GAAG,IAAI,GAAG;OAE/E,QAAO,MAAM,iCAAiC;AAGhD,KACE,MAAM,gBACJ,KACA,SACA,SAAS,OACT,IAAI,SACJ,IAAI,KACJ,SACA;EACE,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACP,EACD,UAAU,YAAY,SACtB,SAAS,UACT,SAAS,OACV,CAED;AAEF,KAAI,SAAS;EACX,MAAM,WAAW,MAAM,gBAAgB,SAAS,aAAa;AAG7D,MAAI,gBAAgB,SAAS,EAAE;GAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE;KAAQ;KAAS;IAC9B,CAAC;GACF,MAAM,cAAc,EAClB,OAAO;IACL,MAAM;IACN,SAAS,SAAS,MAAM;IACxB,QAAQ,SAAS,MAAM,QAAQ;IAChC,EACF;AACD,sBAAmB,KAAK,QAAQ,KAAK,UAAU,YAAY,EAAE,EAC3D,YAAY,SAAS,YACtB,CAAC;AACF;;AAIF,MAAI,oBAAoB,SAAS,EAAE;AACjC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ;KAAK;KAAS;IACnC,CAAC;GACF,MAAM,OAAmC,EACvC,WAAW,EACT,QAAQ,CAAC,GAAG,SAAS,UAAU,EAChC,EACF;AACD,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,KAAK,CAAC;AAC7B;;AAIF,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SACE;GACF,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAKF,KADwB,kBAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;EACnB,MAAM,gBAAgB,qBAAqB,wBAAwB;AACnE,SAAO,MAAM,qBAAqB,IAAI,UAAU,QAAQ,MAAM,wBAAwB,CAAC;AACvF,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IACR,QAAQ;IACR,SAAS;IACT,GAAG,oBAAoB,SAAS,QAAQ,IAAI,QAAQ;IACrD;GACF,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS;GACT,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;AAGF,KAAI,SAAS,QAAQ;EACnB,MAAM,UAAU,MAAM,eACpB,KACA,KACA,cACA,aACA,MACA,UACA,UACA,IACD;AACD,MAAI,YAAY,kBAAmB;AACnC,MAAI,YAAY,kBAAkB;AAChC,WAAQ,IAAI;IACV,QAAQ,IAAI,UAAU;IACtB;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ,IAAI,cAAc;KAAK,SAAS;KAAM,QAAQ;KAAS;IAC5E,CAAC;AACF;;;CAKJ,MAAM,aAAa,SAAS,wBAAwB;CAKpD,MAAM,wBAAwB,aAAa;AAC3C,KACE,yBACC,OAAO,eAAe,aAAa,CAAC,OAAO,UAAU,WAAW,IAAI,aAAa,MACjF,OAAO,eAAe,YAAY,OAAO,MAAM,OAAO,WAAW,CAAC,EACnE;AACA,UAAQ,IAAI;GACV,QAAQ,IAAI,UAAU;GACtB;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GACL,SAAS,wBACL,qEAAqE,6BACrE;GACJ,MAAM;GACN,QAAQ;GACT,EACF,CAAC,CACH;AACD;;CAEF,MAAM,YAAY,+BAA+B,WAAW,WAAW;AAEvE,QAAO,KACL,qCAAqC,UAAU,MAAM,GAAG,GAAG,CAAC,sCAC7D;AAED,SAAQ,IAAI;EACV,QAAQ,IAAI,UAAU;EACtB;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK,SAAS;GAAM;EACzC,CAAC;CAEF,MAAM,OAAmC,EACvC,WAAW,EACT,QAAQ,WACT,EACF;AACD,KAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,KAAI,IAAI,KAAK,UAAU,KAAK,CAAC"}
|
|
1
|
+
{"version":3,"file":"gemini-embeddings.js","names":[],"sources":["../src/gemini-embeddings.ts"],"sourcesContent":["/**\n * Google Gemini embedContent API support.\n *\n * Handles POST /v1beta/models/{model}:embedContent requests. Translates\n * incoming Gemini embedding requests into the ChatCompletionRequest format\n * used by the fixture router (with _endpointType: \"embedding\"), and converts\n * fixture responses back into the Gemini embedContent response format.\n *\n * Falls back to generating a deterministic embedding from the input text hash\n * when no fixture matches — same strategy as the OpenAI embeddings handler.\n */\n\nimport type * as http from \"node:http\";\nimport { resolveRequestDecisions, strictOverrideField } from \"./response/policy.js\";\nimport { renderError } from \"./response/errors.js\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n HandlerDefaults,\n RecordProviderKey,\n} from \"./types.js\";\nimport {\n isEmbeddingResponse,\n isErrorResponse,\n generateDeterministicEmbedding,\n MAX_EMBEDDING_DIMENSIONS,\n flattenHeaders,\n isJsonObject,\n getTestId,\n resolveResponse,\n getContext,\n strictNoMatchMessage,\n strictNoMatchLogLine,\n} from \"./helpers.js\";\nimport { matchFixtureDiagnostic } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaosAsync } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\n// ─── Gemini embedContent request types ────────────────────────────────────\n\ninterface GeminiEmbedContentPart {\n text?: string;\n}\n\ninterface GeminiEmbedContentRequest {\n content?: { parts?: GeminiEmbedContentPart[] };\n model?: string;\n taskType?: string;\n title?: string;\n outputDimensionality?: number;\n [key: string]: unknown;\n}\n\n// ─── Gemini embedContent response type ────────────────────────────────────\n\ninterface GeminiEmbedContentResponse {\n embedding: {\n values: number[];\n };\n}\n\n// ─── Default embedding dimensions for Gemini ──────────────────────────────\n\nconst DEFAULT_GEMINI_EMBEDDING_DIMENSIONS = 768;\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleGeminiEmbedContent(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n model: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n providerKey: RecordProviderKey = \"gemini\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embedReq: GeminiEmbedContentRequest;\n try {\n embedReq = JSON.parse(raw) as GeminiEmbedContentRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: `Malformed JSON body: ${detail}`,\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n\n // Reject bodies that parsed but are not a JSON object (e.g. `null`) before\n // touching fields — otherwise `embedReq.content` throws a TypeError that\n // surfaces as a 500 instead of a 400.\n if (!isJsonObject(embedReq)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: \"Request body must be a JSON object\",\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n\n // Extract text from content.parts\n const parts = embedReq.content?.parts ?? [];\n // Preserve nullish defaults and ignored non-null entries; guard only the\n // shapes that cannot be consumed by the text extraction below.\n const partsError = !Array.isArray(parts)\n ? \"content.parts must be an array\"\n : parts.some((part) => part === null)\n ? \"content.parts must not contain null entries\"\n : undefined;\n if (partsError) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? `/v1beta/models/${model}:embedContent`,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: partsError,\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n const inputText = parts\n .filter((p) => p.text !== undefined)\n .map((p) => p.text!)\n .join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // Uses _endpointType: \"embedding\" and embeddingInput to share fixtures\n // with OpenAI embeddings.\n const syntheticReq: ChatCompletionRequest = {\n model: embedReq.model ?? model,\n messages: [],\n embeddingInput: inputText,\n _endpointType: \"embedding\",\n _context: getContext(req),\n };\n\n const testId = getTestId(req);\n const { fixture, skippedBySequenceOrTurn } = matchFixtureDiagnostic(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = req.url ?? `/v1beta/models/${model}:embedContent`;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n logger.debug(`No fixture matched for request`);\n }\n\n if (\n await applyChaosAsync(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n req.url,\n journal,\n {\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (fixture) {\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n const geminiError = renderError(\"gemini\", \"fixture\", {\n bodyCode: status,\n message: response.error.message,\n status: response.error.type ?? \"ERROR\",\n }).body;\n writeErrorResponse(res, status, geminiError, {\n retryAfter: response.retryAfter,\n });\n return;\n }\n\n // Embedding response — use the fixture's embedding values\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: [...response.embedding],\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n return;\n }\n\n // Fixture matched but response type is not compatible with embeddings\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n renderError(\"gemini\", \"unknown-response\", {\n message:\n \"Fixture response did not match any known embedding type (must have embedding or error)\",\n bodyCode: 500,\n status: \"INTERNAL\",\n }).body,\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n const effectiveStrict = resolveRequestDecisions(defaults.strict, req.headers).fixtureMiss;\n if (effectiveStrict) {\n const strictMessage = strictNoMatchMessage(skippedBySequenceOrTurn);\n logger.error(strictNoMatchLogLine(req.method ?? \"POST\", path, skippedBySequenceOrTurn));\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 503,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 503,\n renderError(\"gemini\", \"strict-miss\", {\n message: strictMessage,\n bodyCode: 503,\n status: \"UNAVAILABLE\",\n }).body,\n );\n return;\n }\n\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n path,\n fixtures,\n defaults,\n raw,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: res.statusCode ?? 200, fixture: null, source: \"proxy\" },\n });\n return;\n }\n }\n\n // No fixture match — generate deterministic embedding from input text\n const dimensions = embedReq.outputDimensionality ?? DEFAULT_GEMINI_EMBEDDING_DIMENSIONS;\n // Match the embedding loop's numeric coercion, including array dimensions.\n // Preserve supported coercion and zero within the local serialization budget.\n // Guard only the fallback allocation, not unused dimensions on earlier branches.\n // Replay, strict, chaos, and proxy paths above do not consume this field.\n const exceedsFallbackBudget = dimensions > MAX_EMBEDDING_DIMENSIONS;\n if (\n exceedsFallbackBudget ||\n (typeof dimensions === \"number\" && (!Number.isInteger(dimensions) || dimensions < 0)) ||\n (typeof dimensions === \"string\" && Number.isNaN(Number(dimensions)))\n ) {\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n renderError(\"gemini\", \"validation\", {\n message: exceedsFallbackBudget\n ? `outputDimensionality exceeds the fallback serialization budget of ${MAX_EMBEDDING_DIMENSIONS}`\n : \"outputDimensionality cannot produce a numeric embedding\",\n bodyCode: 400,\n status: \"INVALID_ARGUMENT\",\n }).body,\n );\n return;\n }\n const embedding = generateDeterministicEmbedding(inputText, dimensions);\n\n logger.warn(\n `No embedding fixture matched for \"${inputText.slice(0, 80)}\" — returning deterministic fallback`,\n );\n\n journal.add({\n method: req.method ?? \"POST\",\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body: GeminiEmbedContentResponse = {\n embedding: {\n values: embedding,\n },\n };\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const require_errors = require('./response/errors.cjs');
|
|
2
|
+
const require_policy = require('./response/policy.cjs');
|
|
1
3
|
const require_helpers = require('./helpers.cjs');
|
|
2
4
|
const require_sse_writer = require('./sse-writer.cjs');
|
|
3
5
|
const require_chaos = require('./chaos.cjs');
|
|
@@ -5,8 +7,17 @@ const require_misbehavior = require('./misbehavior.cjs');
|
|
|
5
7
|
const require_router = require('./router.cjs');
|
|
6
8
|
const require_recorder = require('./recorder.cjs');
|
|
7
9
|
const require_interruption = require('./interruption.cjs');
|
|
10
|
+
const require_interactions = require('./response/usage/codecs/interactions.cjs');
|
|
8
11
|
|
|
9
12
|
//#region src/gemini-interactions.ts
|
|
13
|
+
/**
|
|
14
|
+
* Google Gemini Interactions API support.
|
|
15
|
+
*
|
|
16
|
+
* Translates incoming Interactions requests into the ChatCompletionRequest
|
|
17
|
+
* format used by the fixture router, and converts fixture responses back
|
|
18
|
+
* into the Gemini Interactions format — either a single JSON response or
|
|
19
|
+
* an SSE stream with event_type-based framing.
|
|
20
|
+
*/
|
|
10
21
|
/** Step types whose payload is a tool/agent result keyed by call_id. */
|
|
11
22
|
const STEP_RESULT_TYPES = new Set([
|
|
12
23
|
"function_result",
|
|
@@ -185,18 +196,7 @@ function nextInteractionId() {
|
|
|
185
196
|
return `aimock-int-${interactionCounter++}`;
|
|
186
197
|
}
|
|
187
198
|
function interactionsUsage(overrides) {
|
|
188
|
-
|
|
189
|
-
total_input_tokens: 0,
|
|
190
|
-
total_output_tokens: 0,
|
|
191
|
-
total_tokens: 0
|
|
192
|
-
};
|
|
193
|
-
const input = overrides.usage.input_tokens ?? overrides.usage.prompt_tokens ?? overrides.usage.promptTokenCount ?? 0;
|
|
194
|
-
const output = overrides.usage.output_tokens ?? overrides.usage.completion_tokens ?? overrides.usage.candidatesTokenCount ?? 0;
|
|
195
|
-
return {
|
|
196
|
-
total_input_tokens: input,
|
|
197
|
-
total_output_tokens: output,
|
|
198
|
-
total_tokens: overrides.usage.total_tokens ?? overrides.usage.totalTokenCount ?? input + output
|
|
199
|
-
};
|
|
199
|
+
return require_interactions.render(overrides);
|
|
200
200
|
}
|
|
201
201
|
function buildInteractionsTextResponse(content, model, interactionId, overrides) {
|
|
202
202
|
return {
|
|
@@ -277,12 +277,6 @@ function buildInteractionsContentWithToolCallsResponse(content, toolCalls, model
|
|
|
277
277
|
usage: interactionsUsage(overrides)
|
|
278
278
|
};
|
|
279
279
|
}
|
|
280
|
-
function buildInteractionsErrorResponse(message, code) {
|
|
281
|
-
return { error: {
|
|
282
|
-
code: code ?? "INVALID_ARGUMENT",
|
|
283
|
-
message
|
|
284
|
-
} };
|
|
285
|
-
}
|
|
286
280
|
let eventIdCounter = 0;
|
|
287
281
|
function resetEventIdCounter() {
|
|
288
282
|
eventIdCounter = 0;
|
|
@@ -570,7 +564,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
570
564
|
fixture: null
|
|
571
565
|
}
|
|
572
566
|
});
|
|
573
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
567
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("interactions", "validation", {
|
|
568
|
+
message: `Malformed JSON body: ${detail}`,
|
|
569
|
+
code: "INVALID_ARGUMENT"
|
|
570
|
+
}).body);
|
|
574
571
|
return;
|
|
575
572
|
}
|
|
576
573
|
if (!require_helpers.isJsonObject(interactionsReq)) {
|
|
@@ -584,7 +581,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
584
581
|
fixture: null
|
|
585
582
|
}
|
|
586
583
|
});
|
|
587
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
584
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("interactions", "validation", {
|
|
585
|
+
message: "Request body must be a JSON object",
|
|
586
|
+
code: "INVALID_ARGUMENT"
|
|
587
|
+
}).body);
|
|
588
588
|
return;
|
|
589
589
|
}
|
|
590
590
|
const validationError = validateInteractionsInput(interactionsReq.input) ?? validateInteractionsTools(interactionsReq.tools);
|
|
@@ -599,7 +599,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
599
599
|
fixture: null
|
|
600
600
|
}
|
|
601
601
|
});
|
|
602
|
-
require_sse_writer.writeErrorResponse(res, 400,
|
|
602
|
+
require_sse_writer.writeErrorResponse(res, 400, require_errors.renderError("interactions", "validation", {
|
|
603
|
+
message: validationError,
|
|
604
|
+
code: "INVALID_ARGUMENT"
|
|
605
|
+
}).body);
|
|
603
606
|
return;
|
|
604
607
|
}
|
|
605
608
|
const completionReq = geminiInteractionsToCompletionRequest(interactionsReq);
|
|
@@ -617,7 +620,8 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
617
620
|
body: completionReq
|
|
618
621
|
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
619
622
|
if (!fixture) {
|
|
620
|
-
|
|
623
|
+
const decisions = require_policy.resolveRequestDecisions(defaults.strict, req.headers);
|
|
624
|
+
if (decisions.fixtureMiss) {
|
|
621
625
|
const strictStatus = 503;
|
|
622
626
|
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
623
627
|
logger.error(require_helpers.strictNoMatchLogLine(req.method ?? "POST", urlPath, skippedBySequenceOrTurn));
|
|
@@ -629,13 +633,16 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
629
633
|
response: {
|
|
630
634
|
status: strictStatus,
|
|
631
635
|
fixture: null,
|
|
632
|
-
...
|
|
636
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
633
637
|
}
|
|
634
638
|
});
|
|
635
|
-
require_sse_writer.writeErrorResponse(res, strictStatus,
|
|
639
|
+
require_sse_writer.writeErrorResponse(res, strictStatus, require_errors.renderError("interactions", "strict-miss", {
|
|
640
|
+
message: strictMessage,
|
|
641
|
+
code: "UNAVAILABLE"
|
|
642
|
+
}).body);
|
|
636
643
|
return;
|
|
637
644
|
}
|
|
638
|
-
if (defaults.record) {
|
|
645
|
+
if (decisions.upstream && defaults.record) {
|
|
639
646
|
const outcome = await require_recorder.proxyAndRecord(req, res, completionReq, "gemini-interactions", urlPath, fixtures, defaults, raw);
|
|
640
647
|
if (outcome === "handled_by_hook") return;
|
|
641
648
|
if (outcome !== "not_configured") {
|
|
@@ -661,10 +668,13 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
661
668
|
response: {
|
|
662
669
|
status: 404,
|
|
663
670
|
fixture: null,
|
|
664
|
-
...
|
|
671
|
+
...require_policy.strictOverrideField(defaults.strict, req.headers)
|
|
665
672
|
}
|
|
666
673
|
});
|
|
667
|
-
require_sse_writer.writeErrorResponse(res, 404,
|
|
674
|
+
require_sse_writer.writeErrorResponse(res, 404, require_errors.renderError("interactions", "fixture-miss", {
|
|
675
|
+
message: "No fixture matched",
|
|
676
|
+
code: "NOT_FOUND"
|
|
677
|
+
}).body);
|
|
668
678
|
return;
|
|
669
679
|
}
|
|
670
680
|
let response = await require_helpers.resolveResponse(fixture, completionReq);
|
|
@@ -696,7 +706,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
696
706
|
testId
|
|
697
707
|
});
|
|
698
708
|
if (!misbehavior.summary?.evaluations.some((evaluation) => evaluation.outcome === "error")) logger.error(`${req.method ?? "POST"} ${urlPath}: ${misbehavior.message}`);
|
|
699
|
-
require_sse_writer.writeErrorResponse(res, misbehavior.status,
|
|
709
|
+
require_sse_writer.writeErrorResponse(res, misbehavior.status, require_errors.renderError("interactions", "misbehavior", {
|
|
710
|
+
message: misbehavior.message,
|
|
711
|
+
code: misbehavior.code
|
|
712
|
+
}).body);
|
|
700
713
|
return;
|
|
701
714
|
}
|
|
702
715
|
if (misbehavior.kind === "applied") {
|
|
@@ -724,7 +737,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
724
737
|
defaults,
|
|
725
738
|
testId
|
|
726
739
|
});
|
|
727
|
-
require_sse_writer.writeErrorResponse(res, status,
|
|
740
|
+
require_sse_writer.writeErrorResponse(res, status, require_errors.renderError("interactions", "fixture", {
|
|
741
|
+
message: response.error.message,
|
|
742
|
+
code: response.error.type ?? "ERROR"
|
|
743
|
+
}).body, { retryAfter: response.retryAfter });
|
|
728
744
|
return;
|
|
729
745
|
}
|
|
730
746
|
if (misbehavior.kind === "applied" && misbehavior.summary.fault === "reasoning-only") {
|
|
@@ -745,11 +761,7 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
745
761
|
testId
|
|
746
762
|
});
|
|
747
763
|
const interactionId = nextInteractionId();
|
|
748
|
-
const usage = {
|
|
749
|
-
...interactionsUsage({ usage: appliedUsage }),
|
|
750
|
-
total_output_tokens: 0,
|
|
751
|
-
total_thought_tokens: appliedUsage?.completion_tokens ?? 0
|
|
752
|
-
};
|
|
764
|
+
const usage = require_interactions.render({ usage: appliedUsage }, "applied-reasoning-only");
|
|
753
765
|
if (!streaming) {
|
|
754
766
|
const overrides = require_helpers.isTextResponse(response) || require_helpers.isToolCallResponse(response) || require_helpers.isCombinedFixtureResponse(response) ? require_helpers.extractOverrides(response) : void 0;
|
|
755
767
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -991,7 +1003,10 @@ async function handleGeminiInteractions(req, res, raw, fixtures, journal, defaul
|
|
|
991
1003
|
defaults,
|
|
992
1004
|
testId
|
|
993
1005
|
});
|
|
994
|
-
require_sse_writer.writeErrorResponse(res, 500,
|
|
1006
|
+
require_sse_writer.writeErrorResponse(res, 500, require_errors.renderError("interactions", "unknown-response", {
|
|
1007
|
+
message: "Fixture response did not match any known type",
|
|
1008
|
+
code: "INTERNAL"
|
|
1009
|
+
}).body);
|
|
995
1010
|
}
|
|
996
1011
|
|
|
997
1012
|
//#endregion
|