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