@copilotkit/aimock 1.37.4 → 1.39.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 +48 -0
- package/README.md +50 -11
- package/dist/agui-handler.cjs +20 -18
- package/dist/agui-handler.cjs.map +1 -1
- package/dist/agui-handler.d.cts +8 -1
- package/dist/agui-handler.d.cts.map +1 -1
- package/dist/agui-handler.d.ts +8 -1
- package/dist/agui-handler.d.ts.map +1 -1
- package/dist/agui-handler.js +20 -18
- package/dist/agui-handler.js.map +1 -1
- package/dist/agui-recorder.cjs +6 -0
- package/dist/agui-recorder.cjs.map +1 -1
- package/dist/agui-recorder.d.cts.map +1 -1
- package/dist/agui-recorder.d.ts.map +1 -1
- package/dist/agui-recorder.js +6 -0
- package/dist/agui-recorder.js.map +1 -1
- package/dist/agui-stub.d.cts +2 -2
- package/dist/agui-stub.d.ts +2 -2
- package/dist/agui-types.d.cts +19 -1
- package/dist/agui-types.d.cts.map +1 -1
- package/dist/agui-types.d.ts +19 -1
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +1 -1
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.js +1 -1
- package/dist/aimock-cli.js.map +1 -1
- package/dist/api-key-auth.cjs +166 -0
- package/dist/api-key-auth.cjs.map +1 -0
- package/dist/api-key-auth.d.cts +17 -0
- package/dist/api-key-auth.d.cts.map +1 -0
- package/dist/api-key-auth.d.ts +17 -0
- package/dist/api-key-auth.d.ts.map +1 -0
- package/dist/api-key-auth.js +158 -0
- package/dist/api-key-auth.js.map +1 -0
- package/dist/cli.cjs +14 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +14 -11
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.cjs +4 -2
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +2 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +2 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +5 -3
- package/dist/config-loader.js.map +1 -1
- package/dist/fal-audio.cjs +2 -0
- 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 +2 -0
- package/dist/fal-audio.js.map +1 -1
- package/dist/fal.cjs +4 -1
- 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 +4 -1
- package/dist/fal.js.map +1 -1
- package/dist/fixture-loader.cjs +0 -0
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +3 -1
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +3 -1
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +0 -0
- package/dist/fixture-loader.js.map +1 -1
- package/dist/grok-video.cjs +9 -2
- package/dist/grok-video.cjs.map +1 -1
- package/dist/grok-video.js +10 -3
- package/dist/grok-video.js.map +1 -1
- package/dist/helpers.cjs +16 -22
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +16 -21
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/llmock.cjs +37 -12
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +26 -2
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +26 -2
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +39 -15
- package/dist/llmock.js.map +1 -1
- package/dist/openrouter-chat.cjs +333 -0
- package/dist/openrouter-chat.cjs.map +1 -0
- package/dist/openrouter-chat.js +325 -0
- package/dist/openrouter-chat.js.map +1 -0
- package/dist/openrouter-video.cjs +24 -7
- package/dist/openrouter-video.cjs.map +1 -1
- package/dist/openrouter-video.d.cts.map +1 -1
- package/dist/openrouter-video.d.ts.map +1 -1
- package/dist/openrouter-video.js +25 -8
- package/dist/openrouter-video.js.map +1 -1
- package/dist/provider-auth.cjs +20 -0
- package/dist/provider-auth.cjs.map +1 -1
- package/dist/provider-auth.js +20 -1
- package/dist/provider-auth.js.map +1 -1
- package/dist/recorder.cjs +50 -8
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +50 -10
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +200 -89
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +18 -3
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +18 -3
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +199 -90
- package/dist/responses.js.map +1 -1
- package/dist/server.cjs +190 -83
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +7 -2
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts +7 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +190 -85
- package/dist/server.js.map +1 -1
- package/dist/sse-writer.cjs +3 -0
- package/dist/sse-writer.cjs.map +1 -1
- package/dist/sse-writer.d.cts +8 -0
- package/dist/sse-writer.d.cts.map +1 -1
- package/dist/sse-writer.d.ts +8 -0
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/sse-writer.js +3 -0
- package/dist/sse-writer.js.map +1 -1
- package/dist/stream-collapse.cjs +103 -0
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +7 -0
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +7 -0
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +103 -0
- package/dist/stream-collapse.js.map +1 -1
- package/dist/transcription.cjs +49 -2
- 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 +50 -3
- package/dist/transcription.js.map +1 -1
- package/dist/types.d.cts +187 -5
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +187 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/veo-video.cjs +9 -2
- package/dist/veo-video.cjs.map +1 -1
- package/dist/veo-video.js +10 -3
- package/dist/veo-video.js.map +1 -1
- package/dist/ws-gemini-live.cjs +41 -6
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.js +41 -6
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +306 -57
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +1 -0
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +1 -0
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +307 -58
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +11 -6
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +12 -7
- package/dist/ws-responses.js.map +1 -1
- package/package.json +3 -2
- package/skills/write-fixtures/SKILL.md +53 -19
package/dist/ws-realtime.cjs
CHANGED
|
@@ -19,6 +19,123 @@ let node_crypto = require("node:crypto");
|
|
|
19
19
|
function realtimeId(prefix) {
|
|
20
20
|
return `${prefix}_${(0, node_crypto.randomBytes)(12).toString("base64url")}`;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Transcription `usage` is MODEL-DEPENDENT on the live API, so the admitted
|
|
24
|
+
* models are split by the usage shape they actually report rather than kept in
|
|
25
|
+
* one flat list. Verified against api.openai.com with a single 5s clip on BOTH
|
|
26
|
+
* the realtime WS (`conversation.item.input_audio_transcription.completed`) and
|
|
27
|
+
* HTTP (`POST /v1/audio/transcriptions`) surfaces:
|
|
28
|
+
*
|
|
29
|
+
* whisper-1 → {"type":"duration","seconds":5}
|
|
30
|
+
* gpt-transcribe → {"type":"duration","seconds":5}
|
|
31
|
+
* gpt-live-transcribe → {"type":"duration","seconds":5} (realtime only)
|
|
32
|
+
* gpt-4o-transcribe → {"type":"tokens","total_tokens":66,…}
|
|
33
|
+
* gpt-4o-mini-transcribe → {"type":"tokens","total_tokens":66,…}
|
|
34
|
+
*
|
|
35
|
+
* Keeping the split here — instead of a second list beside the admission
|
|
36
|
+
* predicate — means a newly admitted model has to declare its family, and the
|
|
37
|
+
* two can never disagree about which models exist.
|
|
38
|
+
*/
|
|
39
|
+
function isDurationUsageTranscriptionModel(model) {
|
|
40
|
+
return model === "whisper-1" || model === "gpt-transcribe" || model === "gpt-live-transcribe" || model.startsWith("gpt-live-transcribe-");
|
|
41
|
+
}
|
|
42
|
+
function isTokenUsageTranscriptionModel(model) {
|
|
43
|
+
return model === "gpt-4o-transcribe" || model.startsWith("gpt-4o-mini-transcribe");
|
|
44
|
+
}
|
|
45
|
+
function isLiveTranscriptionModel(model) {
|
|
46
|
+
return isDurationUsageTranscriptionModel(model) || isTokenUsageTranscriptionModel(model);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The synthesized `usage` for a transcription with no fixture-supplied one.
|
|
50
|
+
* aimock has no real audio to measure, so the magnitudes are zero — but the
|
|
51
|
+
* DISCRIMINATOR must match the model's live family, otherwise a consumer
|
|
52
|
+
* branching on `usage.type` reads `undefined` counts and fails silently.
|
|
53
|
+
*/
|
|
54
|
+
function defaultTranscriptionUsage(model) {
|
|
55
|
+
if (isTokenUsageTranscriptionModel(model)) return {
|
|
56
|
+
type: "tokens",
|
|
57
|
+
total_tokens: 0,
|
|
58
|
+
input_tokens: 0,
|
|
59
|
+
input_token_details: {
|
|
60
|
+
text_tokens: 0,
|
|
61
|
+
audio_tokens: 0
|
|
62
|
+
},
|
|
63
|
+
output_tokens: 0
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
type: "duration",
|
|
67
|
+
seconds: 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function normalizeAudioFormat(value) {
|
|
71
|
+
if (value === null) return null;
|
|
72
|
+
if (typeof value === "string") return { type: value };
|
|
73
|
+
if (value && typeof value === "object" && typeof value.type === "string") return { ...value };
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function transcriptionModel(session) {
|
|
77
|
+
return session.input_audio_transcription?.model ?? session.model;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A live-transcription session is discriminated by the session TYPE, never by
|
|
81
|
+
* the mere presence of `input_audio_transcription`.
|
|
82
|
+
*
|
|
83
|
+
* `input_audio_transcription` is the documented way to configure input
|
|
84
|
+
* transcription on an ordinary realtime CONVERSATION session, so treating it as
|
|
85
|
+
* the discriminator made the canonical realtime setup (`{model:"whisper-1"}`)
|
|
86
|
+
* route `input_audio_buffer.commit` into the transcription path. That silently
|
|
87
|
+
* corrupted the conversation: it appended a phantom `[audio]` turn and consumed
|
|
88
|
+
* a fixture match, so the next `response.create` replayed the FOLLOWING turn's
|
|
89
|
+
* content. The client saw a well-formed — but wrong — response.
|
|
90
|
+
*
|
|
91
|
+
* A transcription session is established explicitly, either by connecting with
|
|
92
|
+
* `?intent=transcription` or via `transcription_session.update` (both set
|
|
93
|
+
* `session.type`), which is what the OpenAI realtime API requires too.
|
|
94
|
+
*/
|
|
95
|
+
function isLiveTranscriptionSession(session) {
|
|
96
|
+
return isLiveTranscriptionModel(transcriptionModel(session)) && session.type === "transcription";
|
|
97
|
+
}
|
|
98
|
+
function serializeSession(session, sessionId) {
|
|
99
|
+
if (session.type === "transcription") return {
|
|
100
|
+
...sessionId ? { id: sessionId } : {},
|
|
101
|
+
object: "realtime.transcription_session",
|
|
102
|
+
type: "transcription",
|
|
103
|
+
expires_at: Math.floor(Date.now() / 1e3) + 3600,
|
|
104
|
+
audio: { input: {
|
|
105
|
+
format: session.input_audio_format,
|
|
106
|
+
noise_reduction: session.input_audio_noise_reduction,
|
|
107
|
+
transcription: session.input_audio_transcription,
|
|
108
|
+
turn_detection: session.turn_detection
|
|
109
|
+
} },
|
|
110
|
+
include: null
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
...sessionId ? { id: sessionId } : {},
|
|
114
|
+
object: "realtime.session",
|
|
115
|
+
model: session.model,
|
|
116
|
+
expires_at: Math.floor(Date.now() / 1e3) + 3600,
|
|
117
|
+
modalities: session.modalities,
|
|
118
|
+
instructions: session.instructions,
|
|
119
|
+
tools: session.tools,
|
|
120
|
+
tool_choice: "auto",
|
|
121
|
+
temperature: session.temperature,
|
|
122
|
+
max_response_output_tokens: "inf",
|
|
123
|
+
audio: {
|
|
124
|
+
input: {
|
|
125
|
+
format: session.input_audio_format,
|
|
126
|
+
noise_reduction: session.input_audio_noise_reduction,
|
|
127
|
+
transcription: session.input_audio_transcription,
|
|
128
|
+
turn_detection: session.turn_detection
|
|
129
|
+
},
|
|
130
|
+
output: {
|
|
131
|
+
format: session.output_audio_format,
|
|
132
|
+
voice: session.voice
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
type: session.type === "conversation" || session.type === "realtime" ? "realtime" : session.type,
|
|
136
|
+
reasoning: session.reasoning
|
|
137
|
+
};
|
|
138
|
+
}
|
|
22
139
|
function realtimeItemsToMessages(items, instructions, logger) {
|
|
23
140
|
const messages = [];
|
|
24
141
|
if (instructions) messages.push({
|
|
@@ -146,10 +263,14 @@ function translateGAToBeta(event) {
|
|
|
146
263
|
const session = { ...translated.session };
|
|
147
264
|
if (session.audio && typeof session.audio === "object") {
|
|
148
265
|
const audio = session.audio;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
session.
|
|
152
|
-
session.
|
|
266
|
+
const input = audio.input;
|
|
267
|
+
const output = audio.output;
|
|
268
|
+
session.voice = output?.voice;
|
|
269
|
+
session.input_audio_format = (input?.format)?.type;
|
|
270
|
+
session.output_audio_format = (output?.format)?.type;
|
|
271
|
+
session.input_audio_transcription = input?.transcription;
|
|
272
|
+
session.input_audio_noise_reduction = input?.noise_reduction;
|
|
273
|
+
session.turn_detection = input?.turn_detection ?? null;
|
|
153
274
|
delete session.audio;
|
|
154
275
|
}
|
|
155
276
|
delete session.type;
|
|
@@ -196,34 +317,13 @@ function handleWebSocketRealtime(ws, fixtures, journal, defaults) {
|
|
|
196
317
|
input_audio_transcription: null,
|
|
197
318
|
turn_detection: null,
|
|
198
319
|
temperature: .8,
|
|
199
|
-
type: "conversation",
|
|
320
|
+
type: defaults.transcriptionIntent ? "transcription" : "conversation",
|
|
200
321
|
reasoning: null
|
|
201
322
|
};
|
|
202
323
|
const conversationItems = [];
|
|
203
324
|
sendEvent(ws, {
|
|
204
325
|
type: "session.created",
|
|
205
|
-
session:
|
|
206
|
-
id: sessionId,
|
|
207
|
-
object: "realtime.session",
|
|
208
|
-
model: session.model,
|
|
209
|
-
expires_at: Math.floor(Date.now() / 1e3) + 3600,
|
|
210
|
-
modalities: session.modalities,
|
|
211
|
-
instructions: session.instructions,
|
|
212
|
-
tools: session.tools,
|
|
213
|
-
tool_choice: "auto",
|
|
214
|
-
temperature: session.temperature,
|
|
215
|
-
max_response_output_tokens: "inf",
|
|
216
|
-
audio: {
|
|
217
|
-
voice: session.voice,
|
|
218
|
-
input_audio_format: session.input_audio_format,
|
|
219
|
-
output_audio_format: session.output_audio_format,
|
|
220
|
-
input_audio_noise_reduction: session.input_audio_noise_reduction,
|
|
221
|
-
input_audio_transcription: session.input_audio_transcription
|
|
222
|
-
},
|
|
223
|
-
turn_detection: session.turn_detection,
|
|
224
|
-
type: session.type,
|
|
225
|
-
reasoning: session.reasoning
|
|
226
|
-
}
|
|
326
|
+
session: serializeSession(session, sessionId)
|
|
227
327
|
}, isBeta);
|
|
228
328
|
let pending = Promise.resolve();
|
|
229
329
|
ws.on("message", (raw) => {
|
|
@@ -247,11 +347,17 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
247
347
|
return;
|
|
248
348
|
}
|
|
249
349
|
const msgType = parsed.type;
|
|
250
|
-
|
|
350
|
+
const isTranscriptionSessionUpdate = msgType === "transcription_session.update";
|
|
351
|
+
if (msgType === "session.update" || isTranscriptionSessionUpdate) {
|
|
352
|
+
if (isTranscriptionSessionUpdate && !defaults.transcriptionIntent) {
|
|
353
|
+
buildErrorRealtimeEvent(ws, "transcription_session.update requires a realtime connection with intent=transcription", isBeta, "invalid_request_error", "invalid_session_config");
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
251
356
|
if (parsed.session) {
|
|
252
357
|
const s = parsed.session;
|
|
253
358
|
const validTypes = new Set([
|
|
254
359
|
"conversation",
|
|
360
|
+
"realtime",
|
|
255
361
|
"transcription",
|
|
256
362
|
"translation"
|
|
257
363
|
]);
|
|
@@ -269,23 +375,35 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
269
375
|
}
|
|
270
376
|
}
|
|
271
377
|
const prevSession = { ...session };
|
|
378
|
+
if (isTranscriptionSessionUpdate) session.type = "transcription";
|
|
272
379
|
if (s.instructions !== void 0) session.instructions = s.instructions;
|
|
273
380
|
if (s.tools !== void 0) session.tools = s.tools;
|
|
274
381
|
if (s.modalities !== void 0) session.modalities = s.modalities;
|
|
275
|
-
if (s.model !== void 0) session.model = s.model;
|
|
276
382
|
if (s.temperature !== void 0) session.temperature = s.temperature;
|
|
277
383
|
if (s.type !== void 0) session.type = s.type;
|
|
278
384
|
if (s.audio) {
|
|
279
385
|
const audio = s.audio;
|
|
280
386
|
if (audio.voice !== void 0) session.voice = audio.voice;
|
|
281
|
-
if (audio.input_audio_format !== void 0) session.input_audio_format = audio.input_audio_format;
|
|
282
|
-
if (audio.output_audio_format !== void 0) session.output_audio_format = audio.output_audio_format;
|
|
387
|
+
if (audio.input_audio_format !== void 0) session.input_audio_format = normalizeAudioFormat(audio.input_audio_format);
|
|
388
|
+
if (audio.output_audio_format !== void 0) session.output_audio_format = normalizeAudioFormat(audio.output_audio_format);
|
|
283
389
|
if (audio.input_audio_noise_reduction !== void 0) session.input_audio_noise_reduction = audio.input_audio_noise_reduction;
|
|
284
390
|
if (audio.input_audio_transcription !== void 0) session.input_audio_transcription = audio.input_audio_transcription;
|
|
391
|
+
if (audio.input && typeof audio.input === "object") {
|
|
392
|
+
const input = audio.input;
|
|
393
|
+
if (input.transcription !== void 0) session.input_audio_transcription = input.transcription;
|
|
394
|
+
if (input.noise_reduction !== void 0) session.input_audio_noise_reduction = input.noise_reduction;
|
|
395
|
+
if (input.turn_detection !== void 0) session.turn_detection = input.turn_detection;
|
|
396
|
+
if (input.format !== void 0) session.input_audio_format = normalizeAudioFormat(input.format);
|
|
397
|
+
}
|
|
398
|
+
if (audio.output && typeof audio.output === "object") {
|
|
399
|
+
const output = audio.output;
|
|
400
|
+
if (output.voice !== void 0) session.voice = output.voice;
|
|
401
|
+
if (output.format !== void 0) session.output_audio_format = normalizeAudioFormat(output.format);
|
|
402
|
+
}
|
|
285
403
|
}
|
|
286
404
|
if (s.voice !== void 0) session.voice = s.voice;
|
|
287
|
-
if (s.input_audio_format !== void 0) session.input_audio_format = s.input_audio_format;
|
|
288
|
-
if (s.output_audio_format !== void 0) session.output_audio_format = s.output_audio_format;
|
|
405
|
+
if (s.input_audio_format !== void 0) session.input_audio_format = normalizeAudioFormat(s.input_audio_format);
|
|
406
|
+
if (s.output_audio_format !== void 0) session.output_audio_format = normalizeAudioFormat(s.output_audio_format);
|
|
289
407
|
if (s.input_audio_noise_reduction !== void 0) session.input_audio_noise_reduction = s.input_audio_noise_reduction;
|
|
290
408
|
if (s.input_audio_transcription !== void 0) session.input_audio_transcription = s.input_audio_transcription;
|
|
291
409
|
if (s.turn_detection !== void 0) session.turn_detection = s.turn_detection;
|
|
@@ -301,12 +419,13 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
301
419
|
"gpt-4o-mini-transcribe",
|
|
302
420
|
"gpt-realtime-translate"
|
|
303
421
|
]);
|
|
304
|
-
|
|
422
|
+
const candidateTranscriptionModel = transcriptionModel(session);
|
|
423
|
+
if (session.type === "transcription" && !transcriptionModels.has(candidateTranscriptionModel) && !isLiveTranscriptionSession(session)) {
|
|
305
424
|
Object.assign(session, prevSession);
|
|
306
425
|
sendEvent(ws, {
|
|
307
426
|
type: "error",
|
|
308
427
|
error: {
|
|
309
|
-
message: `Model ${
|
|
428
|
+
message: `Model ${candidateTranscriptionModel} does not support session type transcription`,
|
|
310
429
|
type: "invalid_request_error",
|
|
311
430
|
code: "invalid_session_config"
|
|
312
431
|
}
|
|
@@ -327,28 +446,8 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
327
446
|
}
|
|
328
447
|
}
|
|
329
448
|
sendEvent(ws, {
|
|
330
|
-
type: "session.updated",
|
|
331
|
-
session:
|
|
332
|
-
object: "realtime.session",
|
|
333
|
-
model: session.model,
|
|
334
|
-
expires_at: Math.floor(Date.now() / 1e3) + 3600,
|
|
335
|
-
modalities: session.modalities,
|
|
336
|
-
instructions: session.instructions,
|
|
337
|
-
tools: session.tools,
|
|
338
|
-
tool_choice: "auto",
|
|
339
|
-
temperature: session.temperature,
|
|
340
|
-
max_response_output_tokens: "inf",
|
|
341
|
-
audio: {
|
|
342
|
-
voice: session.voice,
|
|
343
|
-
input_audio_format: session.input_audio_format,
|
|
344
|
-
output_audio_format: session.output_audio_format,
|
|
345
|
-
input_audio_noise_reduction: session.input_audio_noise_reduction,
|
|
346
|
-
input_audio_transcription: session.input_audio_transcription
|
|
347
|
-
},
|
|
348
|
-
turn_detection: session.turn_detection,
|
|
349
|
-
type: session.type,
|
|
350
|
-
reasoning: session.reasoning
|
|
351
|
-
}
|
|
449
|
+
type: isTranscriptionSessionUpdate ? "transcription_session.updated" : "session.updated",
|
|
450
|
+
session: serializeSession(session)
|
|
352
451
|
}, isBeta);
|
|
353
452
|
return;
|
|
354
453
|
}
|
|
@@ -390,6 +489,7 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
390
489
|
type: "conversation.item.added",
|
|
391
490
|
item: audioItem
|
|
392
491
|
}, isBeta);
|
|
492
|
+
if (isLiveTranscriptionSession(session)) await emitLiveTranscriptionEvents(ws, fixtures, journal, defaults, session, audioItem, isBeta);
|
|
393
493
|
}
|
|
394
494
|
return;
|
|
395
495
|
}
|
|
@@ -402,6 +502,155 @@ async function processMessage(raw, ws, fixtures, journal, defaults, session, con
|
|
|
402
502
|
return;
|
|
403
503
|
}
|
|
404
504
|
}
|
|
505
|
+
async function emitLiveTranscriptionEvents(ws, fixtures, journal, defaults, session, audioItem, isBeta) {
|
|
506
|
+
const realtimeContextHeader = defaults.upgradeHeaders?.["x-aimock-context"];
|
|
507
|
+
const realtimeContext = typeof realtimeContextHeader === "string" ? realtimeContextHeader : Array.isArray(realtimeContextHeader) && realtimeContextHeader.length > 0 ? realtimeContextHeader[0] : void 0;
|
|
508
|
+
const request = {
|
|
509
|
+
model: transcriptionModel(session),
|
|
510
|
+
messages: realtimeItemsToMessages([audioItem], void 0, defaults.logger),
|
|
511
|
+
_endpointType: "realtime-transcription",
|
|
512
|
+
_context: realtimeContext
|
|
513
|
+
};
|
|
514
|
+
const testId = defaults.testId ?? require_constants.DEFAULT_TEST_ID;
|
|
515
|
+
const { fixture, skippedBySequenceOrTurn } = require_router.matchFixtureDiagnostic(fixtures, request, journal.getFixtureMatchCountsForTest(testId), defaults.requestTransform);
|
|
516
|
+
const itemId = audioItem.id ?? realtimeId("item");
|
|
517
|
+
if (!fixture) {
|
|
518
|
+
if (require_helpers.resolveStrictMode(defaults.strict, defaults.upgradeHeaders)) {
|
|
519
|
+
const strictMessage = require_helpers.strictNoMatchMessage(skippedBySequenceOrTurn);
|
|
520
|
+
defaults.logger.error(require_helpers.strictNoMatchLogLine("WS", "/v1/realtime", skippedBySequenceOrTurn));
|
|
521
|
+
journal.add({
|
|
522
|
+
method: "WS",
|
|
523
|
+
path: "/v1/realtime",
|
|
524
|
+
headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
|
|
525
|
+
body: request,
|
|
526
|
+
response: {
|
|
527
|
+
status: 503,
|
|
528
|
+
fixture: null,
|
|
529
|
+
...require_helpers.strictOverrideField(defaults.strict, defaults.upgradeHeaders)
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
ws.close(1008, strictMessage);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
journal.add({
|
|
536
|
+
method: "WS",
|
|
537
|
+
path: "/v1/realtime",
|
|
538
|
+
headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
|
|
539
|
+
body: request,
|
|
540
|
+
response: {
|
|
541
|
+
status: 404,
|
|
542
|
+
fixture: null,
|
|
543
|
+
...require_helpers.strictOverrideField(defaults.strict, defaults.upgradeHeaders)
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
sendLiveTranscriptionFailure(ws, itemId, {
|
|
547
|
+
message: "No fixture matched",
|
|
548
|
+
type: "invalid_request_error",
|
|
549
|
+
code: "no_fixture_match"
|
|
550
|
+
}, isBeta);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const response = await require_helpers.resolveResponse(fixture, request);
|
|
554
|
+
if (require_helpers.isErrorResponse(response)) {
|
|
555
|
+
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
556
|
+
journal.add({
|
|
557
|
+
method: "WS",
|
|
558
|
+
path: "/v1/realtime",
|
|
559
|
+
headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
|
|
560
|
+
body: request,
|
|
561
|
+
response: {
|
|
562
|
+
status: response.status ?? 500,
|
|
563
|
+
fixture
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
sendLiveTranscriptionFailure(ws, itemId, {
|
|
567
|
+
message: response.error.message,
|
|
568
|
+
type: response.error.type ?? "server_error",
|
|
569
|
+
...response.error.code !== void 0 && { code: response.error.code }
|
|
570
|
+
}, isBeta);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!require_helpers.isTranscriptionResponse(response)) {
|
|
574
|
+
journal.add({
|
|
575
|
+
method: "WS",
|
|
576
|
+
path: "/v1/realtime",
|
|
577
|
+
headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
|
|
578
|
+
body: request,
|
|
579
|
+
response: {
|
|
580
|
+
status: 500,
|
|
581
|
+
fixture
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
sendLiveTranscriptionFailure(ws, itemId, {
|
|
585
|
+
message: "Fixture response is not a transcription type",
|
|
586
|
+
type: "server_error"
|
|
587
|
+
}, isBeta);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
journal.incrementFixtureMatchCount(fixture, fixtures, testId);
|
|
591
|
+
const transcript = response.transcription.text;
|
|
592
|
+
const usage = response.transcription.usage ?? defaultTranscriptionUsage(request.model);
|
|
593
|
+
const journalEntry = journal.add({
|
|
594
|
+
method: "WS",
|
|
595
|
+
path: "/v1/realtime",
|
|
596
|
+
headers: require_helpers.flattenHeaders(defaults.upgradeHeaders ?? {}),
|
|
597
|
+
body: request,
|
|
598
|
+
response: {
|
|
599
|
+
status: 200,
|
|
600
|
+
fixture
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
const latency = fixture.latency ?? defaults.latency;
|
|
604
|
+
const chunkSize = Math.max(1, fixture.chunkSize ?? defaults.chunkSize);
|
|
605
|
+
const replaySpeed = fixture.replaySpeed ?? defaults.replaySpeed;
|
|
606
|
+
const interruption = require_interruption.createInterruptionSignal(fixture);
|
|
607
|
+
let chunkIndex = 0;
|
|
608
|
+
for (let index = 0; index < transcript.length; index += chunkSize) {
|
|
609
|
+
const chunkDelay = require_sse_writer.calculateDelay(chunkIndex, fixture.streamingProfile, latency, fixture.recordedTimings, replaySpeed);
|
|
610
|
+
if (chunkDelay > 0) await require_sse_writer.delay(chunkDelay, interruption?.signal);
|
|
611
|
+
if (interruption?.signal.aborted || ws.isClosed) {
|
|
612
|
+
if (interruption?.signal.aborted) {
|
|
613
|
+
journalEntry.response.interrupted = true;
|
|
614
|
+
journalEntry.response.interruptReason = interruption.reason();
|
|
615
|
+
ws.destroy();
|
|
616
|
+
}
|
|
617
|
+
interruption?.cleanup();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
sendEvent(ws, {
|
|
621
|
+
type: "conversation.item.input_audio_transcription.delta",
|
|
622
|
+
item_id: itemId,
|
|
623
|
+
content_index: 0,
|
|
624
|
+
delta: transcript.slice(index, index + chunkSize)
|
|
625
|
+
}, isBeta);
|
|
626
|
+
interruption?.tick();
|
|
627
|
+
chunkIndex++;
|
|
628
|
+
}
|
|
629
|
+
if (interruption?.signal.aborted) {
|
|
630
|
+
journalEntry.response.interrupted = true;
|
|
631
|
+
journalEntry.response.interruptReason = interruption.reason();
|
|
632
|
+
ws.destroy();
|
|
633
|
+
interruption.cleanup();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
sendEvent(ws, {
|
|
637
|
+
type: "conversation.item.input_audio_transcription.completed",
|
|
638
|
+
item_id: itemId,
|
|
639
|
+
content_index: 0,
|
|
640
|
+
transcript,
|
|
641
|
+
usage,
|
|
642
|
+
...response.transcription.languages !== void 0 ? { languages: response.transcription.languages } : {}
|
|
643
|
+
}, isBeta);
|
|
644
|
+
interruption?.cleanup();
|
|
645
|
+
}
|
|
646
|
+
function sendLiveTranscriptionFailure(ws, itemId, error, isBeta) {
|
|
647
|
+
sendEvent(ws, {
|
|
648
|
+
type: "conversation.item.input_audio_transcription.failed",
|
|
649
|
+
item_id: itemId,
|
|
650
|
+
content_index: 0,
|
|
651
|
+
error
|
|
652
|
+
}, isBeta);
|
|
653
|
+
}
|
|
405
654
|
async function handleResponseCreate(ws, fixtures, journal, defaults, session, conversationItems, isBeta, responseOverrides) {
|
|
406
655
|
const messages = realtimeItemsToMessages(conversationItems, (responseOverrides?.instructions ?? session.instructions) || void 0, defaults.logger);
|
|
407
656
|
const endpointType = {
|