@copilotkit/aimock 1.37.4 → 1.38.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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +27 -9
  5. package/dist/agui-recorder.cjs +6 -0
  6. package/dist/agui-recorder.cjs.map +1 -1
  7. package/dist/agui-recorder.d.cts.map +1 -1
  8. package/dist/agui-recorder.d.ts.map +1 -1
  9. package/dist/agui-recorder.js +6 -0
  10. package/dist/agui-recorder.js.map +1 -1
  11. package/dist/agui-types.d.ts.map +1 -1
  12. package/dist/api-key-auth.cjs +166 -0
  13. package/dist/api-key-auth.cjs.map +1 -0
  14. package/dist/api-key-auth.d.cts +17 -0
  15. package/dist/api-key-auth.d.cts.map +1 -0
  16. package/dist/api-key-auth.d.ts +17 -0
  17. package/dist/api-key-auth.d.ts.map +1 -0
  18. package/dist/api-key-auth.js +158 -0
  19. package/dist/api-key-auth.js.map +1 -0
  20. package/dist/cli.cjs +4 -1
  21. package/dist/cli.cjs.map +1 -1
  22. package/dist/cli.js +4 -1
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config-loader.cjs +4 -2
  25. package/dist/config-loader.cjs.map +1 -1
  26. package/dist/config-loader.d.cts +2 -1
  27. package/dist/config-loader.d.cts.map +1 -1
  28. package/dist/config-loader.d.ts +2 -1
  29. package/dist/config-loader.d.ts.map +1 -1
  30. package/dist/config-loader.js +5 -3
  31. package/dist/config-loader.js.map +1 -1
  32. package/dist/fal-audio.cjs +2 -0
  33. package/dist/fal-audio.cjs.map +1 -1
  34. package/dist/fal-audio.d.cts.map +1 -1
  35. package/dist/fal-audio.d.ts.map +1 -1
  36. package/dist/fal-audio.js +2 -0
  37. package/dist/fal-audio.js.map +1 -1
  38. package/dist/fal.cjs +4 -1
  39. package/dist/fal.cjs.map +1 -1
  40. package/dist/fal.d.cts.map +1 -1
  41. package/dist/fal.d.ts.map +1 -1
  42. package/dist/fal.js +4 -1
  43. package/dist/fal.js.map +1 -1
  44. package/dist/fixture-loader.cjs +0 -0
  45. package/dist/fixture-loader.cjs.map +1 -1
  46. package/dist/fixture-loader.d.cts +3 -1
  47. package/dist/fixture-loader.d.cts.map +1 -1
  48. package/dist/fixture-loader.d.ts +3 -1
  49. package/dist/fixture-loader.d.ts.map +1 -1
  50. package/dist/fixture-loader.js +0 -0
  51. package/dist/fixture-loader.js.map +1 -1
  52. package/dist/grok-video.cjs +9 -2
  53. package/dist/grok-video.cjs.map +1 -1
  54. package/dist/grok-video.js +10 -3
  55. package/dist/grok-video.js.map +1 -1
  56. package/dist/helpers.cjs +16 -22
  57. package/dist/helpers.cjs.map +1 -1
  58. package/dist/helpers.d.cts.map +1 -1
  59. package/dist/helpers.d.ts.map +1 -1
  60. package/dist/helpers.js +16 -21
  61. package/dist/helpers.js.map +1 -1
  62. package/dist/index.d.cts +2 -2
  63. package/dist/index.d.ts +2 -2
  64. package/dist/llmock.cjs +14 -3
  65. package/dist/llmock.cjs.map +1 -1
  66. package/dist/llmock.d.cts +4 -2
  67. package/dist/llmock.d.cts.map +1 -1
  68. package/dist/llmock.d.ts +4 -2
  69. package/dist/llmock.d.ts.map +1 -1
  70. package/dist/llmock.js +15 -5
  71. package/dist/llmock.js.map +1 -1
  72. package/dist/openrouter-chat.cjs +333 -0
  73. package/dist/openrouter-chat.cjs.map +1 -0
  74. package/dist/openrouter-chat.js +325 -0
  75. package/dist/openrouter-chat.js.map +1 -0
  76. package/dist/openrouter-video.cjs +24 -7
  77. package/dist/openrouter-video.cjs.map +1 -1
  78. package/dist/openrouter-video.d.cts.map +1 -1
  79. package/dist/openrouter-video.d.ts.map +1 -1
  80. package/dist/openrouter-video.js +25 -8
  81. package/dist/openrouter-video.js.map +1 -1
  82. package/dist/provider-auth.cjs +20 -0
  83. package/dist/provider-auth.cjs.map +1 -1
  84. package/dist/provider-auth.js +20 -1
  85. package/dist/provider-auth.js.map +1 -1
  86. package/dist/recorder.cjs +50 -8
  87. package/dist/recorder.cjs.map +1 -1
  88. package/dist/recorder.d.cts.map +1 -1
  89. package/dist/recorder.d.ts.map +1 -1
  90. package/dist/recorder.js +50 -10
  91. package/dist/recorder.js.map +1 -1
  92. package/dist/responses.cjs +200 -89
  93. package/dist/responses.cjs.map +1 -1
  94. package/dist/responses.d.cts +18 -3
  95. package/dist/responses.d.cts.map +1 -1
  96. package/dist/responses.d.ts +18 -3
  97. package/dist/responses.d.ts.map +1 -1
  98. package/dist/responses.js +199 -90
  99. package/dist/responses.js.map +1 -1
  100. package/dist/server.cjs +156 -66
  101. package/dist/server.cjs.map +1 -1
  102. package/dist/server.d.cts +1 -1
  103. package/dist/server.d.cts.map +1 -1
  104. package/dist/server.d.ts +1 -1
  105. package/dist/server.d.ts.map +1 -1
  106. package/dist/server.js +157 -68
  107. package/dist/server.js.map +1 -1
  108. package/dist/sse-writer.cjs +3 -0
  109. package/dist/sse-writer.cjs.map +1 -1
  110. package/dist/sse-writer.d.cts +8 -0
  111. package/dist/sse-writer.d.cts.map +1 -1
  112. package/dist/sse-writer.d.ts +8 -0
  113. package/dist/sse-writer.d.ts.map +1 -1
  114. package/dist/sse-writer.js +3 -0
  115. package/dist/sse-writer.js.map +1 -1
  116. package/dist/stream-collapse.cjs +26 -0
  117. package/dist/stream-collapse.cjs.map +1 -1
  118. package/dist/stream-collapse.d.cts +7 -0
  119. package/dist/stream-collapse.d.cts.map +1 -1
  120. package/dist/stream-collapse.d.ts +7 -0
  121. package/dist/stream-collapse.d.ts.map +1 -1
  122. package/dist/stream-collapse.js +26 -0
  123. package/dist/stream-collapse.js.map +1 -1
  124. package/dist/transcription.cjs +49 -2
  125. package/dist/transcription.cjs.map +1 -1
  126. package/dist/transcription.d.cts.map +1 -1
  127. package/dist/transcription.d.ts.map +1 -1
  128. package/dist/transcription.js +50 -3
  129. package/dist/transcription.js.map +1 -1
  130. package/dist/types.d.cts +187 -5
  131. package/dist/types.d.cts.map +1 -1
  132. package/dist/types.d.ts +187 -5
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/veo-video.cjs +9 -2
  135. package/dist/veo-video.cjs.map +1 -1
  136. package/dist/veo-video.js +10 -3
  137. package/dist/veo-video.js.map +1 -1
  138. package/dist/ws-realtime.cjs +306 -57
  139. package/dist/ws-realtime.cjs.map +1 -1
  140. package/dist/ws-realtime.d.cts +1 -0
  141. package/dist/ws-realtime.d.cts.map +1 -1
  142. package/dist/ws-realtime.d.ts +1 -0
  143. package/dist/ws-realtime.d.ts.map +1 -1
  144. package/dist/ws-realtime.js +307 -58
  145. package/dist/ws-realtime.js.map +1 -1
  146. package/dist/ws-responses.cjs +11 -6
  147. package/dist/ws-responses.cjs.map +1 -1
  148. package/dist/ws-responses.d.cts.map +1 -1
  149. package/dist/ws-responses.d.ts.map +1 -1
  150. package/dist/ws-responses.js +12 -7
  151. package/dist/ws-responses.js.map +1 -1
  152. package/package.json +1 -1
  153. package/skills/write-fixtures/SKILL.md +53 -19
@@ -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
- session.voice = audio.voice;
149
- session.input_audio_format = audio.input_audio_format;
150
- session.output_audio_format = audio.output_audio_format;
151
- session.input_audio_transcription = audio.input_audio_transcription;
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
- if (msgType === "session.update") {
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
- if (session.type === "transcription" && !transcriptionModels.has(session.model)) {
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 ${s.model ?? prevSession.model} does not support session type transcription`,
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 = {