@copilotkit/aimock 1.16.4 → 1.18.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 +35 -0
- package/README.md +10 -10
- package/dist/a2a-mock.d.cts +2 -2
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts +2 -2
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +2 -2
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-handler.cjs +120 -5
- package/dist/agui-handler.cjs.map +1 -1
- package/dist/agui-handler.d.cts +41 -5
- package/dist/agui-handler.d.cts.map +1 -1
- package/dist/agui-handler.d.ts +41 -5
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-handler.js +114 -6
- package/dist/agui-handler.js.map +1 -1
- package/dist/agui-mock.cjs +18 -7
- package/dist/agui-mock.cjs.map +1 -1
- package/dist/agui-mock.d.cts +2 -2
- package/dist/agui-mock.d.cts.map +1 -1
- package/dist/agui-mock.d.ts +2 -2
- package/dist/agui-mock.d.ts.map +1 -1
- package/dist/agui-mock.js +20 -9
- package/dist/agui-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +43 -22
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts +4 -3
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts +4 -3
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +45 -24
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-stub.cjs +28 -0
- package/dist/agui-stub.d.cts +5 -0
- package/dist/agui-stub.d.ts +5 -0
- package/dist/agui-stub.js +5 -0
- package/dist/agui-types.d.cts +33 -6
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +33 -6
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +1 -1
- package/dist/aimock-cli.js +1 -1
- package/dist/aws-event-stream.d.cts +2 -2
- package/dist/aws-event-stream.d.cts.map +1 -1
- package/dist/aws-event-stream.d.ts +2 -2
- package/dist/aws-event-stream.d.ts.map +1 -1
- package/dist/bedrock-converse.cjs +4 -4
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts +3 -3
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts +3 -3
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +4 -4
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +4 -4
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts +3 -3
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts +3 -3
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +4 -4
- package/dist/bedrock.js.map +1 -1
- package/dist/chaos.cjs +35 -9
- package/dist/chaos.cjs.map +1 -1
- package/dist/chaos.d.cts +19 -4
- package/dist/chaos.d.cts.map +1 -1
- package/dist/chaos.d.ts +19 -4
- package/dist/chaos.d.ts.map +1 -1
- package/dist/chaos.js +35 -10
- package/dist/chaos.js.map +1 -1
- package/dist/cli.cjs +6 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +6 -5
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +2 -2
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts +2 -2
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts +2 -2
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +2 -2
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +3 -3
- package/dist/config-loader.d.cts +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +2 -2
- package/dist/convert-vidaimock.cjs +1 -1
- package/dist/convert-vidaimock.js +1 -1
- package/dist/convert.cjs +1 -1
- package/dist/convert.js +1 -1
- package/dist/elevenlabs-audio.cjs +212 -0
- package/dist/elevenlabs-audio.cjs.map +1 -0
- package/dist/elevenlabs-audio.d.cts +11 -0
- package/dist/elevenlabs-audio.d.cts.map +1 -0
- package/dist/elevenlabs-audio.d.ts +11 -0
- package/dist/elevenlabs-audio.d.ts.map +1 -0
- package/dist/elevenlabs-audio.js +212 -0
- package/dist/elevenlabs-audio.js.map +1 -0
- package/dist/embeddings.cjs +2 -2
- package/dist/embeddings.cjs.map +1 -1
- package/dist/embeddings.d.cts +2 -2
- package/dist/embeddings.d.cts.map +1 -1
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +2 -2
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +484 -0
- package/dist/fal-audio.cjs.map +1 -0
- package/dist/fal-audio.d.cts +10 -0
- package/dist/fal-audio.d.cts.map +1 -0
- package/dist/fal-audio.d.ts +10 -0
- package/dist/fal-audio.d.ts.map +1 -0
- package/dist/fal-audio.js +480 -0
- package/dist/fal-audio.js.map +1 -0
- package/dist/fal.cjs +424 -0
- package/dist/fal.cjs.map +1 -0
- package/dist/fal.d.cts +39 -0
- package/dist/fal.d.cts.map +1 -0
- package/dist/fal.d.ts +39 -0
- package/dist/fal.d.ts.map +1 -0
- package/dist/fal.js +420 -0
- package/dist/fal.js.map +1 -0
- package/dist/fixture-loader.cjs +16 -3
- 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 +17 -4
- package/dist/fixture-loader.js.map +1 -1
- package/dist/fixtures-remote.cjs +1 -1
- package/dist/fixtures-remote.js +1 -1
- package/dist/gemini-interactions.cjs +619 -0
- package/dist/gemini-interactions.cjs.map +1 -0
- package/dist/gemini-interactions.d.cts +46 -0
- package/dist/gemini-interactions.d.cts.map +1 -0
- package/dist/gemini-interactions.d.ts +46 -0
- package/dist/gemini-interactions.d.ts.map +1 -0
- package/dist/gemini-interactions.js +618 -0
- package/dist/gemini-interactions.js.map +1 -0
- package/dist/gemini.cjs +78 -2
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts +2 -2
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts +2 -2
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +79 -3
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +28 -1
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +13 -3
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +13 -3
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +26 -2
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +2 -2
- package/dist/images.cjs.map +1 -1
- package/dist/images.d.cts +2 -2
- package/dist/images.d.cts.map +1 -1
- package/dist/images.d.ts +2 -2
- package/dist/images.d.ts.map +1 -1
- package/dist/images.js +2 -2
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +24 -4
- package/dist/index.d.cts +12 -8
- package/dist/index.d.ts +12 -8
- package/dist/index.js +11 -7
- package/dist/jest.cjs +1 -1
- package/dist/jest.js +1 -1
- package/dist/jsonrpc.d.cts +3 -3
- package/dist/jsonrpc.d.cts.map +1 -1
- package/dist/jsonrpc.d.ts +3 -3
- package/dist/jsonrpc.d.ts.map +1 -1
- package/dist/llmock.cjs +53 -2
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +6 -0
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +6 -0
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +53 -2
- package/dist/llmock.js.map +1 -1
- package/dist/logger.cjs +5 -4
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.d.cts +1 -1
- package/dist/logger.d.cts.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mcp-mock.d.cts +2 -2
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +2 -2
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +2 -2
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +2 -2
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts +2 -2
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts +2 -2
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +2 -2
- package/dist/messages.js.map +1 -1
- package/dist/moderation.d.cts +3 -3
- package/dist/moderation.d.cts.map +1 -1
- package/dist/moderation.d.ts +3 -3
- package/dist/moderation.d.ts.map +1 -1
- package/dist/ndjson-writer.d.cts +2 -2
- package/dist/ndjson-writer.d.cts.map +1 -1
- package/dist/ndjson-writer.d.ts +2 -2
- package/dist/ndjson-writer.d.ts.map +1 -1
- package/dist/ollama.cjs +4 -4
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts +3 -3
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts +3 -3
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +4 -4
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +106 -38
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +52 -7
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +52 -7
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +108 -40
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.d.cts +3 -3
- package/dist/rerank.d.cts.map +1 -1
- package/dist/rerank.d.ts +3 -3
- package/dist/rerank.d.ts.map +1 -1
- package/dist/responses.cjs +2 -2
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +2 -2
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +2 -2
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +2 -2
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +1 -1
- 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 +2 -2
- package/dist/router.js.map +1 -1
- package/dist/search.d.cts +3 -3
- package/dist/search.d.cts.map +1 -1
- package/dist/search.d.ts +3 -3
- package/dist/search.d.ts.map +1 -1
- package/dist/server.cjs +253 -8
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -2
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +257 -12
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +20 -11
- package/dist/speech.cjs.map +1 -1
- package/dist/speech.d.cts +2 -2
- package/dist/speech.d.cts.map +1 -1
- package/dist/speech.d.ts +2 -2
- package/dist/speech.d.ts.map +1 -1
- package/dist/speech.js +20 -11
- package/dist/speech.js.map +1 -1
- package/dist/sse-writer.d.cts +3 -3
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +3 -3
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/stream-collapse.cjs +80 -9
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +11 -1
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +11 -1
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +80 -10
- package/dist/stream-collapse.js.map +1 -1
- package/dist/suite.cjs +1 -1
- package/dist/suite.d.cts +2 -2
- package/dist/suite.d.ts +2 -2
- package/dist/suite.js +1 -1
- package/dist/transcription.cjs +2 -2
- package/dist/transcription.cjs.map +1 -1
- package/dist/transcription.d.cts +2 -2
- package/dist/transcription.d.cts.map +1 -1
- package/dist/transcription.d.ts +2 -2
- package/dist/transcription.d.ts.map +1 -1
- package/dist/transcription.js +2 -2
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +38 -11
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +38 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.d.cts +2 -2
- package/dist/vector-mock.d.cts.map +1 -1
- package/dist/vector-mock.d.ts +2 -2
- package/dist/vector-mock.d.ts.map +1 -1
- package/dist/vector-mock.js +2 -2
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-types.d.cts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/video.cjs +9 -3
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +3 -3
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +3 -3
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +9 -3
- package/dist/video.js.map +1 -1
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.js +1 -1
- package/dist/ws-framing.d.cts +2 -2
- package/dist/ws-framing.d.cts.map +1 -1
- package/dist/ws-framing.d.ts +2 -2
- package/dist/ws-framing.d.ts.map +1 -1
- package/dist/ws-gemini-live.cjs +145 -2
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +146 -3
- package/dist/ws-gemini-live.js.map +1 -1
- package/package.json +16 -2
- package/skills/write-fixtures/SKILL.md +10 -10
package/dist/video.js
CHANGED
|
@@ -109,10 +109,10 @@ async function handleVideoCreate(req, res, raw, fixtures, journal, defaults, set
|
|
|
109
109
|
path,
|
|
110
110
|
headers: flattenHeaders(req.headers),
|
|
111
111
|
body: syntheticReq
|
|
112
|
-
}, defaults.registry, defaults.logger)) return;
|
|
112
|
+
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
113
113
|
if (!fixture) {
|
|
114
114
|
if (defaults.record) {
|
|
115
|
-
if (await proxyAndRecord(req, res, syntheticReq, "openai", req.url ?? "/v1/videos", fixtures, defaults, raw)) {
|
|
115
|
+
if (await proxyAndRecord(req, res, syntheticReq, "openai", req.url ?? "/v1/videos", fixtures, defaults, raw) !== "not_configured") {
|
|
116
116
|
journal.add({
|
|
117
117
|
method,
|
|
118
118
|
path,
|
|
@@ -210,10 +210,16 @@ async function handleVideoCreate(req, res, raw, fixtures, journal, defaults, set
|
|
|
210
210
|
}));
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
-
function handleVideoStatus(req, res, videoId, journal, setCorsHeaders, videoStates) {
|
|
213
|
+
function handleVideoStatus(req, res, videoId, journal, defaults, setCorsHeaders, videoStates) {
|
|
214
214
|
setCorsHeaders(res);
|
|
215
215
|
const path = req.url ?? `/v1/videos/${videoId}`;
|
|
216
216
|
const method = req.method ?? "GET";
|
|
217
|
+
if (applyChaos(res, null, defaults.chaos, req.headers, journal, {
|
|
218
|
+
method,
|
|
219
|
+
path,
|
|
220
|
+
headers: flattenHeaders(req.headers),
|
|
221
|
+
body: null
|
|
222
|
+
}, "internal", defaults.registry, defaults.logger)) return;
|
|
217
223
|
const stateKey = `${getTestId(req)}:${videoId}`;
|
|
218
224
|
const video = videoStates.get(stateKey);
|
|
219
225
|
if (!video) {
|
package/dist/video.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"video.js","names":[],"sources":["../src/video.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults, VideoResponse } from \"./types.js\";\nimport { isVideoResponse, isErrorResponse, flattenHeaders, getTestId } from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface VideoRequest {\n model?: string;\n prompt: string;\n [key: string]: unknown;\n}\n\n// ─── VideoStateMap with TTL and size bound ────────────────────────────────\n\nconst VIDEO_STATE_MAX_ENTRIES = 10_000;\nconst VIDEO_STATE_TTL_MS = 3_600_000; // 1 hour\n\ninterface VideoStateEntry {\n video: VideoResponse[\"video\"];\n createdAt: number;\n}\n\n/**\n * A Map wrapper for video state that enforces a maximum size and per-entry TTL.\n * Entries older than VIDEO_STATE_TTL_MS are lazily evicted on `get`.\n * When the map exceeds VIDEO_STATE_MAX_ENTRIES on `set`, the oldest entries\n * are removed to stay within bounds.\n */\nexport class VideoStateMap {\n private readonly entries = new Map<string, VideoStateEntry>();\n\n get(key: string): VideoResponse[\"video\"] | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > VIDEO_STATE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.video;\n }\n\n set(key: string, video: VideoResponse[\"video\"]): void {\n this.entries.set(key, { video, createdAt: Date.now() });\n // Evict oldest entries if over capacity\n if (this.entries.size > VIDEO_STATE_MAX_ENTRIES) {\n const excess = this.entries.size - VIDEO_STATE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport async function handleVideoCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n videoStates: VideoStateMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/videos\";\n const method = req.method ?? \"POST\";\n\n let videoReq: VideoRequest;\n try {\n videoReq = JSON.parse(raw) as VideoRequest;\n } catch {\n journal.add({\n method,\n path,\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: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!videoReq.prompt) {\n journal.add({\n method,\n path,\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: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: videoReq.model ?? \"sora-2\",\n messages: [{ role: \"user\", content: videoReq.prompt }],\n _endpointType: \"video\",\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const proxied = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/videos\",\n fixtures,\n defaults,\n raw,\n );\n if (proxied) {\n journal.add({\n method,\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 const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isVideoResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not a video type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const video = response.video;\n const created_at = Math.floor(Date.now() / 1000);\n\n // Store for GET status checks\n const stateKey = `${testId}:${video.id}`;\n videoStates.set(stateKey, video);\n\n if (video.status === \"completed\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ id: video.id, status: video.status, url: video.url, created_at }));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ id: video.id, status: video.status, created_at }));\n }\n}\n\nexport function handleVideoStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n videoId: string,\n journal: Journal,\n setCorsHeaders: (res: http.ServerResponse) => void,\n videoStates: VideoStateMap,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? `/v1/videos/${videoId}`;\n const method = req.method ?? \"GET\";\n\n const testId = getTestId(req);\n const stateKey = `${testId}:${videoId}`;\n const video = videoStates.get(stateKey);\n\n if (!video) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({ error: { message: `Video ${videoId} not found`, type: \"not_found\" } }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n\n const created_at = Math.floor(Date.now() / 1000);\n const body: Record<string, unknown> = {\n id: video.id,\n status: video.status,\n created_at,\n };\n if (video.url) body.url = video.url;\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":"video.js","names":[],"sources":["../src/video.ts"],"sourcesContent":["import type * as http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults, VideoResponse } from \"./types.js\";\nimport { isVideoResponse, isErrorResponse, flattenHeaders, getTestId } from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\n\ninterface VideoRequest {\n model?: string;\n prompt: string;\n [key: string]: unknown;\n}\n\n// ─── VideoStateMap with TTL and size bound ────────────────────────────────\n\nconst VIDEO_STATE_MAX_ENTRIES = 10_000;\nconst VIDEO_STATE_TTL_MS = 3_600_000; // 1 hour\n\ninterface VideoStateEntry {\n video: VideoResponse[\"video\"];\n createdAt: number;\n}\n\n/**\n * A Map wrapper for video state that enforces a maximum size and per-entry TTL.\n * Entries older than VIDEO_STATE_TTL_MS are lazily evicted on `get`.\n * When the map exceeds VIDEO_STATE_MAX_ENTRIES on `set`, the oldest entries\n * are removed to stay within bounds.\n */\nexport class VideoStateMap {\n private readonly entries = new Map<string, VideoStateEntry>();\n\n get(key: string): VideoResponse[\"video\"] | undefined {\n const entry = this.entries.get(key);\n if (!entry) return undefined;\n if (Date.now() - entry.createdAt > VIDEO_STATE_TTL_MS) {\n this.entries.delete(key);\n return undefined;\n }\n return entry.video;\n }\n\n set(key: string, video: VideoResponse[\"video\"]): void {\n this.entries.set(key, { video, createdAt: Date.now() });\n // Evict oldest entries if over capacity\n if (this.entries.size > VIDEO_STATE_MAX_ENTRIES) {\n const excess = this.entries.size - VIDEO_STATE_MAX_ENTRIES;\n const iter = this.entries.keys();\n for (let i = 0; i < excess; i++) {\n const next = iter.next();\n if (!next.done) this.entries.delete(next.value);\n }\n }\n }\n\n delete(key: string): boolean {\n return this.entries.delete(key);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\nexport async function handleVideoCreate(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n raw: string,\n fixtures: Fixture[],\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n videoStates: VideoStateMap,\n): Promise<void> {\n setCorsHeaders(res);\n const path = req.url ?? \"/v1/videos\";\n const method = req.method ?? \"POST\";\n\n let videoReq: VideoRequest;\n try {\n videoReq = JSON.parse(raw) as VideoRequest;\n } catch {\n journal.add({\n method,\n path,\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: \"Malformed JSON\", type: \"invalid_request_error\", code: \"invalid_json\" },\n }),\n );\n return;\n }\n\n if (!videoReq.prompt) {\n journal.add({\n method,\n path,\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: \"Missing required parameter: 'prompt'\", type: \"invalid_request_error\" },\n }),\n );\n return;\n }\n\n const syntheticReq: ChatCompletionRequest = {\n model: videoReq.model ?? \"sora-2\",\n messages: [{ role: \"user\", content: videoReq.prompt }],\n _endpointType: \"video\",\n };\n\n const testId = getTestId(req);\n const fixture = matchFixture(\n fixtures,\n syntheticReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n defaults.logger.debug(`Fixture matched: ${JSON.stringify(fixture.match).slice(0, 120)}`);\n } else {\n defaults.logger.debug(`No fixture matched for request`);\n }\n\n if (\n applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: syntheticReq },\n fixture ? \"fixture\" : \"proxy\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"openai\",\n req.url ?? \"/v1/videos\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method,\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 const strictStatus = defaults.strict ? 503 : 404;\n const strictMessage = defaults.strict\n ? \"Strict mode: no fixture matched\"\n : \"No fixture matched\";\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: strictStatus, fixture: null },\n });\n writeErrorResponse(\n res,\n strictStatus,\n JSON.stringify({\n error: { message: strictMessage, type: \"invalid_request_error\", code: \"no_fixture_match\" },\n }),\n );\n return;\n }\n\n const response = fixture.response;\n\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n if (!isVideoResponse(response)) {\n journal.add({\n method,\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: { message: \"Fixture response is not a video type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n\n const video = response.video;\n const created_at = Math.floor(Date.now() / 1000);\n\n // Store for GET status checks\n const stateKey = `${testId}:${video.id}`;\n videoStates.set(stateKey, video);\n\n if (video.status === \"completed\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ id: video.id, status: video.status, url: video.url, created_at }));\n } else {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ id: video.id, status: video.status, created_at }));\n }\n}\n\nexport function handleVideoStatus(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n videoId: string,\n journal: Journal,\n defaults: HandlerDefaults,\n setCorsHeaders: (res: http.ServerResponse) => void,\n videoStates: VideoStateMap,\n): void {\n setCorsHeaders(res);\n const path = req.url ?? `/v1/videos/${videoId}`;\n const method = req.method ?? \"GET\";\n\n if (\n applyChaos(\n res,\n null,\n defaults.chaos,\n req.headers,\n journal,\n { method, path, headers: flattenHeaders(req.headers), body: null },\n \"internal\",\n defaults.registry,\n defaults.logger,\n )\n )\n return;\n\n const testId = getTestId(req);\n const stateKey = `${testId}:${videoId}`;\n const video = videoStates.get(stateKey);\n\n if (!video) {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 404, fixture: null },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({ error: { message: `Video ${videoId} not found`, type: \"not_found\" } }),\n );\n return;\n }\n\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: null,\n response: { status: 200, fixture: null },\n });\n\n const created_at = Math.floor(Date.now() / 1000);\n const body: Record<string, unknown> = {\n id: video.id,\n status: video.status,\n created_at,\n };\n if (video.url) body.url = video.url;\n\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
package/dist/vitest.cjs
CHANGED
|
@@ -2,8 +2,8 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
3
3
|
const require_fixture_loader = require('./fixture-loader.cjs');
|
|
4
4
|
const require_llmock = require('./llmock.cjs');
|
|
5
|
-
let node_path = require("node:path");
|
|
6
5
|
let node_fs = require("node:fs");
|
|
6
|
+
let node_path = require("node:path");
|
|
7
7
|
let vitest = require("vitest");
|
|
8
8
|
|
|
9
9
|
//#region src/vitest.ts
|
package/dist/vitest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadFixtureFile, loadFixturesFromDir } from "./fixture-loader.js";
|
|
2
2
|
import { LLMock } from "./llmock.js";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
3
|
import { statSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
5
|
import { afterAll, beforeAll, beforeEach } from "vitest";
|
|
6
6
|
|
|
7
7
|
//#region src/vitest.ts
|
package/dist/ws-framing.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as http from "node:http";
|
|
1
|
+
import * as http$1 from "node:http";
|
|
2
2
|
import * as net from "node:net";
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ declare class WebSocketConnection extends EventEmitter {
|
|
|
19
19
|
private handleFrame;
|
|
20
20
|
}
|
|
21
21
|
declare function computeAcceptKey(wsKey: string): string;
|
|
22
|
-
declare function upgradeToWebSocket(req: http.IncomingMessage, socket: net.Socket): WebSocketConnection;
|
|
22
|
+
declare function upgradeToWebSocket(req: http$1.IncomingMessage, socket: net.Socket): WebSocketConnection;
|
|
23
23
|
//# sourceMappingURL=ws-framing.d.ts.map
|
|
24
24
|
//#endregion
|
|
25
25
|
export { WebSocketConnection, computeAcceptKey, upgradeToWebSocket };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-framing.d.cts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;
|
|
1
|
+
{"version":3,"file":"ws-framing.d.cts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;UAC3B,MAAK;UACE,SAAA;aACX,CAAA,MAAA,EA1MmB,GAAA,CAAI,MA0MvB;EAAmB,IAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;iBATN,gBAAA;iBAMA,kBAAA,MACT,MAAA,CAAK,yBACF,GAAA,CAAI,SACX"}
|
package/dist/ws-framing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as http from "node:http";
|
|
1
|
+
import * as http$1 from "node:http";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import * as net from "node:net";
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ declare class WebSocketConnection extends EventEmitter {
|
|
|
19
19
|
private handleFrame;
|
|
20
20
|
}
|
|
21
21
|
declare function computeAcceptKey(wsKey: string): string;
|
|
22
|
-
declare function upgradeToWebSocket(req: http.IncomingMessage, socket: net.Socket): WebSocketConnection;
|
|
22
|
+
declare function upgradeToWebSocket(req: http$1.IncomingMessage, socket: net.Socket): WebSocketConnection;
|
|
23
23
|
//# sourceMappingURL=ws-framing.d.ts.map
|
|
24
24
|
//#endregion
|
|
25
25
|
export { WebSocketConnection, computeAcceptKey, upgradeToWebSocket };
|
package/dist/ws-framing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-framing.d.ts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;
|
|
1
|
+
{"version":3,"file":"ws-framing.d.ts","names":[],"sources":["../src/ws-framing.ts"],"sourcesContent":[],"mappings":";;;;;;AA+NgB,cAzMH,mBAAA,SAA4B,YAAA,CAyMT;EAMhB,QAAA,MAAA;EAAkB,QAAA,MAAA;UAC3B,MAAK;UACE,SAAA;aACX,CAAA,MAAA,EA1MmB,GAAA,CAAI,MA0MvB;EAAmB,IAAA,CAAA,IAAA,EAAA,MAAA,CAAA,EAAA,IAAA;;;;;;;;iBATN,gBAAA;iBAMA,kBAAA,MACT,MAAA,CAAK,yBACF,GAAA,CAAI,SACX"}
|
package/dist/ws-gemini-live.cjs
CHANGED
|
@@ -15,7 +15,7 @@ function geminiTurnsToMessages(turns) {
|
|
|
15
15
|
const role = turn.role ?? "user";
|
|
16
16
|
if (role === "user") {
|
|
17
17
|
const funcResponses = turn.parts.filter((p) => p.functionResponse);
|
|
18
|
-
const textParts = turn.parts.filter((p) => p.text !== void 0);
|
|
18
|
+
const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
|
|
19
19
|
if (funcResponses.length > 0) {
|
|
20
20
|
for (let i = 0; i < funcResponses.length; i++) {
|
|
21
21
|
const fr = funcResponses[i].functionResponse;
|
|
@@ -38,7 +38,7 @@ function geminiTurnsToMessages(turns) {
|
|
|
38
38
|
}
|
|
39
39
|
} else if (role === "model") {
|
|
40
40
|
const funcCalls = turn.parts.filter((p) => p.functionCall);
|
|
41
|
-
const textParts = turn.parts.filter((p) => p.text !== void 0);
|
|
41
|
+
const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
|
|
42
42
|
if (funcCalls.length > 0) messages.push({
|
|
43
43
|
role: "assistant",
|
|
44
44
|
content: null,
|
|
@@ -178,6 +178,16 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
178
178
|
if (!fixture) {
|
|
179
179
|
if (defaults.strict) {
|
|
180
180
|
defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
|
|
181
|
+
journal.add({
|
|
182
|
+
method: "WS",
|
|
183
|
+
path,
|
|
184
|
+
headers: {},
|
|
185
|
+
body: completionReq,
|
|
186
|
+
response: {
|
|
187
|
+
status: 404,
|
|
188
|
+
fixture: null
|
|
189
|
+
}
|
|
190
|
+
});
|
|
181
191
|
ws.close(1008, "Strict mode: no fixture matched");
|
|
182
192
|
return;
|
|
183
193
|
}
|
|
@@ -221,6 +231,139 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
221
231
|
} }));
|
|
222
232
|
return;
|
|
223
233
|
}
|
|
234
|
+
if (require_helpers.isAudioResponse(response)) {
|
|
235
|
+
journal.add({
|
|
236
|
+
method: "WS",
|
|
237
|
+
path,
|
|
238
|
+
headers: {},
|
|
239
|
+
body: completionReq,
|
|
240
|
+
response: {
|
|
241
|
+
status: 200,
|
|
242
|
+
fixture
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
const audioResp = response;
|
|
246
|
+
let mimeType;
|
|
247
|
+
let data;
|
|
248
|
+
if (typeof audioResp.audio === "string") {
|
|
249
|
+
mimeType = require_helpers.formatToMime(audioResp.format ?? "mp3");
|
|
250
|
+
data = audioResp.audio;
|
|
251
|
+
} else {
|
|
252
|
+
mimeType = audioResp.audio.contentType ?? "audio/mpeg";
|
|
253
|
+
data = audioResp.audio.b64Json;
|
|
254
|
+
}
|
|
255
|
+
ws.send(JSON.stringify({ serverContent: {
|
|
256
|
+
modelTurn: { parts: [{ inlineData: {
|
|
257
|
+
mimeType,
|
|
258
|
+
data
|
|
259
|
+
} }] },
|
|
260
|
+
turnComplete: true
|
|
261
|
+
} }));
|
|
262
|
+
session.conversationHistory.push({
|
|
263
|
+
role: "assistant",
|
|
264
|
+
content: "[audio]"
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (require_helpers.isContentWithToolCallsResponse(response)) {
|
|
269
|
+
const journalEntry = journal.add({
|
|
270
|
+
method: "WS",
|
|
271
|
+
path,
|
|
272
|
+
headers: {},
|
|
273
|
+
body: completionReq,
|
|
274
|
+
response: {
|
|
275
|
+
status: 200,
|
|
276
|
+
fixture
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const content = response.content;
|
|
280
|
+
const chunkList = [];
|
|
281
|
+
for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
|
|
282
|
+
const interruption = require_interruption.createInterruptionSignal(fixture);
|
|
283
|
+
let interrupted = false;
|
|
284
|
+
if (content.length === 0) {
|
|
285
|
+
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: {
|
|
286
|
+
modelTurn: { parts: [{ text: "" }] },
|
|
287
|
+
turnComplete: false
|
|
288
|
+
} }));
|
|
289
|
+
} else for (let i = 0; i < chunkList.length; i++) {
|
|
290
|
+
if (ws.isClosed) break;
|
|
291
|
+
if (latency > 0) await require_sse_writer.delay(latency, interruption?.signal);
|
|
292
|
+
if (interruption?.signal.aborted) {
|
|
293
|
+
interrupted = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (ws.isClosed) break;
|
|
297
|
+
ws.send(JSON.stringify({ serverContent: {
|
|
298
|
+
modelTurn: { parts: [{ text: chunkList[i] }] },
|
|
299
|
+
turnComplete: false
|
|
300
|
+
} }));
|
|
301
|
+
interruption?.tick();
|
|
302
|
+
if (interruption?.signal.aborted) {
|
|
303
|
+
interrupted = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (interrupted) {
|
|
308
|
+
ws.destroy();
|
|
309
|
+
journalEntry.response.interrupted = true;
|
|
310
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
311
|
+
interruption?.cleanup();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const resolvedToolCalls = response.toolCalls.map((tc) => ({
|
|
315
|
+
...tc,
|
|
316
|
+
resolvedId: tc.id ?? require_helpers.generateToolCallId()
|
|
317
|
+
}));
|
|
318
|
+
if (!ws.isClosed) {
|
|
319
|
+
if (latency > 0) await require_sse_writer.delay(latency, interruption?.signal);
|
|
320
|
+
if (interruption?.signal.aborted) {
|
|
321
|
+
ws.destroy();
|
|
322
|
+
journalEntry.response.interrupted = true;
|
|
323
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
324
|
+
interruption?.cleanup();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const functionCalls = resolvedToolCalls.map((tc) => {
|
|
328
|
+
let argsObj;
|
|
329
|
+
try {
|
|
330
|
+
argsObj = JSON.parse(tc.arguments || "{}");
|
|
331
|
+
} catch {
|
|
332
|
+
defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
|
|
333
|
+
argsObj = {};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
name: tc.name,
|
|
337
|
+
args: argsObj,
|
|
338
|
+
id: tc.resolvedId
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
ws.send(JSON.stringify({ toolCall: { functionCalls } }));
|
|
342
|
+
interruption?.tick();
|
|
343
|
+
}
|
|
344
|
+
if (interruption?.signal.aborted) {
|
|
345
|
+
ws.destroy();
|
|
346
|
+
journalEntry.response.interrupted = true;
|
|
347
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
348
|
+
interruption?.cleanup();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
interruption?.cleanup();
|
|
352
|
+
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
353
|
+
session.conversationHistory.push({
|
|
354
|
+
role: "assistant",
|
|
355
|
+
content: content || null,
|
|
356
|
+
tool_calls: resolvedToolCalls.map((tc) => ({
|
|
357
|
+
id: tc.resolvedId,
|
|
358
|
+
type: "function",
|
|
359
|
+
function: {
|
|
360
|
+
name: tc.name,
|
|
361
|
+
arguments: tc.arguments
|
|
362
|
+
}
|
|
363
|
+
}))
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
224
367
|
if (require_helpers.isTextResponse(response)) {
|
|
225
368
|
const journalEntry = journal.add({
|
|
226
369
|
method: "WS",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-gemini-live.cjs","names":["DEFAULT_TEST_ID","matchFixture","isErrorResponse","isTextResponse","createInterruptionSignal","delay","isToolCallResponse"],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\n\nimport type { Fixture, ChatMessage, ChatCompletionRequest, ToolDefinition } from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport { isTextResponse, isToolCallResponse, isErrorResponse } from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\n// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[]): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n const textParts = turn.parts.filter((p) => p.text !== undefined);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined);\n\n if (funcCalls.length > 0) {\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: funcCalls.map((p, i) => ({\n id: `call_gemini_${p.functionCall!.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr, i) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters,\n },\n }));\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n },\n): void {\n const { logger } = defaults;\n const session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults, session).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n try {\n ws.send(\n JSON.stringify({\n error: { code: 500, message: msg, status: \"INTERNAL\" },\n }),\n );\n } catch {\n // Connection already gone — original error already logged above\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n },\n session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Malformed JSON\", status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message\n if (parsed.setup) {\n session.setupDone = true;\n session.model = parsed.setup.model ?? defaults.model;\n session.tools = convertTools(parsed.setup.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = WS_PATH;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (defaults.strict) {\n defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.send(\n JSON.stringify({\n error: { code: 404, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\n\n const response = fixture.response;\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify({\n error: { code: status, message: response.error.message, status: \"ERROR\" },\n }),\n );\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n\n if (content.length === 0) {\n if (ws.isClosed) return;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n turnComplete: true,\n },\n }),\n );\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let i = 0; i < chunks.length; i++) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n const isLast = i === chunks.length - 1;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n turnComplete: isLast,\n },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const interruption = createInterruptionSignal(fixture);\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = response.toolCalls.map((tc, i) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Add assistant tool_calls to conversation history\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: response.toolCalls.map((tc, i) => ({\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: 500,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":";;;;;;;AA8EA,MAAM,UAAU;;;;AAKhB,SAAS,sBAAsB,OAAwC;CACrE,MAAM,WAA0B,EAAE;AAElC,MAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,KAAK,QAAQ;AAE1B,MAAI,SAAS,QAAQ;GACnB,MAAM,gBAAgB,KAAK,MAAM,QAAQ,MAAM,EAAE,iBAAiB;GAClE,MAAM,YAAY,KAAK,MAAM,QAAQ,MAAM,EAAE,SAAS,OAAU;AAEhE,OAAI,cAAc,SAAS,GAAG;AAC5B,SAAK,IAAI,IAAI,GAAG,IAAI,cAAc,QAAQ,KAAK;KAE7C,MAAM,KADO,cAAc,GACX;AAChB,cAAS,KAAK;MACZ,MAAM;MACN,SAAS,OAAO,GAAG,aAAa,WAAW,GAAG,WAAW,KAAK,UAAU,GAAG,SAAS;MACpF,cAAc,GAAG,MAAM,eAAe,GAAG,KAAK,GAAG;MAClD,CAAC;;AAEJ,QAAI,UAAU,SAAS,EACrB,UAAS,KAAK;KACZ,MAAM;KACN,SAAS,UAAU,KAAK,MAAM,EAAE,KAAM,CAAC,KAAK,GAAG;KAChD,CAAC;UAEC;IACL,MAAM,OAAO,UAAU,KAAK,MAAM,EAAE,KAAM,CAAC,KAAK,GAAG;AACnD,aAAS,KAAK;KAAE,MAAM;KAAQ,SAAS;KAAM,CAAC;;aAEvC,SAAS,SAAS;GAC3B,MAAM,YAAY,KAAK,MAAM,QAAQ,MAAM,EAAE,aAAa;GAC1D,MAAM,YAAY,KAAK,MAAM,QAAQ,MAAM,EAAE,SAAS,OAAU;AAEhE,OAAI,UAAU,SAAS,EACrB,UAAS,KAAK;IACZ,MAAM;IACN,SAAS;IACT,YAAY,UAAU,KAAK,GAAG,OAAO;KACnC,IAAI,eAAe,EAAE,aAAc,KAAK,GAAG;KAC3C,MAAM;KACN,UAAU;MACR,MAAM,EAAE,aAAc;MACtB,WAAW,KAAK,UAAU,EAAE,aAAc,KAAK;MAChD;KACF,EAAE;IACJ,CAAC;QACG;IACL,MAAM,OAAO,UAAU,KAAK,MAAM,EAAE,KAAM,CAAC,KAAK,GAAG;AACnD,aAAS,KAAK;KAAE,MAAM;KAAa,SAAS;KAAM,CAAC;;;;AAKzD,QAAO;;;;;AAMT,SAAS,uBAAuB,cAAqD;AACnF,QAAO,aAAa,kBAAkB,KAAK,IAAI,OAAO;EACpD,MAAM;EACN,SAAS,OAAO,GAAG,aAAa,WAAW,GAAG,WAAW,KAAK,UAAU,GAAG,SAAS;EACpF,cAAc,GAAG,MAAM,eAAe,GAAG,KAAK,GAAG;EAClD,EAAE;;;;;AAML,SAAS,aAAa,aAAqD;AACzE,KAAI,CAAC,eAAe,YAAY,WAAW,EAAG,QAAO,EAAE;AAEvD,QADc,YAAY,SAAS,MAAM,EAAE,wBAAwB,EAAE,CAAC,CACzD,KAAK,OAAO;EACvB,MAAM;EACN,UAAU;GACR,MAAM,EAAE;GACR,aAAa,EAAE;GACf,YAAY,EAAE;GACf;EACF,EAAE;;AAKL,SAAgB,0BACd,IACA,UACA,SACA,UASM;CACN,MAAM,EAAE,WAAW;CACnB,MAAM,UAAwB;EAC5B,WAAW;EACX,OAAO,SAAS;EAChB,OAAO,EAAE;EACT,qBAAqB,EAAE;EACxB;CAED,IAAI,UAAU,QAAQ,SAAS;AAC/B,IAAG,GAAG,YAAY,QAAgB;AAChC,YAAU,QAAQ,WAChB,eAAe,KAAK,IAAI,UAAU,SAAS,UAAU,QAAQ,CAAC,OAAO,QAAiB;GACpF,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU;AACjD,UAAO,MAAM,gCAAgC,MAAM;AACnD,OAAI;AACF,OAAG,KACD,KAAK,UAAU,EACb,OAAO;KAAE,MAAM;KAAK,SAAS;KAAK,QAAQ;KAAY,EACvD,CAAC,CACH;WACK;IAGR,CACH;GACD;;AAGJ,eAAe,eACb,KACA,IACA,UACA,SACA,UASA,SACe;CACf,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;SAClB;AACN,KAAG,KACD,KAAK,UAAU,EACb,OAAO;GAAE,MAAM;GAAK,SAAS;GAAkB,QAAQ;GAAoB,EAC5E,CAAC,CACH;AACD;;AAIF,KAAI,OAAO,OAAO;AAChB,UAAQ,YAAY;AACpB,UAAQ,QAAQ,OAAO,MAAM,SAAS,SAAS;AAC/C,UAAQ,QAAQ,aAAa,OAAO,MAAM,MAAM;AAChD,KAAG,KAAK,KAAK,UAAU,EAAE,eAAe,EAAE,EAAE,CAAC,CAAC;AAC9C;;AAIF,KAAI,CAAC,QAAQ,WAAW;AACtB,KAAG,KACD,KAAK,UAAU,EACb,OAAO;GAAE,MAAM;GAAK,SAAS;GAAkB,QAAQ;GAAuB,EAC/E,CAAC,CACH;AACD;;CAIF,IAAI;AAEJ,KAAI,OAAO,eAAe;AACxB,MAAI,CAAC,OAAO,cAAc,SAAS,CAAC,MAAM,QAAQ,OAAO,cAAc,MAAM,EAAE;AAC7E,MAAG,KACD,KAAK,UAAU,EACb,OAAO;IACL,MAAM;IACN,SAAS;IACT,QAAQ;IACT,EACF,CAAC,CACH;AACD;;AAEF,gBAAc,sBAAsB,OAAO,cAAc,MAAM;YACtD,OAAO,cAAc;AAC9B,MACE,CAAC,OAAO,aAAa,qBACrB,CAAC,MAAM,QAAQ,OAAO,aAAa,kBAAkB,EACrD;AACA,MAAG,KACD,KAAK,UAAU,EACb,OAAO;IACL,MAAM;IACN,SAAS;IACT,QAAQ;IACT,EACF,CAAC,CACH;AACD;;AAEF,gBAAc,uBAAuB,OAAO,aAAa;QACpD;AACL,KAAG,KACD,KAAK,UAAU,EACb,OAAO;GACL,MAAM;GACN,SAAS;GACT,QAAQ;GACT,EACF,CAAC,CACH;AACD;;CAIF,MAAM,gBAAuC;EAC3C,OAAO,QAAQ;EACf,UAAU,CAAC,GAAG,QAAQ,qBAAqB,GAAG,YAAY;EAC1D,QAAQ;EACR,OAAO,QAAQ,MAAM,SAAS,IAAI,QAAQ,QAAQ;EACnD;CAED,MAAM,SAAS,SAAS,UAAUA;CAClC,MAAM,UAAUC,4BACd,UACA,eACA,QAAQ,6BAA6B,OAAO,EAC5C,SAAS,iBACV;CACD,MAAM,OAAO;AAEb,KAAI,QACF,SAAQ,2BAA2B,SAAS,UAAU,OAAO;AAG/D,KAAI,CAAC,SAAS;AACZ,MAAI,SAAS,QAAQ;AACnB,YAAS,OAAO,KAAK,mDAAmD;AACxE,MAAG,MAAM,MAAM,kCAAkC;AACjD;;AAEF,UAAQ,IAAI;GACV,QAAQ;GACR;GACA,SAAS,EAAE;GACX,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK,SAAS;IAAM;GACzC,CAAC;AACF,KAAG,KACD,KAAK,UAAU,EACb,OAAO;GAAE,MAAM;GAAK,SAAS;GAAsB,QAAQ;GAAa,EACzE,CAAC,CACH;AACD;;AAIF,SAAQ,oBAAoB,KAAK,GAAG,YAAY;CAEhD,MAAM,WAAW,QAAQ;CACzB,MAAM,UAAU,QAAQ,WAAW,SAAS;CAC5C,MAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,aAAa,SAAS,UAAU;AAGtE,KAAIC,gCAAgB,SAAS,EAAE;EAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,UAAQ,IAAI;GACV,QAAQ;GACR;GACA,SAAS,EAAE;GACX,MAAM;GACN,UAAU;IAAE;IAAQ;IAAS;GAC9B,CAAC;AACF,KAAG,KACD,KAAK,UAAU,EACb,OAAO;GAAE,MAAM;GAAQ,SAAS,SAAS,MAAM;GAAS,QAAQ;GAAS,EAC1E,CAAC,CACH;AACD;;AAIF,KAAIC,+BAAe,SAAS,EAAE;EAC5B,MAAM,eAAe,QAAQ,IAAI;GAC/B,QAAQ;GACR;GACA,SAAS,EAAE;GACX,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;EAEF,MAAM,UAAU,SAAS;AAEzB,MAAI,QAAQ,WAAW,GAAG;AACxB,OAAI,GAAG,SAAU;AACjB,MAAG,KACD,KAAK,UAAU,EACb,eAAe;IACb,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC,EAAE;IACpC,cAAc;IACf,EACF,CAAC,CACH;AACD;;EAIF,MAAM,SAAmB,EAAE;AAC3B,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,UACvC,QAAO,KAAK,QAAQ,MAAM,GAAG,IAAI,UAAU,CAAC;EAG9C,MAAM,eAAeC,8CAAyB,QAAQ;EACtD,IAAI,cAAc;AAElB,OAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,OAAI,GAAG,SAAU;AACjB,OAAI,UAAU,EAAG,OAAMC,yBAAM,SAAS,cAAc,OAAO;AAC3D,OAAI,cAAc,OAAO,SAAS;AAChC,kBAAc;AACd;;AAEF,OAAI,GAAG,SAAU;GAEjB,MAAM,SAAS,MAAM,OAAO,SAAS;AACrC,MAAG,KACD,KAAK,UAAU,EACb,eAAe;IACb,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,OAAO,IAAI,CAAC,EAAE;IAC3C,cAAc;IACf,EACF,CAAC,CACH;AACD,iBAAc,MAAM;AACpB,OAAI,cAAc,OAAO,SAAS;AAChC,kBAAc;AACd;;;AAIJ,MAAI,aAAa;AACf,MAAG,SAAS;AACZ,gBAAa,SAAS,cAAc;AACpC,gBAAa,SAAS,kBAAkB,cAAc,QAAQ;AAC9D,iBAAc,SAAS;AACvB;;AAGF,gBAAc,SAAS;AAGvB,UAAQ,oBAAoB,KAAK;GAAE,MAAM;GAAa;GAAS,CAAC;AAChE;;AAIF,KAAIC,mCAAmB,SAAS,EAAE;EAChC,MAAM,eAAe,QAAQ,IAAI;GAC/B,QAAQ;GACR;GACA,SAAS,EAAE;GACX,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;EAEF,MAAM,eAAeF,8CAAyB,QAAQ;AAEtD,MAAI,GAAG,UAAU;AACf,iBAAc,SAAS;AACvB;;AAEF,MAAI,UAAU,EAAG,OAAMC,yBAAM,SAAS,cAAc,OAAO;AAC3D,MAAI,cAAc,OAAO,SAAS;AAChC,MAAG,SAAS;AACZ,gBAAa,SAAS,cAAc;AACpC,gBAAa,SAAS,kBAAkB,cAAc,QAAQ;AAC9D,iBAAc,SAAS;AACvB;;AAEF,MAAI,GAAG,UAAU;AACf,iBAAc,SAAS;AACvB;;EAGF,MAAM,gBAAgB,SAAS,UAAU,KAAK,IAAI,MAAM;GACtD,IAAI;AACJ,OAAI;AACF,cAAU,KAAK,MAAM,GAAG,aAAa,KAAK;WACpC;AACN,aAAS,OAAO,KACd,sDAAsD,GAAG,KAAK,KAAK,GAAG,YACvE;AACD,cAAU,EAAE;;AAEd,UAAO;IACL,MAAM,GAAG;IACT,MAAM;IACN,IAAI,GAAG,MAAM,eAAe,GAAG,KAAK,GAAG;IACxC;IACD;AAEF,KAAG,KAAK,KAAK,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,CAAC;AACxD,gBAAc,MAAM;AAEpB,MAAI,cAAc,OAAO,SAAS;AAChC,MAAG,SAAS;AACZ,gBAAa,SAAS,cAAc;AACpC,gBAAa,SAAS,kBAAkB,cAAc,QAAQ;AAC9D,iBAAc,SAAS;AACvB;;AAGF,gBAAc,SAAS;AAGvB,UAAQ,oBAAoB,KAAK;GAC/B,MAAM;GACN,SAAS;GACT,YAAY,SAAS,UAAU,KAAK,IAAI,OAAO;IAC7C,IAAI,GAAG,MAAM,eAAe,GAAG,KAAK,GAAG;IACvC,MAAM;IACN,UAAU;KACR,MAAM,GAAG;KACT,WAAW,GAAG;KACf;IACF,EAAE;GACJ,CAAC;AACF;;AAIF,SAAQ,IAAI;EACV,QAAQ;EACR;EACA,SAAS,EAAE;EACX,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK;GAAS;EACnC,CAAC;AACF,IAAG,KACD,KAAK,UAAU,EACb,OAAO;EACL,MAAM;EACN,SAAS;EACT,QAAQ;EACT,EACF,CAAC,CACH"}
|
|
1
|
+
{"version":3,"file":"ws-gemini-live.cjs","names":["DEFAULT_TEST_ID","matchFixture","isErrorResponse","isAudioResponse","formatToMime","isContentWithToolCallsResponse","createInterruptionSignal","delay","generateToolCallId","isTextResponse","isToolCallResponse"],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":["/**\n * WebSocket handler for Gemini Live BidiGenerateContent API.\n *\n * Accepts setup, clientContent, and toolResponse messages over WebSocket\n * and responds with setupComplete, serverContent, toolCall, and error\n * messages in the Gemini Live streaming format.\n */\n\nimport type {\n Fixture,\n ChatMessage,\n ChatCompletionRequest,\n ToolDefinition,\n AudioResponse,\n} from \"./types.js\";\nimport { matchFixture } from \"./router.js\";\nimport {\n isTextResponse,\n isToolCallResponse,\n isContentWithToolCallsResponse,\n isErrorResponse,\n isAudioResponse,\n formatToMime,\n generateToolCallId,\n} from \"./helpers.js\";\nimport { createInterruptionSignal } from \"./interruption.js\";\nimport { delay } from \"./sse-writer.js\";\nimport { DEFAULT_TEST_ID, type Journal } from \"./journal.js\";\nimport type { Logger } from \"./logger.js\";\nimport type { WebSocketConnection } from \"./ws-framing.js\";\n\n// ─── Gemini Live protocol types ─────────────────────────────────────────────\n\ninterface GeminiLivePart {\n text?: string;\n thought?: boolean;\n functionCall?: { name: string; args: Record<string, unknown> };\n functionResponse?: { name: string; response: unknown; id?: string };\n inlineData?: { mimeType: string; data: string };\n}\n\ninterface GeminiLiveTurn {\n role: string;\n parts: GeminiLivePart[];\n}\n\ninterface GeminiLiveFunctionDeclaration {\n name: string;\n description?: string;\n parameters?: object;\n}\n\ninterface GeminiLiveToolDef {\n functionDeclarations?: GeminiLiveFunctionDeclaration[];\n}\n\ninterface GeminiLiveSetup {\n model?: string;\n generationConfig?: Record<string, unknown>;\n tools?: GeminiLiveToolDef[];\n}\n\ninterface GeminiLiveClientContent {\n turns: GeminiLiveTurn[];\n turnComplete?: boolean;\n}\n\ninterface GeminiLiveFunctionResponse {\n id?: string;\n name: string;\n response: unknown;\n}\n\ninterface GeminiLiveToolResponse {\n functionResponses: GeminiLiveFunctionResponse[];\n}\n\ninterface GeminiLiveMessage {\n setup?: GeminiLiveSetup;\n clientContent?: GeminiLiveClientContent;\n toolResponse?: GeminiLiveToolResponse;\n}\n\n// ─── Session state ──────────────────────────────────────────────────────────\n\ninterface SessionState {\n setupDone: boolean;\n model: string;\n tools: ToolDefinition[];\n conversationHistory: ChatMessage[];\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nconst WS_PATH = \"/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\";\n\n/**\n * Convert Gemini Live turns into ChatMessage[] for fixture matching.\n */\nfunction geminiTurnsToMessages(turns: GeminiLiveTurn[]): ChatMessage[] {\n const messages: ChatMessage[] = [];\n\n for (const turn of turns) {\n const role = turn.role ?? \"user\";\n\n if (role === \"user\") {\n const funcResponses = turn.parts.filter((p) => p.functionResponse);\n // inlineData parts (e.g. client audio input) are silently skipped —\n // only text and functionResponse parts are relevant for fixture matching.\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcResponses.length > 0) {\n for (let i = 0; i < funcResponses.length; i++) {\n const part = funcResponses[i];\n const fr = part.functionResponse!;\n messages.push({\n role: \"tool\",\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n });\n }\n if (textParts.length > 0) {\n messages.push({\n role: \"user\",\n content: textParts.map((p) => p.text!).join(\"\"),\n });\n }\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"user\", content: text });\n }\n } else if (role === \"model\") {\n const funcCalls = turn.parts.filter((p) => p.functionCall);\n const textParts = turn.parts.filter((p) => p.text !== undefined && !p.thought);\n\n if (funcCalls.length > 0) {\n messages.push({\n role: \"assistant\",\n content: null,\n tool_calls: funcCalls.map((p, i) => ({\n id: `call_gemini_${p.functionCall!.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: p.functionCall!.name,\n arguments: JSON.stringify(p.functionCall!.args),\n },\n })),\n });\n } else {\n const text = textParts.map((p) => p.text!).join(\"\");\n messages.push({ role: \"assistant\", content: text });\n }\n }\n }\n\n return messages;\n}\n\n/**\n * Convert toolResponse messages into ChatMessage[] for fixture matching.\n */\nfunction toolResponseToMessages(toolResponse: GeminiLiveToolResponse): ChatMessage[] {\n return toolResponse.functionResponses.map((fr, i) => ({\n role: \"tool\" as const,\n content: typeof fr.response === \"string\" ? fr.response : JSON.stringify(fr.response),\n tool_call_id: fr.id ?? `call_gemini_${fr.name}_${i}`,\n }));\n}\n\n/**\n * Convert Gemini tool definitions to ChatCompletion ToolDefinition[].\n */\nfunction convertTools(geminiTools?: GeminiLiveToolDef[]): ToolDefinition[] {\n if (!geminiTools || geminiTools.length === 0) return [];\n const decls = geminiTools.flatMap((t) => t.functionDeclarations ?? []);\n return decls.map((d) => ({\n type: \"function\" as const,\n function: {\n name: d.name,\n description: d.description,\n parameters: d.parameters,\n },\n }));\n}\n\n// ─── Main handler ───────────────────────────────────────────────────────────\n\nexport function handleWebSocketGeminiLive(\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n },\n): void {\n const { logger } = defaults;\n const session: SessionState = {\n setupDone: false,\n model: defaults.model,\n tools: [],\n conversationHistory: [],\n };\n\n let pending = Promise.resolve();\n ws.on(\"message\", (raw: string) => {\n pending = pending.then(() =>\n processMessage(raw, ws, fixtures, journal, defaults, session).catch((err: unknown) => {\n const msg = err instanceof Error ? err.message : \"Internal error\";\n logger.error(`WebSocket Gemini Live error: ${msg}`);\n try {\n ws.send(\n JSON.stringify({\n error: { code: 500, message: msg, status: \"INTERNAL\" },\n }),\n );\n } catch {\n // Connection already gone — original error already logged above\n }\n }),\n );\n });\n}\n\nasync function processMessage(\n raw: string,\n ws: WebSocketConnection,\n fixtures: Fixture[],\n journal: Journal,\n defaults: {\n latency: number;\n chunkSize: number;\n model: string;\n logger: Logger;\n strict?: boolean;\n requestTransform?: (req: ChatCompletionRequest) => ChatCompletionRequest;\n testId?: string;\n },\n session: SessionState,\n): Promise<void> {\n let parsed: GeminiLiveMessage;\n try {\n parsed = JSON.parse(raw) as GeminiLiveMessage;\n } catch {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Malformed JSON\", status: \"INVALID_ARGUMENT\" },\n }),\n );\n return;\n }\n\n // Handle setup message\n if (parsed.setup) {\n session.setupDone = true;\n session.model = parsed.setup.model ?? defaults.model;\n session.tools = convertTools(parsed.setup.tools);\n ws.send(JSON.stringify({ setupComplete: {} }));\n return;\n }\n\n // Reject messages before setup\n if (!session.setupDone) {\n ws.send(\n JSON.stringify({\n error: { code: 400, message: \"Setup required\", status: \"FAILED_PRECONDITION\" },\n }),\n );\n return;\n }\n\n // Build messages from this interaction\n let newMessages: ChatMessage[];\n\n if (parsed.clientContent) {\n if (!parsed.clientContent.turns || !Array.isArray(parsed.clientContent.turns)) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'turns' in clientContent\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = geminiTurnsToMessages(parsed.clientContent.turns);\n } else if (parsed.toolResponse) {\n if (\n !parsed.toolResponse.functionResponses ||\n !Array.isArray(parsed.toolResponse.functionResponses)\n ) {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Missing 'functionResponses' in toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n newMessages = toolResponseToMessages(parsed.toolResponse);\n } else {\n ws.send(\n JSON.stringify({\n error: {\n code: 400,\n message: \"Expected clientContent or toolResponse\",\n status: \"INVALID_ARGUMENT\",\n },\n }),\n );\n return;\n }\n\n // Build completion request for fixture matching (include new messages speculatively)\n const completionReq: ChatCompletionRequest = {\n model: session.model,\n messages: [...session.conversationHistory, ...newMessages],\n stream: true,\n tools: session.tools.length > 0 ? session.tools : undefined,\n };\n\n const testId = defaults.testId ?? DEFAULT_TEST_ID;\n const fixture = matchFixture(\n fixtures,\n completionReq,\n journal.getFixtureMatchCountsForTest(testId),\n defaults.requestTransform,\n );\n const path = WS_PATH;\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n if (!fixture) {\n if (defaults.strict) {\n defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.close(1008, \"Strict mode: no fixture matched\");\n return;\n }\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 404, fixture: null },\n });\n ws.send(\n JSON.stringify({\n error: { code: 404, message: \"No fixture matched\", status: \"NOT_FOUND\" },\n }),\n );\n return;\n }\n\n // Commit messages to conversation history only after successful fixture match\n session.conversationHistory.push(...newMessages);\n\n const response = fixture.response;\n const latency = fixture.latency ?? defaults.latency;\n const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);\n\n // Error response\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status, fixture },\n });\n ws.send(\n JSON.stringify({\n error: { code: status, message: response.error.message, status: \"ERROR\" },\n }),\n );\n return;\n }\n\n // Audio response — single frame with inlineData and turnComplete: true\n if (isAudioResponse(response)) {\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const audioResp = response as AudioResponse;\n let mimeType: string;\n let data: string;\n\n if (typeof audioResp.audio === \"string\") {\n mimeType = formatToMime(audioResp.format ?? \"mp3\");\n data = audioResp.audio;\n } else {\n mimeType = audioResp.audio.contentType ?? \"audio/mpeg\";\n data = audioResp.audio.b64Json;\n }\n\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: {\n parts: [{ inlineData: { mimeType, data } }],\n },\n turnComplete: true,\n },\n }),\n );\n\n session.conversationHistory.push({\n role: \"assistant\",\n content: \"[audio]\",\n });\n return;\n }\n\n // Content + tool calls response (must be checked before isTextResponse / isToolCallResponse)\n if (isContentWithToolCallsResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n const chunkList: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunkList.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n // Stream text content chunks\n if (content.length === 0) {\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n turnComplete: false,\n },\n }),\n );\n }\n } else {\n for (let i = 0; i < chunkList.length; i++) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunkList[i] }] },\n turnComplete: false,\n },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n // Pre-compute tool calls with stable IDs so wire message and history match\n const resolvedToolCalls = response.toolCalls.map((tc) => ({\n ...tc,\n resolvedId: tc.id ?? generateToolCallId(),\n }));\n\n // Send tool calls\n if (!ws.isClosed) {\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = resolvedToolCalls.map((tc) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.resolvedId,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n }\n\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Send turnComplete\n if (!ws.isClosed) {\n ws.send(\n JSON.stringify({\n serverContent: { turnComplete: true },\n }),\n );\n }\n\n // Add to conversation history using the same resolved IDs from the wire message\n session.conversationHistory.push({\n role: \"assistant\",\n content: content || null,\n tool_calls: resolvedToolCalls.map((tc) => ({\n id: tc.resolvedId,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Text response — stream chunks with serverContent\n if (isTextResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const content = response.content;\n\n if (content.length === 0) {\n if (ws.isClosed) return;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: \"\" }] },\n turnComplete: true,\n },\n }),\n );\n return;\n }\n\n // Chunk the content\n const chunks: string[] = [];\n for (let i = 0; i < content.length; i += chunkSize) {\n chunks.push(content.slice(i, i + chunkSize));\n }\n\n const interruption = createInterruptionSignal(fixture);\n let interrupted = false;\n\n for (let i = 0; i < chunks.length; i++) {\n if (ws.isClosed) break;\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n if (ws.isClosed) break;\n\n const isLast = i === chunks.length - 1;\n ws.send(\n JSON.stringify({\n serverContent: {\n modelTurn: { parts: [{ text: chunks[i] }] },\n turnComplete: isLast,\n },\n }),\n );\n interruption?.tick();\n if (interruption?.signal.aborted) {\n interrupted = true;\n break;\n }\n }\n\n if (interrupted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Add assistant response to conversation history\n session.conversationHistory.push({ role: \"assistant\", content });\n return;\n }\n\n // Tool call response\n if (isToolCallResponse(response)) {\n const journalEntry = journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 200, fixture },\n });\n\n const interruption = createInterruptionSignal(fixture);\n\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n if (latency > 0) await delay(latency, interruption?.signal);\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n if (ws.isClosed) {\n interruption?.cleanup();\n return;\n }\n\n const functionCalls = response.toolCalls.map((tc, i) => {\n let argsObj: Record<string, unknown>;\n try {\n argsObj = JSON.parse(tc.arguments || \"{}\") as Record<string, unknown>;\n } catch {\n defaults.logger.warn(\n `Malformed JSON in fixture tool call arguments for \"${tc.name}\": ${tc.arguments}`,\n );\n argsObj = {};\n }\n return {\n name: tc.name,\n args: argsObj,\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n };\n });\n\n ws.send(JSON.stringify({ toolCall: { functionCalls } }));\n interruption?.tick();\n\n if (interruption?.signal.aborted) {\n ws.destroy();\n journalEntry.response.interrupted = true;\n journalEntry.response.interruptReason = interruption?.reason();\n interruption?.cleanup();\n return;\n }\n\n interruption?.cleanup();\n\n // Add assistant tool_calls to conversation history\n session.conversationHistory.push({\n role: \"assistant\",\n content: null,\n tool_calls: response.toolCalls.map((tc, i) => ({\n id: tc.id ?? `call_gemini_${tc.name}_${i}`,\n type: \"function\" as const,\n function: {\n name: tc.name,\n arguments: tc.arguments,\n },\n })),\n });\n return;\n }\n\n // Unknown response type\n journal.add({\n method: \"WS\",\n path,\n headers: {},\n body: completionReq,\n response: { status: 500, fixture },\n });\n ws.send(\n JSON.stringify({\n error: {\n code: 500,\n message: \"Fixture response did not match any known type\",\n status: \"INTERNAL\",\n },\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-gemini-live.d.cts","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"ws-gemini-live.d.cts","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":[],"mappings":";;;;;;;AAmMY,iBARI,yBAAA,CAQJ,EAAA,EAPN,mBAOM,EAAA,QAAA,EANA,OAMA,EAAA,EAAA,OAAA,EALD,OAKC,EAAA,QAAA,EAAA;SAEiB,EAAA,MAAA;WAA0B,EAAA,MAAA;EAAqB,KAAA,EAAA,MAAA;UAFhE;;2BAEiB,0BAA0B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-gemini-live.d.ts","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":[],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"ws-gemini-live.d.ts","names":[],"sources":["../src/ws-gemini-live.ts"],"sourcesContent":[],"mappings":";;;;;;;AAmMY,iBARI,yBAAA,CAQJ,EAAA,EAPN,mBAOM,EAAA,QAAA,EANA,OAMA,EAAA,EAAA,OAAA,EALD,OAKC,EAAA,QAAA,EAAA;SAEiB,EAAA,MAAA;WAA0B,EAAA,MAAA;EAAqB,KAAA,EAAA,MAAA;UAFhE;;2BAEiB,0BAA0B"}
|
package/dist/ws-gemini-live.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isErrorResponse, isTextResponse, isToolCallResponse } from "./helpers.js";
|
|
1
|
+
import { formatToMime, generateToolCallId, isAudioResponse, isContentWithToolCallsResponse, isErrorResponse, isTextResponse, isToolCallResponse } from "./helpers.js";
|
|
2
2
|
import { DEFAULT_TEST_ID } from "./journal.js";
|
|
3
3
|
import { matchFixture } from "./router.js";
|
|
4
4
|
import { delay } from "./sse-writer.js";
|
|
@@ -15,7 +15,7 @@ function geminiTurnsToMessages(turns) {
|
|
|
15
15
|
const role = turn.role ?? "user";
|
|
16
16
|
if (role === "user") {
|
|
17
17
|
const funcResponses = turn.parts.filter((p) => p.functionResponse);
|
|
18
|
-
const textParts = turn.parts.filter((p) => p.text !== void 0);
|
|
18
|
+
const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
|
|
19
19
|
if (funcResponses.length > 0) {
|
|
20
20
|
for (let i = 0; i < funcResponses.length; i++) {
|
|
21
21
|
const fr = funcResponses[i].functionResponse;
|
|
@@ -38,7 +38,7 @@ function geminiTurnsToMessages(turns) {
|
|
|
38
38
|
}
|
|
39
39
|
} else if (role === "model") {
|
|
40
40
|
const funcCalls = turn.parts.filter((p) => p.functionCall);
|
|
41
|
-
const textParts = turn.parts.filter((p) => p.text !== void 0);
|
|
41
|
+
const textParts = turn.parts.filter((p) => p.text !== void 0 && !p.thought);
|
|
42
42
|
if (funcCalls.length > 0) messages.push({
|
|
43
43
|
role: "assistant",
|
|
44
44
|
content: null,
|
|
@@ -178,6 +178,16 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
178
178
|
if (!fixture) {
|
|
179
179
|
if (defaults.strict) {
|
|
180
180
|
defaults.logger.warn(`STRICT: No fixture matched for WebSocket message`);
|
|
181
|
+
journal.add({
|
|
182
|
+
method: "WS",
|
|
183
|
+
path,
|
|
184
|
+
headers: {},
|
|
185
|
+
body: completionReq,
|
|
186
|
+
response: {
|
|
187
|
+
status: 404,
|
|
188
|
+
fixture: null
|
|
189
|
+
}
|
|
190
|
+
});
|
|
181
191
|
ws.close(1008, "Strict mode: no fixture matched");
|
|
182
192
|
return;
|
|
183
193
|
}
|
|
@@ -221,6 +231,139 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session) {
|
|
|
221
231
|
} }));
|
|
222
232
|
return;
|
|
223
233
|
}
|
|
234
|
+
if (isAudioResponse(response)) {
|
|
235
|
+
journal.add({
|
|
236
|
+
method: "WS",
|
|
237
|
+
path,
|
|
238
|
+
headers: {},
|
|
239
|
+
body: completionReq,
|
|
240
|
+
response: {
|
|
241
|
+
status: 200,
|
|
242
|
+
fixture
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
const audioResp = response;
|
|
246
|
+
let mimeType;
|
|
247
|
+
let data;
|
|
248
|
+
if (typeof audioResp.audio === "string") {
|
|
249
|
+
mimeType = formatToMime(audioResp.format ?? "mp3");
|
|
250
|
+
data = audioResp.audio;
|
|
251
|
+
} else {
|
|
252
|
+
mimeType = audioResp.audio.contentType ?? "audio/mpeg";
|
|
253
|
+
data = audioResp.audio.b64Json;
|
|
254
|
+
}
|
|
255
|
+
ws.send(JSON.stringify({ serverContent: {
|
|
256
|
+
modelTurn: { parts: [{ inlineData: {
|
|
257
|
+
mimeType,
|
|
258
|
+
data
|
|
259
|
+
} }] },
|
|
260
|
+
turnComplete: true
|
|
261
|
+
} }));
|
|
262
|
+
session.conversationHistory.push({
|
|
263
|
+
role: "assistant",
|
|
264
|
+
content: "[audio]"
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (isContentWithToolCallsResponse(response)) {
|
|
269
|
+
const journalEntry = journal.add({
|
|
270
|
+
method: "WS",
|
|
271
|
+
path,
|
|
272
|
+
headers: {},
|
|
273
|
+
body: completionReq,
|
|
274
|
+
response: {
|
|
275
|
+
status: 200,
|
|
276
|
+
fixture
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const content = response.content;
|
|
280
|
+
const chunkList = [];
|
|
281
|
+
for (let i = 0; i < content.length; i += chunkSize) chunkList.push(content.slice(i, i + chunkSize));
|
|
282
|
+
const interruption = createInterruptionSignal(fixture);
|
|
283
|
+
let interrupted = false;
|
|
284
|
+
if (content.length === 0) {
|
|
285
|
+
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: {
|
|
286
|
+
modelTurn: { parts: [{ text: "" }] },
|
|
287
|
+
turnComplete: false
|
|
288
|
+
} }));
|
|
289
|
+
} else for (let i = 0; i < chunkList.length; i++) {
|
|
290
|
+
if (ws.isClosed) break;
|
|
291
|
+
if (latency > 0) await delay(latency, interruption?.signal);
|
|
292
|
+
if (interruption?.signal.aborted) {
|
|
293
|
+
interrupted = true;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
if (ws.isClosed) break;
|
|
297
|
+
ws.send(JSON.stringify({ serverContent: {
|
|
298
|
+
modelTurn: { parts: [{ text: chunkList[i] }] },
|
|
299
|
+
turnComplete: false
|
|
300
|
+
} }));
|
|
301
|
+
interruption?.tick();
|
|
302
|
+
if (interruption?.signal.aborted) {
|
|
303
|
+
interrupted = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (interrupted) {
|
|
308
|
+
ws.destroy();
|
|
309
|
+
journalEntry.response.interrupted = true;
|
|
310
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
311
|
+
interruption?.cleanup();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const resolvedToolCalls = response.toolCalls.map((tc) => ({
|
|
315
|
+
...tc,
|
|
316
|
+
resolvedId: tc.id ?? generateToolCallId()
|
|
317
|
+
}));
|
|
318
|
+
if (!ws.isClosed) {
|
|
319
|
+
if (latency > 0) await delay(latency, interruption?.signal);
|
|
320
|
+
if (interruption?.signal.aborted) {
|
|
321
|
+
ws.destroy();
|
|
322
|
+
journalEntry.response.interrupted = true;
|
|
323
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
324
|
+
interruption?.cleanup();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const functionCalls = resolvedToolCalls.map((tc) => {
|
|
328
|
+
let argsObj;
|
|
329
|
+
try {
|
|
330
|
+
argsObj = JSON.parse(tc.arguments || "{}");
|
|
331
|
+
} catch {
|
|
332
|
+
defaults.logger.warn(`Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`);
|
|
333
|
+
argsObj = {};
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
name: tc.name,
|
|
337
|
+
args: argsObj,
|
|
338
|
+
id: tc.resolvedId
|
|
339
|
+
};
|
|
340
|
+
});
|
|
341
|
+
ws.send(JSON.stringify({ toolCall: { functionCalls } }));
|
|
342
|
+
interruption?.tick();
|
|
343
|
+
}
|
|
344
|
+
if (interruption?.signal.aborted) {
|
|
345
|
+
ws.destroy();
|
|
346
|
+
journalEntry.response.interrupted = true;
|
|
347
|
+
journalEntry.response.interruptReason = interruption?.reason();
|
|
348
|
+
interruption?.cleanup();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
interruption?.cleanup();
|
|
352
|
+
if (!ws.isClosed) ws.send(JSON.stringify({ serverContent: { turnComplete: true } }));
|
|
353
|
+
session.conversationHistory.push({
|
|
354
|
+
role: "assistant",
|
|
355
|
+
content: content || null,
|
|
356
|
+
tool_calls: resolvedToolCalls.map((tc) => ({
|
|
357
|
+
id: tc.resolvedId,
|
|
358
|
+
type: "function",
|
|
359
|
+
function: {
|
|
360
|
+
name: tc.name,
|
|
361
|
+
arguments: tc.arguments
|
|
362
|
+
}
|
|
363
|
+
}))
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
224
367
|
if (isTextResponse(response)) {
|
|
225
368
|
const journalEntry = journal.add({
|
|
226
369
|
method: "WS",
|