@copilotkit/aimock 1.21.0 → 1.22.1
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 +56 -0
- package/README.md +1 -0
- package/dist/a2a-mock.cjs +1 -1
- package/dist/a2a-mock.cjs.map +1 -1
- package/dist/a2a-mock.d.cts.map +1 -1
- package/dist/a2a-mock.d.ts.map +1 -1
- package/dist/a2a-mock.js +1 -1
- package/dist/a2a-mock.js.map +1 -1
- package/dist/agui-recorder.cjs +25 -12
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.js +25 -12
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/aimock-cli.cjs +0 -0
- package/dist/aimock-cli.js +0 -0
- package/dist/bedrock-converse.cjs +72 -26
- 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 +73 -27
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +69 -24
- 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 +70 -25
- package/dist/bedrock.js.map +1 -1
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +34 -11
- 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 +35 -12
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/constants.cjs +8 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +8 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/elevenlabs-audio.cjs +46 -20
- 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 +47 -21
- package/dist/elevenlabs-audio.js.map +1 -1
- package/dist/embeddings.cjs +25 -21
- 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 +26 -22
- package/dist/embeddings.js.map +1 -1
- package/dist/fal-audio.cjs +138 -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 +139 -44
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +27 -8
- package/dist/fal.cjs.map +1 -1
- package/dist/fal.d.cts.map +1 -1
- package/dist/fal.d.ts.map +1 -1
- package/dist/fal.js +28 -9
- package/dist/fal.js.map +1 -1
- package/dist/fixture-loader.cjs +9 -1
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.js +9 -1
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +34 -9
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +34 -11
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +50 -21
- 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 +51 -22
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +82 -8
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +7 -0
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +80 -9
- package/dist/helpers.js.map +1 -1
- package/dist/images.cjs +31 -10
- 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 +32 -11
- package/dist/images.js.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/journal.cjs +17 -7
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +2 -3
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +2 -3
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +15 -4
- package/dist/journal.js.map +1 -1
- package/dist/mcp-mock.cjs +1 -1
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +1 -1
- package/dist/mcp-mock.js.map +1 -1
- package/dist/messages.cjs +38 -14
- 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 +39 -15
- package/dist/messages.js.map +1 -1
- package/dist/moderation.cjs +3 -2
- package/dist/moderation.cjs.map +1 -1
- package/dist/moderation.js +3 -2
- package/dist/moderation.js.map +1 -1
- package/dist/ollama.cjs +69 -22
- 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 +70 -23
- package/dist/ollama.js.map +1 -1
- package/dist/recorder.cjs +89 -41
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +3 -2
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +3 -2
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +89 -41
- package/dist/recorder.js.map +1 -1
- package/dist/rerank.cjs +3 -2
- package/dist/rerank.cjs.map +1 -1
- package/dist/rerank.js +3 -2
- package/dist/rerank.js.map +1 -1
- package/dist/responses.cjs +66 -54
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +1 -1
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +1 -1
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +67 -55
- package/dist/responses.js.map +1 -1
- package/dist/search.cjs +3 -2
- package/dist/search.cjs.map +1 -1
- package/dist/search.js +3 -2
- package/dist/search.js.map +1 -1
- package/dist/server.cjs +117 -171
- 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 +95 -149
- package/dist/server.js.map +1 -1
- package/dist/speech.cjs +31 -10
- 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 +32 -11
- package/dist/speech.js.map +1 -1
- package/dist/stream-collapse.cjs +51 -21
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +1 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +1 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +51 -21
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +59 -19
- 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 +60 -20
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +4 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-mock.cjs +10 -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 +10 -8
- package/dist/vector-mock.js.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/video.cjs +55 -16
- package/dist/video.cjs.map +1 -1
- package/dist/video.d.cts +8 -1
- package/dist/video.d.cts.map +1 -1
- package/dist/video.d.ts +8 -1
- package/dist/video.d.ts.map +1 -1
- package/dist/video.js +56 -17
- package/dist/video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +40 -31
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +2 -0
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +2 -0
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +40 -31
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +257 -16
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +2 -0
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +2 -0
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +257 -16
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +54 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +2 -0
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +2 -0
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +55 -17
- package/dist/ws-responses.js.map +1 -1
- package/package.json +2 -2
package/dist/elevenlabs-audio.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { FORMAT_TO_CONTENT_TYPE, getTestId, isAudioResponse, isErrorResponse, isTextResponse, resolveResponse } from "./helpers.js";
|
|
1
|
+
import { FORMAT_TO_CONTENT_TYPE, flattenHeaders, getTestId, isAudioResponse, isErrorResponse, isTextResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
|
|
2
2
|
import { matchFixture } from "./router.js";
|
|
3
3
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
|
+
import { applyChaos } from "./chaos.js";
|
|
4
5
|
import { proxyAndRecord } from "./recorder.js";
|
|
5
6
|
|
|
6
7
|
//#region src/elevenlabs-audio.ts
|
|
@@ -10,11 +11,12 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
10
11
|
let parsed;
|
|
11
12
|
try {
|
|
12
13
|
parsed = JSON.parse(body);
|
|
13
|
-
} catch {
|
|
14
|
+
} catch (parseErr) {
|
|
15
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
14
16
|
journal.add({
|
|
15
17
|
method,
|
|
16
18
|
path,
|
|
17
|
-
headers:
|
|
19
|
+
headers: flattenHeaders(req.headers),
|
|
18
20
|
body: null,
|
|
19
21
|
response: {
|
|
20
22
|
status: 400,
|
|
@@ -22,7 +24,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
22
24
|
}
|
|
23
25
|
});
|
|
24
26
|
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
25
|
-
message:
|
|
27
|
+
message: `Malformed JSON: ${detail}`,
|
|
26
28
|
type: "invalid_request_error",
|
|
27
29
|
code: "invalid_json"
|
|
28
30
|
} }));
|
|
@@ -46,7 +48,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
46
48
|
journal.add({
|
|
47
49
|
method,
|
|
48
50
|
path,
|
|
49
|
-
headers:
|
|
51
|
+
headers: flattenHeaders(req.headers),
|
|
50
52
|
body: syntheticReq,
|
|
51
53
|
response: {
|
|
52
54
|
status: 400,
|
|
@@ -62,15 +64,40 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
62
64
|
const testId = getTestId(req);
|
|
63
65
|
const fixture = matchFixture(fixtures, syntheticReq, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
|
|
64
66
|
if (fixture) journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
67
|
+
if (applyChaos(res, fixture, defaults.chaos, req.headers, journal, {
|
|
68
|
+
method,
|
|
69
|
+
path,
|
|
70
|
+
headers: flattenHeaders(req.headers),
|
|
71
|
+
body: syntheticReq
|
|
72
|
+
}, fixture ? "fixture" : "proxy", defaults.registry, defaults.logger)) return;
|
|
65
73
|
if (!fixture) {
|
|
74
|
+
if (resolveStrictMode(defaults.strict, req.headers)) {
|
|
75
|
+
journal.add({
|
|
76
|
+
method,
|
|
77
|
+
path,
|
|
78
|
+
headers: {},
|
|
79
|
+
body: syntheticReq,
|
|
80
|
+
response: {
|
|
81
|
+
status: 503,
|
|
82
|
+
fixture: null,
|
|
83
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
writeErrorResponse(res, 503, JSON.stringify({ error: {
|
|
87
|
+
message: "Strict mode: no fixture matched",
|
|
88
|
+
type: "invalid_request_error",
|
|
89
|
+
code: "no_fixture_match"
|
|
90
|
+
} }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
66
93
|
if (defaults.record) {
|
|
67
94
|
const outcome = await proxyAndRecord(req, res, syntheticReq, "elevenlabs", req.url ?? "/v1/sound-generation", fixtures, defaults, body);
|
|
68
95
|
if (outcome === "handled_by_hook") return;
|
|
69
|
-
if (outcome
|
|
96
|
+
if (outcome !== "not_configured") {
|
|
70
97
|
journal.add({
|
|
71
98
|
method,
|
|
72
99
|
path,
|
|
73
|
-
headers:
|
|
100
|
+
headers: flattenHeaders(req.headers),
|
|
74
101
|
body: syntheticReq,
|
|
75
102
|
response: {
|
|
76
103
|
status: res.statusCode ?? 200,
|
|
@@ -81,20 +108,19 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
81
108
|
return;
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
|
-
const strictStatus = defaults.strict ? 503 : 404;
|
|
85
|
-
const strictMessage = defaults.strict ? "Strict mode: no fixture matched" : "No fixture matched";
|
|
86
111
|
journal.add({
|
|
87
112
|
method,
|
|
88
113
|
path,
|
|
89
|
-
headers:
|
|
114
|
+
headers: flattenHeaders(req.headers),
|
|
90
115
|
body: syntheticReq,
|
|
91
116
|
response: {
|
|
92
|
-
status:
|
|
93
|
-
fixture: null
|
|
117
|
+
status: 404,
|
|
118
|
+
fixture: null,
|
|
119
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
94
120
|
}
|
|
95
121
|
});
|
|
96
|
-
writeErrorResponse(res,
|
|
97
|
-
message:
|
|
122
|
+
writeErrorResponse(res, 404, JSON.stringify({ error: {
|
|
123
|
+
message: "No fixture matched",
|
|
98
124
|
type: "invalid_request_error",
|
|
99
125
|
code: "no_fixture_match"
|
|
100
126
|
} }));
|
|
@@ -106,14 +132,14 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
106
132
|
journal.add({
|
|
107
133
|
method,
|
|
108
134
|
path,
|
|
109
|
-
headers:
|
|
135
|
+
headers: flattenHeaders(req.headers),
|
|
110
136
|
body: syntheticReq,
|
|
111
137
|
response: {
|
|
112
138
|
status,
|
|
113
139
|
fixture
|
|
114
140
|
}
|
|
115
141
|
});
|
|
116
|
-
writeErrorResponse(res, status,
|
|
142
|
+
writeErrorResponse(res, status, serializeErrorResponse(response));
|
|
117
143
|
return;
|
|
118
144
|
}
|
|
119
145
|
if (subType === "plan") {
|
|
@@ -121,7 +147,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
121
147
|
journal.add({
|
|
122
148
|
method,
|
|
123
149
|
path,
|
|
124
|
-
headers:
|
|
150
|
+
headers: flattenHeaders(req.headers),
|
|
125
151
|
body: syntheticReq,
|
|
126
152
|
response: {
|
|
127
153
|
status: 500,
|
|
@@ -137,7 +163,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
137
163
|
journal.add({
|
|
138
164
|
method,
|
|
139
165
|
path,
|
|
140
|
-
headers:
|
|
166
|
+
headers: flattenHeaders(req.headers),
|
|
141
167
|
body: syntheticReq,
|
|
142
168
|
response: {
|
|
143
169
|
status: 200,
|
|
@@ -152,7 +178,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
152
178
|
journal.add({
|
|
153
179
|
method,
|
|
154
180
|
path,
|
|
155
|
-
headers:
|
|
181
|
+
headers: flattenHeaders(req.headers),
|
|
156
182
|
body: syntheticReq,
|
|
157
183
|
response: {
|
|
158
184
|
status: 500,
|
|
@@ -179,7 +205,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
179
205
|
journal.add({
|
|
180
206
|
method,
|
|
181
207
|
path,
|
|
182
|
-
headers:
|
|
208
|
+
headers: flattenHeaders(req.headers),
|
|
183
209
|
body: syntheticReq,
|
|
184
210
|
response: {
|
|
185
211
|
status: 200,
|
|
@@ -196,7 +222,7 @@ async function handleElevenLabsAudio(req, res, body, fixtures, defaults, journal
|
|
|
196
222
|
journal.add({
|
|
197
223
|
method,
|
|
198
224
|
path,
|
|
199
|
-
headers:
|
|
225
|
+
headers: flattenHeaders(req.headers),
|
|
200
226
|
body: syntheticReq,
|
|
201
227
|
response: {
|
|
202
228
|
status: 200,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elevenlabs-audio.js","names":[],"sources":["../src/elevenlabs-audio.ts"],"sourcesContent":["import type http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isTextResponse,\n isErrorResponse,\n FORMAT_TO_CONTENT_TYPE,\n getTestId,\n resolveResponse,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport type { Journal } from \"./journal.js\";\n\nexport async function handleElevenLabsAudio(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n subType: string, // \"sound-generation\" | \"music\" | \"stream\" | \"plan\"\n): Promise<void> {\n const path = req.url ?? \"/v1/sound-generation\";\n const method = req.method ?? \"POST\";\n\n // Parse JSON body\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch {\n journal.add({\n method,\n path,\n 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 // Extract prompt text based on subType\n let promptText: string | undefined;\n if (subType === \"sound-generation\") {\n if (typeof parsed.text === \"string\" && parsed.text) {\n promptText = parsed.text;\n }\n } else {\n // music, music-stream, music-plan all use \"prompt\" (or composition_plan fallback)\n if (typeof parsed.prompt === \"string\" && parsed.prompt) {\n promptText = parsed.prompt;\n } else if (parsed.composition_plan != null) {\n promptText =\n typeof parsed.composition_plan === \"string\"\n ? parsed.composition_plan\n : JSON.stringify(parsed.composition_plan);\n }\n }\n\n // Build synthetic ChatCompletionRequest for fixture matching (needed for journal even on validation failure)\n const syntheticReq: ChatCompletionRequest = {\n model:\n (parsed.model_id as string) ??\n (subType === \"sound-generation\" ? \"eleven_text_to_sound_v2\" : \"music_v1\"),\n messages: [{ role: \"user\", content: promptText ?? \"\" }],\n _endpointType: \"audio-gen\",\n };\n\n // Validate required field\n if (!promptText) {\n const field = subType === \"sound-generation\" ? \"text\" : \"prompt\";\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status: 400, fixture: null },\n });\n writeErrorResponse(\n res,\n 400,\n JSON.stringify({\n error: {\n message: `Missing required parameter: '${field}'`,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Match fixture\n const testId = getTestId(req);\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\n }\n\n // No fixture match\n if (!fixture) {\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"elevenlabs\",\n req.url ?? \"/v1/sound-generation\",\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\n if (outcome === \"relayed\") {\n journal.add({\n method,\n path,\n 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: {},\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 = await resolveResponse(fixture, syntheticReq);\n\n // Error fixture\n if (isErrorResponse(response)) {\n const status = response.status ?? 500;\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, JSON.stringify(response));\n return;\n }\n\n // plan returns JSON text, not audio\n if (subType === \"plan\") {\n if (!isTextResponse(response)) {\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status: 500, fixture },\n });\n writeErrorResponse(\n res,\n 500,\n JSON.stringify({\n error: {\n message: \"Fixture response is not a text type for plan endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(response.content);\n return;\n }\n\n // All other subTypes expect audio\n if (!isAudioResponse(response)) {\n journal.add({\n method,\n path,\n 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 an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Decode audio bytes and determine content type\n let audioBytes: Buffer;\n let contentType: string;\n\n if (typeof response.audio === \"string\") {\n audioBytes = Buffer.from(response.audio, \"base64\");\n const format = response.format ?? \"mp3\";\n contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n } else {\n audioBytes = Buffer.from(response.audio.b64Json, \"base64\");\n contentType = response.audio.contentType ?? \"audio/mpeg\";\n }\n\n // Music endpoints get a song-id header\n if (subType === \"music\" || subType === \"stream\") {\n res.setHeader(\"song-id\", \"mock-song-\" + Date.now());\n }\n\n // Stream uses chunked transfer encoding\n if (subType === \"stream\") {\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, {\n \"Content-Type\": contentType,\n \"Transfer-Encoding\": \"chunked\",\n });\n res.end(audioBytes);\n return;\n }\n\n // Standard binary response for sound-generation and music\n journal.add({\n method,\n path,\n headers: {},\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"elevenlabs-audio.js","names":[],"sources":["../src/elevenlabs-audio.ts"],"sourcesContent":["import type http from \"node:http\";\nimport type { ChatCompletionRequest, Fixture, HandlerDefaults } from \"./types.js\";\nimport {\n isAudioResponse,\n isTextResponse,\n isErrorResponse,\n serializeErrorResponse,\n flattenHeaders,\n FORMAT_TO_CONTENT_TYPE,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} from \"./helpers.js\";\nimport { matchFixture } from \"./router.js\";\nimport { writeErrorResponse } from \"./sse-writer.js\";\nimport { proxyAndRecord } from \"./recorder.js\";\nimport type { Journal } from \"./journal.js\";\nimport { applyChaos } from \"./chaos.js\";\n\nexport async function handleElevenLabsAudio(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n body: string,\n fixtures: Fixture[],\n defaults: HandlerDefaults,\n journal: Journal,\n subType: string, // \"sound-generation\" | \"music\" | \"stream\" | \"plan\"\n): Promise<void> {\n const path = req.url ?? \"/v1/sound-generation\";\n const method = req.method ?? \"POST\";\n\n // Parse JSON body\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(body) as Record<string, unknown>;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\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: {\n message: `Malformed JSON: ${detail}`,\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Extract prompt text based on subType\n let promptText: string | undefined;\n if (subType === \"sound-generation\") {\n if (typeof parsed.text === \"string\" && parsed.text) {\n promptText = parsed.text;\n }\n } else {\n // music, music-stream, music-plan all use \"prompt\" (or composition_plan fallback)\n if (typeof parsed.prompt === \"string\" && parsed.prompt) {\n promptText = parsed.prompt;\n } else if (parsed.composition_plan != null) {\n promptText =\n typeof parsed.composition_plan === \"string\"\n ? parsed.composition_plan\n : JSON.stringify(parsed.composition_plan);\n }\n }\n\n // Build synthetic ChatCompletionRequest for fixture matching (needed for journal even on validation failure)\n const syntheticReq: ChatCompletionRequest = {\n model:\n (parsed.model_id as string) ??\n (subType === \"sound-generation\" ? \"eleven_text_to_sound_v2\" : \"music_v1\"),\n messages: [{ role: \"user\", content: promptText ?? \"\" }],\n _endpointType: \"audio-gen\",\n };\n\n // Validate required field\n if (!promptText) {\n const field = subType === \"sound-generation\" ? \"text\" : \"prompt\";\n journal.add({\n method,\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: `Missing required parameter: '${field}'`,\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Match fixture\n const testId = getTestId(req);\n const matchCounts = journal.getFixtureMatchCountsForTest(testId);\n const fixture = matchFixture(fixtures, syntheticReq, matchCounts, defaults.requestTransform);\n\n if (fixture) {\n journal.incrementFixtureMatchCount(fixture, fixtures, testId);\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 // No fixture match\n if (!fixture) {\n const effectiveStrict = resolveStrictMode(defaults.strict, req.headers);\n if (effectiveStrict) {\n journal.add({\n method,\n path,\n 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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n \"elevenlabs\",\n req.url ?? \"/v1/sound-generation\",\n fixtures,\n defaults,\n body,\n );\n if (outcome === \"handled_by_hook\") return;\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 journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: {\n status: 404,\n fixture: null,\n ...strictOverrideField(defaults.strict, req.headers),\n },\n });\n writeErrorResponse(\n res,\n 404,\n JSON.stringify({\n error: {\n message: \"No fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n const response = await resolveResponse(fixture, syntheticReq);\n\n // Error fixture\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, serializeErrorResponse(response));\n return;\n }\n\n // plan returns JSON text, not audio\n if (subType === \"plan\") {\n if (!isTextResponse(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: {\n message: \"Fixture response is not a text type for plan endpoint\",\n type: \"server_error\",\n },\n }),\n );\n return;\n }\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(response.content);\n return;\n }\n\n // All other subTypes expect audio\n if (!isAudioResponse(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 an audio type\", type: \"server_error\" },\n }),\n );\n return;\n }\n\n // Decode audio bytes and determine content type\n let audioBytes: Buffer;\n let contentType: string;\n\n if (typeof response.audio === \"string\") {\n audioBytes = Buffer.from(response.audio, \"base64\");\n const format = response.format ?? \"mp3\";\n contentType = FORMAT_TO_CONTENT_TYPE[format] ?? \"audio/mpeg\";\n } else {\n audioBytes = Buffer.from(response.audio.b64Json, \"base64\");\n contentType = response.audio.contentType ?? \"audio/mpeg\";\n }\n\n // Music endpoints get a song-id header\n if (subType === \"music\" || subType === \"stream\") {\n res.setHeader(\"song-id\", \"mock-song-\" + Date.now());\n }\n\n // Stream uses chunked transfer encoding\n if (subType === \"stream\") {\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, {\n \"Content-Type\": contentType,\n \"Transfer-Encoding\": \"chunked\",\n });\n res.end(audioBytes);\n return;\n }\n\n // Standard binary response for sound-generation and music\n journal.add({\n method,\n path,\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n res.writeHead(200, { \"Content-Type\": contentType });\n res.end(audioBytes);\n}\n"],"mappings":";;;;;;;AAoBA,eAAsB,sBACpB,KACA,KACA,MACA,UACA,UACA,SACA,SACe;CACf,MAAM,OAAO,IAAI,OAAO;CACxB,MAAM,SAAS,IAAI,UAAU;CAG7B,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,KAAK;UAClB,UAAU;EACjB,MAAM,SAAS,oBAAoB,QAAQ,SAAS,UAAU;AAC9D,UAAQ,IAAI;GACV;GACA;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,mBAAmB;GAC5B,MAAM;GACN,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAIF,IAAI;AACJ,KAAI,YAAY,oBACd;MAAI,OAAO,OAAO,SAAS,YAAY,OAAO,KAC5C,cAAa,OAAO;YAIlB,OAAO,OAAO,WAAW,YAAY,OAAO,OAC9C,cAAa,OAAO;UACX,OAAO,oBAAoB,KACpC,cACE,OAAO,OAAO,qBAAqB,WAC/B,OAAO,mBACP,KAAK,UAAU,OAAO,iBAAiB;CAKjD,MAAM,eAAsC;EAC1C,OACG,OAAO,aACP,YAAY,qBAAqB,4BAA4B;EAChE,UAAU,CAAC;GAAE,MAAM;GAAQ,SAAS,cAAc;GAAI,CAAC;EACvD,eAAe;EAChB;AAGD,KAAI,CAAC,YAAY;EACf,MAAM,QAAQ,YAAY,qBAAqB,SAAS;AACxD,UAAQ,IAAI;GACV;GACA;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,gCAAgC,MAAM;GAC/C,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAIF,MAAM,SAAS,UAAU,IAAI;CAE7B,MAAM,UAAU,aAAa,UAAU,cADnB,QAAQ,6BAA6B,OAAO,EACE,SAAS,iBAAiB;AAE5F,KAAI,QACF,SAAQ,2BAA2B,SAAS,UAAU,OAAO;AAG/D,KACE,WACE,KACA,SACA,SAAS,OACT,IAAI,SACJ,SACA;EAAE;EAAQ;EAAM,SAAS,eAAe,IAAI,QAAQ;EAAE,MAAM;EAAc,EAC1E,UAAU,YAAY,SACtB,SAAS,UACT,SAAS,OACV,CAED;AAGF,KAAI,CAAC,SAAS;AAEZ,MADwB,kBAAkB,SAAS,QAAQ,IAAI,QAAQ,EAClD;AACnB,WAAQ,IAAI;IACV;IACA;IACA,SAAS,EAAE;IACX,MAAM;IACN,UAAU;KACR,QAAQ;KACR,SAAS;KACT,GAAG,oBAAoB,SAAS,QAAQ,IAAI,QAAQ;KACrD;IACF,CAAC;AACF,sBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;IACL,SAAS;IACT,MAAM;IACN,MAAM;IACP,EACF,CAAC,CACH;AACD;;AAEF,MAAI,SAAS,QAAQ;GACnB,MAAM,UAAU,MAAM,eACpB,KACA,KACA,cACA,cACA,IAAI,OAAO,wBACX,UACA,UACA,KACD;AACD,OAAI,YAAY,kBAAmB;AACnC,OAAI,YAAY,kBAAkB;AAChC,YAAQ,IAAI;KACV;KACA;KACA,SAAS,eAAe,IAAI,QAAQ;KACpC,MAAM;KACN,UAAU;MAAE,QAAQ,IAAI,cAAc;MAAK,SAAS;MAAM,QAAQ;MAAS;KAC5E,CAAC;AACF;;;AAIJ,UAAQ,IAAI;GACV;GACA;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,MAAM;GACP,EACF,CAAC,CACH;AACD;;CAGF,MAAM,WAAW,MAAM,gBAAgB,SAAS,aAAa;AAG7D,KAAI,gBAAgB,SAAS,EAAE;EAC7B,MAAM,SAAS,SAAS,UAAU;AAClC,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE;IAAQ;IAAS;GAC9B,CAAC;AACF,qBAAmB,KAAK,QAAQ,uBAAuB,SAAS,CAAC;AACjE;;AAIF,KAAI,YAAY,QAAQ;AACtB,MAAI,CAAC,eAAe,SAAS,EAAE;AAC7B,WAAQ,IAAI;IACV;IACA;IACA,SAAS,eAAe,IAAI,QAAQ;IACpC,MAAM;IACN,UAAU;KAAE,QAAQ;KAAK;KAAS;IACnC,CAAC;AACF,sBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;IACL,SAAS;IACT,MAAM;IACP,EACF,CAAC,CACH;AACD;;AAEF,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,MAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,MAAI,IAAI,SAAS,QAAQ;AACzB;;AAIF,KAAI,CAAC,gBAAgB,SAAS,EAAE;AAC9B,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,qBACE,KACA,KACA,KAAK,UAAU,EACb,OAAO;GAAE,SAAS;GAAyC,MAAM;GAAgB,EAClF,CAAC,CACH;AACD;;CAIF,IAAI;CACJ,IAAI;AAEJ,KAAI,OAAO,SAAS,UAAU,UAAU;AACtC,eAAa,OAAO,KAAK,SAAS,OAAO,SAAS;AAElD,gBAAc,uBADC,SAAS,UAAU,UACc;QAC3C;AACL,eAAa,OAAO,KAAK,SAAS,MAAM,SAAS,SAAS;AAC1D,gBAAc,SAAS,MAAM,eAAe;;AAI9C,KAAI,YAAY,WAAW,YAAY,SACrC,KAAI,UAAU,WAAW,eAAe,KAAK,KAAK,CAAC;AAIrD,KAAI,YAAY,UAAU;AACxB,UAAQ,IAAI;GACV;GACA;GACA,SAAS,eAAe,IAAI,QAAQ;GACpC,MAAM;GACN,UAAU;IAAE,QAAQ;IAAK;IAAS;GACnC,CAAC;AACF,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,qBAAqB;GACtB,CAAC;AACF,MAAI,IAAI,WAAW;AACnB;;AAIF,SAAQ,IAAI;EACV;EACA;EACA,SAAS,eAAe,IAAI,QAAQ;EACpC,MAAM;EACN,UAAU;GAAE,QAAQ;GAAK;GAAS;EACnC,CAAC;AACF,KAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;AACnD,KAAI,IAAI,WAAW"}
|
package/dist/embeddings.cjs
CHANGED
|
@@ -11,7 +11,8 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
11
11
|
let embeddingReq;
|
|
12
12
|
try {
|
|
13
13
|
embeddingReq = JSON.parse(raw);
|
|
14
|
-
} catch {
|
|
14
|
+
} catch (parseErr) {
|
|
15
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
15
16
|
journal.add({
|
|
16
17
|
method: req.method ?? "POST",
|
|
17
18
|
path: req.url ?? "/v1/embeddings",
|
|
@@ -23,7 +24,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
require_sse_writer.writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
26
|
-
message:
|
|
27
|
+
message: `Malformed JSON body: ${detail}`,
|
|
27
28
|
type: "invalid_request_error",
|
|
28
29
|
code: "invalid_json"
|
|
29
30
|
} }));
|
|
@@ -80,7 +81,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
80
81
|
fixture
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
|
-
require_sse_writer.writeErrorResponse(res, status,
|
|
84
|
+
require_sse_writer.writeErrorResponse(res, status, require_helpers.serializeErrorResponse(response));
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
if (require_helpers.isEmbeddingResponse(response)) {
|
|
@@ -115,23 +116,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
115
116
|
} }));
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
|
-
if (defaults.
|
|
119
|
-
if (await require_recorder.proxyAndRecord(req, res, syntheticReq, providerKey, req.url ?? "/v1/embeddings", fixtures, defaults, raw) !== "not_configured") {
|
|
120
|
-
journal.add({
|
|
121
|
-
method: req.method ?? "POST",
|
|
122
|
-
path: req.url ?? "/v1/embeddings",
|
|
123
|
-
headers: require_helpers.flattenHeaders(req.headers),
|
|
124
|
-
body: syntheticReq,
|
|
125
|
-
response: {
|
|
126
|
-
status: res.statusCode ?? 200,
|
|
127
|
-
fixture: null,
|
|
128
|
-
source: "proxy"
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (defaults.strict) {
|
|
119
|
+
if (require_helpers.resolveStrictMode(defaults.strict, req.headers)) {
|
|
135
120
|
logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/embeddings"}`);
|
|
136
121
|
journal.add({
|
|
137
122
|
method: req.method ?? "POST",
|
|
@@ -140,7 +125,8 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
140
125
|
body: syntheticReq,
|
|
141
126
|
response: {
|
|
142
127
|
status: 503,
|
|
143
|
-
fixture: null
|
|
128
|
+
fixture: null,
|
|
129
|
+
...require_helpers.strictOverrideField(defaults.strict, req.headers)
|
|
144
130
|
}
|
|
145
131
|
});
|
|
146
132
|
require_sse_writer.writeErrorResponse(res, 503, JSON.stringify({ error: {
|
|
@@ -150,6 +136,24 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
150
136
|
} }));
|
|
151
137
|
return;
|
|
152
138
|
}
|
|
139
|
+
if (defaults.record) {
|
|
140
|
+
const outcome = await require_recorder.proxyAndRecord(req, res, syntheticReq, providerKey, req.url ?? "/v1/embeddings", fixtures, defaults, raw);
|
|
141
|
+
if (outcome === "handled_by_hook") return;
|
|
142
|
+
if (outcome !== "not_configured") {
|
|
143
|
+
journal.add({
|
|
144
|
+
method: req.method ?? "POST",
|
|
145
|
+
path: req.url ?? "/v1/embeddings",
|
|
146
|
+
headers: require_helpers.flattenHeaders(req.headers),
|
|
147
|
+
body: syntheticReq,
|
|
148
|
+
response: {
|
|
149
|
+
status: res.statusCode ?? 200,
|
|
150
|
+
fixture: null,
|
|
151
|
+
source: "proxy"
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
153
157
|
logger.warn(`No embedding fixture matched for "${combinedInput.slice(0, 80)}" — returning deterministic fallback`);
|
|
154
158
|
const dimensions = embeddingReq.dimensions ?? 1536;
|
|
155
159
|
const embeddings = inputs.map((input) => require_helpers.generateDeterministicEmbedding(input, dimensions));
|
package/dist/embeddings.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","resolveResponse","isErrorResponse","isEmbeddingResponse","buildEmbeddingResponse","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\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 buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} 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\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n input: string | string[];\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: number;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\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 providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings\n const inputs: string[] = Array.isArray(embeddingReq.input)\n ? embeddingReq.input\n : [embeddingReq.input];\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\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 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 applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\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 // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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: req.url ?? \"/v1/embeddings\",\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 type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — try record-and-replay proxy if configured\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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 if (defaults.strict) {\n logger.error(\n `STRICT: No fixture matched for ${req.method ?? \"POST\"} ${req.url ?? \"/v1/embeddings\"}`,\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 503, fixture: null },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n const dimensions = embeddingReq.dimensions ?? 1536;\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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":"embeddings.cjs","names":["flattenHeaders","getTestId","matchFixture","applyChaos","resolveResponse","isErrorResponse","serializeErrorResponse","isEmbeddingResponse","buildEmbeddingResponse","resolveStrictMode","strictOverrideField","proxyAndRecord","generateDeterministicEmbedding"],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\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 serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} 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\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n input: string | string[];\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: number;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\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 providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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 type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings\n const inputs: string[] = Array.isArray(embeddingReq.input)\n ? embeddingReq.input\n : [embeddingReq.input];\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\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 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 applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response));\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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: req.url ?? \"/v1/embeddings\",\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 type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n logger.error(\n `STRICT: No fixture matched for ${req.method ?? \"POST\"} ${req.url ?? \"/v1/embeddings\"}`,\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\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 req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\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 embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n const dimensions = embeddingReq.dimensions ?? 1536;\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings.d.cts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"embeddings.d.cts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AAkDW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,CAAA,EAIR,OAJQ,CAAA,IAAA,CAAA"}
|
package/dist/embeddings.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings.d.ts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"embeddings.d.ts","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":[],"mappings":";;;;;;AAkDW,iBALW,gBAAA,CAKX,GAAA,EAJJ,MAAA,CAAK,eAID,EAAA,GAAA,EAHJ,MAAA,CAAK,cAGD,EAAA,GAAA,EAAA,MAAA,EAAA,QAAA,EADC,OACD,EAAA,EAAA,OAAA,EAAA,OAAA,EAAA,QAAA,EACC,eADD,EAAA,cAAA,EAAA,CAAA,GAAA,EAEa,MAAA,CAAK,cAFlB,EAAA,GAAA,IAAA,EAAA,WAAA,CAAA,EAGI,iBAHJ,CAAA,EAIR,OAJQ,CAAA,IAAA,CAAA"}
|
package/dist/embeddings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildEmbeddingResponse, flattenHeaders, generateDeterministicEmbedding, getTestId, isEmbeddingResponse, isErrorResponse, resolveResponse } from "./helpers.js";
|
|
1
|
+
import { buildEmbeddingResponse, flattenHeaders, generateDeterministicEmbedding, getTestId, isEmbeddingResponse, isErrorResponse, resolveResponse, resolveStrictMode, serializeErrorResponse, strictOverrideField } from "./helpers.js";
|
|
2
2
|
import { matchFixture } from "./router.js";
|
|
3
3
|
import { writeErrorResponse } from "./sse-writer.js";
|
|
4
4
|
import { applyChaos } from "./chaos.js";
|
|
@@ -11,7 +11,8 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
11
11
|
let embeddingReq;
|
|
12
12
|
try {
|
|
13
13
|
embeddingReq = JSON.parse(raw);
|
|
14
|
-
} catch {
|
|
14
|
+
} catch (parseErr) {
|
|
15
|
+
const detail = parseErr instanceof Error ? parseErr.message : "unknown";
|
|
15
16
|
journal.add({
|
|
16
17
|
method: req.method ?? "POST",
|
|
17
18
|
path: req.url ?? "/v1/embeddings",
|
|
@@ -23,7 +24,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
writeErrorResponse(res, 400, JSON.stringify({ error: {
|
|
26
|
-
message:
|
|
27
|
+
message: `Malformed JSON body: ${detail}`,
|
|
27
28
|
type: "invalid_request_error",
|
|
28
29
|
code: "invalid_json"
|
|
29
30
|
} }));
|
|
@@ -80,7 +81,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
80
81
|
fixture
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
|
-
writeErrorResponse(res, status,
|
|
84
|
+
writeErrorResponse(res, status, serializeErrorResponse(response));
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
86
87
|
if (isEmbeddingResponse(response)) {
|
|
@@ -115,23 +116,7 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
115
116
|
} }));
|
|
116
117
|
return;
|
|
117
118
|
}
|
|
118
|
-
if (defaults.
|
|
119
|
-
if (await proxyAndRecord(req, res, syntheticReq, providerKey, req.url ?? "/v1/embeddings", fixtures, defaults, raw) !== "not_configured") {
|
|
120
|
-
journal.add({
|
|
121
|
-
method: req.method ?? "POST",
|
|
122
|
-
path: req.url ?? "/v1/embeddings",
|
|
123
|
-
headers: flattenHeaders(req.headers),
|
|
124
|
-
body: syntheticReq,
|
|
125
|
-
response: {
|
|
126
|
-
status: res.statusCode ?? 200,
|
|
127
|
-
fixture: null,
|
|
128
|
-
source: "proxy"
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (defaults.strict) {
|
|
119
|
+
if (resolveStrictMode(defaults.strict, req.headers)) {
|
|
135
120
|
logger.error(`STRICT: No fixture matched for ${req.method ?? "POST"} ${req.url ?? "/v1/embeddings"}`);
|
|
136
121
|
journal.add({
|
|
137
122
|
method: req.method ?? "POST",
|
|
@@ -140,7 +125,8 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
140
125
|
body: syntheticReq,
|
|
141
126
|
response: {
|
|
142
127
|
status: 503,
|
|
143
|
-
fixture: null
|
|
128
|
+
fixture: null,
|
|
129
|
+
...strictOverrideField(defaults.strict, req.headers)
|
|
144
130
|
}
|
|
145
131
|
});
|
|
146
132
|
writeErrorResponse(res, 503, JSON.stringify({ error: {
|
|
@@ -150,6 +136,24 @@ async function handleEmbeddings(req, res, raw, fixtures, journal, defaults, setC
|
|
|
150
136
|
} }));
|
|
151
137
|
return;
|
|
152
138
|
}
|
|
139
|
+
if (defaults.record) {
|
|
140
|
+
const outcome = await proxyAndRecord(req, res, syntheticReq, providerKey, req.url ?? "/v1/embeddings", fixtures, defaults, raw);
|
|
141
|
+
if (outcome === "handled_by_hook") return;
|
|
142
|
+
if (outcome !== "not_configured") {
|
|
143
|
+
journal.add({
|
|
144
|
+
method: req.method ?? "POST",
|
|
145
|
+
path: req.url ?? "/v1/embeddings",
|
|
146
|
+
headers: flattenHeaders(req.headers),
|
|
147
|
+
body: syntheticReq,
|
|
148
|
+
response: {
|
|
149
|
+
status: res.statusCode ?? 200,
|
|
150
|
+
fixture: null,
|
|
151
|
+
source: "proxy"
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
153
157
|
logger.warn(`No embedding fixture matched for "${combinedInput.slice(0, 80)}" — returning deterministic fallback`);
|
|
154
158
|
const dimensions = embeddingReq.dimensions ?? 1536;
|
|
155
159
|
const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));
|
package/dist/embeddings.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embeddings.js","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\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 buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n} 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\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n input: string | string[];\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: number;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\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 providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings\n const inputs: string[] = Array.isArray(embeddingReq.input)\n ? embeddingReq.input\n : [embeddingReq.input];\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\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 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 applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\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 // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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: req.url ?? \"/v1/embeddings\",\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 type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — try record-and-replay proxy if configured\n if (defaults.record) {\n const outcome = await proxyAndRecord(\n req,\n res,\n syntheticReq,\n providerKey,\n req.url ?? \"/v1/embeddings\",\n fixtures,\n defaults,\n raw,\n );\n if (outcome !== \"not_configured\") {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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 if (defaults.strict) {\n logger.error(\n `STRICT: No fixture matched for ${req.method ?? \"POST\"} ${req.url ?? \"/v1/embeddings\"}`,\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 503, fixture: null },\n });\n writeErrorResponse(\n res,\n 503,\n JSON.stringify({\n error: {\n message: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — generate deterministic embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n const dimensions = embeddingReq.dimensions ?? 1536;\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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":"embeddings.js","names":[],"sources":["../src/embeddings.ts"],"sourcesContent":["/**\n * OpenAI Embeddings API support for aimock.\n *\n * Handles POST /v1/embeddings requests. Matches fixtures using the `inputText`\n * field, and falls back to generating a deterministic embedding from the input\n * text hash when no fixture matches.\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 serializeErrorResponse,\n generateDeterministicEmbedding,\n buildEmbeddingResponse,\n flattenHeaders,\n getTestId,\n resolveResponse,\n resolveStrictMode,\n strictOverrideField,\n} 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\n// ─── Embeddings API request types ──────────────────────────────────────────\n\ninterface EmbeddingRequest {\n input: string | string[];\n model: string;\n encoding_format?: \"float\" | \"base64\";\n dimensions?: number;\n [key: string]: unknown;\n}\n\n// ─── Request handler ───────────────────────────────────────────────────────\n\nexport async function handleEmbeddings(\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 providerKey: RecordProviderKey = \"openai\",\n): Promise<void> {\n const { logger } = defaults;\n setCorsHeaders(res);\n\n let embeddingReq: EmbeddingRequest;\n try {\n embeddingReq = JSON.parse(raw) as EmbeddingRequest;\n } catch (parseErr) {\n const detail = parseErr instanceof Error ? parseErr.message : \"unknown\";\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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 type: \"invalid_request_error\",\n code: \"invalid_json\",\n },\n }),\n );\n return;\n }\n\n // Validate required input parameter\n if (embeddingReq.input === undefined || embeddingReq.input === null) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Missing required parameter: 'input'\",\n type: \"invalid_request_error\",\n },\n }),\n );\n return;\n }\n\n // Normalize input to array of strings\n const inputs: string[] = Array.isArray(embeddingReq.input)\n ? embeddingReq.input\n : [embeddingReq.input];\n\n // Concatenate all inputs for matching purposes\n const combinedInput = inputs.join(\" \");\n\n // Build a synthetic ChatCompletionRequest for the fixture router.\n // We attach `embeddingInput` so the router's inputText matching can use it.\n const syntheticReq: ChatCompletionRequest = {\n model: embeddingReq.model,\n messages: [],\n embeddingInput: combinedInput,\n _endpointType: \"embedding\",\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 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 applyChaos(\n res,\n fixture,\n defaults.chaos,\n req.headers,\n journal,\n {\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status, fixture },\n });\n writeErrorResponse(res, status, serializeErrorResponse(response));\n return;\n }\n\n // Embedding response — use the fixture's embedding for each input\n if (isEmbeddingResponse(response)) {\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture },\n });\n const embeddings = inputs.map(() => [...response.embedding]);\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\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: req.url ?? \"/v1/embeddings\",\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 type: \"server_error\",\n },\n }),\n );\n return;\n }\n\n // No fixture match — check strict mode first, then try proxy\n if (resolveStrictMode(defaults.strict, req.headers)) {\n logger.error(\n `STRICT: No fixture matched for ${req.method ?? \"POST\"} ${req.url ?? \"/v1/embeddings\"}`,\n );\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\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: \"Strict mode: no fixture matched\",\n type: \"invalid_request_error\",\n code: \"no_fixture_match\",\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 req.url ?? \"/v1/embeddings\",\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: req.url ?? \"/v1/embeddings\",\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 embeddings from input text\n logger.warn(\n `No embedding fixture matched for \"${combinedInput.slice(0, 80)}\" — returning deterministic fallback`,\n );\n const dimensions = embeddingReq.dimensions ?? 1536;\n const embeddings = inputs.map((input) => generateDeterministicEmbedding(input, dimensions));\n\n journal.add({\n method: req.method ?? \"POST\",\n path: req.url ?? \"/v1/embeddings\",\n headers: flattenHeaders(req.headers),\n body: syntheticReq,\n response: { status: 200, fixture: null },\n });\n\n const body = buildEmbeddingResponse(embeddings, embeddingReq.model);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(body));\n}\n"],"mappings":"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"}
|