@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
@@ -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
- session.voice = audio.voice;
150
- session.input_audio_format = audio.input_audio_format;
151
- session.output_audio_format = audio.output_audio_format;
152
- session.input_audio_transcription = audio.input_audio_transcription;
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
- if (msgType === "session.update") {
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
- if (session.type === "transcription" && !transcriptionModels.has(session.model)) {
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 ${s.model ?? prevSession.model} does not support session type transcription`,
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 = {