@vapi-ai/web 3.0.1 → 3.1.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/dist/api.js CHANGED
@@ -1338,6 +1338,41 @@ class Api extends HttpClient {
1338
1338
  secure: true,
1339
1339
  ...params,
1340
1340
  }),
1341
+ /**
1342
+ * No description
1343
+ *
1344
+ * @tags Calls
1345
+ * @name GoalEvaluationCurrentControllerCurrentGet
1346
+ * @summary Get the current goal evaluation for a call
1347
+ * @request GET:/call/{id}/goal-evaluation
1348
+ * @secure
1349
+ */
1350
+ goalEvaluationCurrentControllerCurrentGet: (id, query, params = {}) => this.request({
1351
+ path: `/call/${id}/goal-evaluation`,
1352
+ method: "GET",
1353
+ query: query,
1354
+ secure: true,
1355
+ format: "json",
1356
+ ...params,
1357
+ }),
1358
+ /**
1359
+ * No description
1360
+ *
1361
+ * @tags Calls
1362
+ * @name GoalEvaluationCurrentControllerCurrentBatchGet
1363
+ * @summary Get current goal evaluations for up to 100 call/goal pairs
1364
+ * @request POST:/call/goal-evaluation/batch
1365
+ * @secure
1366
+ */
1367
+ goalEvaluationCurrentControllerCurrentBatchGet: (data, params = {}) => this.request({
1368
+ path: `/call/goal-evaluation/batch`,
1369
+ method: "POST",
1370
+ body: data,
1371
+ secure: true,
1372
+ type: ContentType.Json,
1373
+ format: "json",
1374
+ ...params,
1375
+ }),
1341
1376
  };
1342
1377
  chat = {
1343
1378
  /**
@@ -1670,7 +1705,7 @@ class Api extends HttpClient {
1670
1705
  };
1671
1706
  trafficAllocations = {
1672
1707
  /**
1673
- * @description Creates a new traffic allocation for an assistant, replacing the one currently in effect. To start or adjust a split, send targets naming published versions (such as "v7") with percentages totaling 100; allocationIntent is inferred as 'explicit'. To stop splitting and send every call to the newest published version, send allocationIntent 'follow-latest' with no targets field; stopping always names its intent, so a dropped targets field can never end a split by accident. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1708
+ * @description Creates a new traffic allocation for an assistant, replacing the one currently in effect. To start or adjust a split, send targets naming published versions (such as "v7") with percentages totaling 100; allocationIntent is inferred as 'explicit'. To stop splitting and send every call to the newest published version, send allocationIntent 'follow-latest' with no targets field; stopping always names its intent, so a dropped targets field can never end a split by accident.
1674
1709
  *
1675
1710
  * @tags Traffic Allocations
1676
1711
  * @name TrafficAllocationControllerCreate
@@ -1688,7 +1723,7 @@ class Api extends HttpClient {
1688
1723
  ...params,
1689
1724
  }),
1690
1725
  /**
1691
- * @description The append-only history of an assistant's allocations, newest first. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1726
+ * @description The append-only history of an assistant's allocations, newest first.
1692
1727
  *
1693
1728
  * @tags Traffic Allocations
1694
1729
  * @name TrafficAllocationControllerFindAllPaginated
@@ -1705,7 +1740,7 @@ class Api extends HttpClient {
1705
1740
  ...params,
1706
1741
  }),
1707
1742
  /**
1708
- * @description The allocation currently in effect for the assistant, with its targets. The response carries no allocation field when traffic splitting has never been configured. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1743
+ * @description The allocation currently in effect for the assistant, with its targets. The response carries no allocation field when traffic splitting has never been configured.
1709
1744
  *
1710
1745
  * @tags Traffic Allocations
1711
1746
  * @name TrafficAllocationControllerLatestGet
@@ -1722,7 +1757,7 @@ class Api extends HttpClient {
1722
1757
  ...params,
1723
1758
  }),
1724
1759
  /**
1725
- * @description Per-assistant-version call metrics over a time window: call counts, configured vs actual traffic share, duration, cost per minute, latency, ended reasons, a bucketed timeseries, and optional structured output aggregations. Calls with no assistant version are reported as an 'unversioned' row. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1760
+ * @description Per-assistant-version call metrics over a time window: call counts, configured vs actual traffic share, duration, cost per minute, latency, ended reasons, a bucketed timeseries, and optional structured output aggregations. Calls with no assistant version are reported as an 'unversioned' row.
1726
1761
  *
1727
1762
  * @tags Traffic Allocations
1728
1763
  * @name TrafficAllocationControllerMetricsGet
@@ -1739,7 +1774,7 @@ class Api extends HttpClient {
1739
1774
  ...params,
1740
1775
  }),
1741
1776
  /**
1742
- * @description Returns a single allocation by id, including its targets and actor attribution. Traffic splitting is in beta, rolling out to select organizations; requests from organizations without access receive a 403.
1777
+ * @description Returns a single allocation by id, including its targets and actor attribution.
1743
1778
  *
1744
1779
  * @tags Traffic Allocations
1745
1780
  * @name TrafficAllocationControllerFindOne
@@ -3132,6 +3167,60 @@ class Api extends HttpClient {
3132
3167
  format: "json",
3133
3168
  ...params,
3134
3169
  }),
3170
+ /**
3171
+ * @description Generate draft test requirements from an assistant's prompt, tools, and handoff configuration. The response includes numbered requirements describing expected behavior and open questions about behavior that needs clarification. Use this endpoint before generating a simulation suite. Review the requirements and resolve any open questions. To revise a draft, send it as `previousDraft` together with natural-language `feedback` describing the changes or answers. Once the requirements are ready, pass them to `POST /eval/simulation/onboarding/suite`. Requests can take up to 2 minutes. Each organization can draft requirements 60 times per UTC day, shared with the dashboard. A 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED` response means the daily limit has been reached. Wait until the limit resets at 00:00 UTC before retrying.
3172
+ *
3173
+ * @tags Simulation Suites
3174
+ * @name SimulationOnboardingControllerRequirementsDraft
3175
+ * @summary Draft simulation requirements
3176
+ * @request POST:/eval/simulation/onboarding/requirements
3177
+ * @secure
3178
+ */
3179
+ simulationOnboardingControllerRequirementsDraft: (data, params = {}) => this.request({
3180
+ path: `/eval/simulation/onboarding/requirements`,
3181
+ method: "POST",
3182
+ body: data,
3183
+ secure: true,
3184
+ type: ContentType.Json,
3185
+ format: "json",
3186
+ ...params,
3187
+ }),
3188
+ /**
3189
+ * @description Generate and save a simulation suite from reviewed test requirements, and assign it to an assistant. First draft requirements with `POST /eval/simulation/onboarding/requirements`. The response includes the saved suite, its simulations, and requirement coverage, including any gaps. Requests can take up to 3 minutes. Set a client timeout of at least 200 seconds. Use a unique `idempotencyKey` for each suite generation request. Retrying with the same key does not create a duplicate suite: - On a timeout or a 5xx (502, 504, 524), retry with the same key. You get the saved suite, a 409 while it is still generating, or a fresh generation if the first one never saved. - On a 409 `SIMULATION_ONBOARDING_GENERATION_IN_PROGRESS`, the first request is still generating. Wait `Retry-After` seconds (15) and retry with the same key. - On a 409 `SIMULATION_ONBOARDING_IDEMPOTENCY_KEY_REUSED`, the key was used for a different assistant or requirements. Send a new key. Review the generated simulations and coverage gaps. Revise the suite with `PATCH /eval/simulation/onboarding/suite/{suiteId}`, then run it with `POST /eval/simulation/run`. Each organization can generate 20 suites per UTC day, shared with the dashboard, and 2 at a time. A retry that returns a saved suite does not count toward these limits. On a 429 `SIMULATION_ONBOARDING_CONCURRENCY_LIMITED`, wait `Retry-After` seconds (30) before retrying. On a 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED`, wait until the daily limit resets at 00:00 UTC before retrying.
3190
+ *
3191
+ * @tags Simulation Suites
3192
+ * @name SimulationOnboardingControllerSuiteCreate
3193
+ * @summary Generate a simulation suite
3194
+ * @request POST:/eval/simulation/onboarding/suite
3195
+ * @secure
3196
+ */
3197
+ simulationOnboardingControllerSuiteCreate: (data, params = {}) => this.request({
3198
+ path: `/eval/simulation/onboarding/suite`,
3199
+ method: "POST",
3200
+ body: data,
3201
+ secure: true,
3202
+ type: ContentType.Json,
3203
+ format: "json",
3204
+ ...params,
3205
+ }),
3206
+ /**
3207
+ * @description Revise a suite created by `POST /eval/simulation/onboarding/suite`. Remove simulations, rewrite scenarios using natural-language instructions, or set `retryFailed` to regenerate scenarios that failed or exceeded generation limits. The response includes the updated suite and requirement coverage. Requests can take up to 2 minutes. Send a new `patchKey` for each edit. Replaying an applied key changes nothing and returns the current suite, with `failedEditSimulationIds` listing the simulations that edit could not rewrite, so retry a timeout or a 5xx with the same key. A suite remembers its last 50 keys, so never reuse one. Each organization can make 60 edits per UTC day, shared with the dashboard. Requests that only remove simulations and replays of applied edits do not count toward this limit. On a 429 `SIMULATION_ONBOARDING_QUOTA_EXCEEDED`, wait until the daily limit resets at 00:00 UTC before retrying.
3208
+ *
3209
+ * @tags Simulation Suites
3210
+ * @name SimulationOnboardingControllerSuiteUpdate
3211
+ * @summary Edit a generated simulation suite
3212
+ * @request PATCH:/eval/simulation/onboarding/suite/{suiteId}
3213
+ * @secure
3214
+ */
3215
+ simulationOnboardingControllerSuiteUpdate: (suiteId, data, params = {}) => this.request({
3216
+ path: `/eval/simulation/onboarding/suite/${suiteId}`,
3217
+ method: "PATCH",
3218
+ body: data,
3219
+ secure: true,
3220
+ type: ContentType.Json,
3221
+ format: "json",
3222
+ ...params,
3223
+ }),
3135
3224
  /**
3136
3225
  * No description
3137
3226
  *
package/dist/vapi.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ChatCompletionMessageParam } from 'openai/resources';
2
2
  import { DailyCall, DailyAdvancedConfig, DailyFactoryOptions, DailyEventObjectNoPayload, DailyEventObjectRecordingError, DailyEventObjectRecordingStarted, DailyEventObjectRecordingStopped, DailyParticipant, DailyVideoSendSettings } from '@daily-co/daily-js';
3
3
  import EventEmitter from 'events';
4
- import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWorkflowDTO, WorkflowOverrides } from './api';
4
+ import { Call, CreateSquadDTO, CreateAssistantDTO, AssistantOverrides, CreateWebCallDTO, CreateWorkflowDTO, WorkflowOverrides } from './api';
5
5
  export interface EndCallMessage {
6
6
  type: 'end-call';
7
7
  }
@@ -69,6 +69,7 @@ type VapiEventListeners = {
69
69
  'recording-error': (event: DailyEventObjectRecordingError) => void;
70
70
  'recording-upload-completed': (event: DailyEventObjectNoPayload) => void;
71
71
  };
72
+ type VideoRecordingStartMode = NonNullable<CreateWebCallDTO['videoRecordingStartMode']>;
72
73
  type StartCallOptions = {
73
74
  /** Acquire the microphone before creating a backend call. Defaults to true. */
74
75
  microphonePreflightEnabled?: boolean;
@@ -81,6 +82,25 @@ type StartCallOptions = {
81
82
  * @example true
82
83
  */
83
84
  roomDeleteOnUserLeaveEnabled?: boolean;
85
+ /**
86
+ * This determines who starts the video recording. Only applies when video
87
+ * recording is enabled (`artifactPlan.videoRecordingEnabled`).
88
+ *
89
+ * - `client`: the web client starts the recording when it joins.
90
+ * - `server`: Vapi starts the recording once the user has joined, so a weak
91
+ * client connection can't stop it from starting. If Vapi can't set up the
92
+ * server-side start, or the start itself fails, the SDK emits
93
+ * `recording-error`.
94
+ *
95
+ * In `server` mode the SDK never calls `startRecording()` itself. If the
96
+ * server-side start couldn't be set up, calling it yourself may still start
97
+ * the recording.
98
+ *
99
+ * Requires a Vapi API that supports this field; older servers reject the
100
+ * request. The server defaults to `client` when it is not set.
101
+ * @example "server"
102
+ */
103
+ videoRecordingStartMode?: VideoRecordingStartMode;
84
104
  };
85
105
  type WebCall = {
86
106
  /**
@@ -113,13 +133,21 @@ type WebCall = {
113
133
  };
114
134
  /**
115
135
  * The Vapi WebCall transport. When video recording is enabled, the server
116
- * sets `callToken` to a Daily meeting token that auto-starts the cloud
117
- * recording on join.
136
+ * sets `callToken` to the Daily meeting token to join with. In `client` mode
137
+ * the token auto-starts the cloud recording on join; in `server` mode it
138
+ * starts nothing and Vapi starts the recording. If there is no token, the
139
+ * SDK emits `recording-error` and starts nothing, though `startRecording()`
140
+ * may still start the recording from the client.
141
+ *
142
+ * `videoRecordingStartMode` is `server` when Vapi starts the recording, and
143
+ * is absent when video recording is off or the server-side start couldn't
144
+ * be set up.
118
145
  *
119
146
  * call.transport
120
147
  */
121
148
  transport?: {
122
149
  callToken?: string;
150
+ videoRecordingStartMode?: VideoRecordingStartMode;
123
151
  };
124
152
  };
125
153
  declare class VapiEventEmitter extends EventEmitter {
@@ -220,6 +248,16 @@ export default class Vapi extends VapiEventEmitter {
220
248
  * which EventEmitter throws on when nothing is listening.
221
249
  */
222
250
  private emitToConsumer;
251
+ /**
252
+ * Reported in a later task, like Daily's own recording-error: start() or
253
+ * reconnect() has resolved and the caller's await continuation has run, so a
254
+ * listener registered right after it still hears this, and one that calls
255
+ * stop() can't interrupt call setup. Skipped if that call was stopped or
256
+ * replaced.
257
+ */
258
+ private recordingTokenMissingReportSchedule;
259
+ /** The fields are read defensively: the app message is untrusted JSON. */
260
+ private videoRecordingStartFailedReport;
223
261
  /** Drops the reference before removing, so nothing hands out a detached element. */
224
262
  private detachAudioPlayer;
225
263
  /**
package/dist/vapi.js CHANGED
@@ -58,6 +58,8 @@ function serializeError(error) {
58
58
  }
59
59
  return { message: String(error) };
60
60
  }
61
+ // The app message Vapi sends when it couldn't start a server-mode recording.
62
+ const VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE = 'video-recording.start-failed';
61
63
  async function startAudioPlayer(player, track) {
62
64
  player.muted = false;
63
65
  player.autoplay = true;
@@ -88,13 +90,13 @@ function destroyAudioPlayer(participantId) {
88
90
  const player = document.querySelector(`audio[data-participant-id="${participantId}"]`);
89
91
  player?.remove();
90
92
  }
91
- function subscribeToTracks(e, call, isVideoRecordingEnabled, isVideoEnabled) {
93
+ function subscribeToTracks(e, call, isVideoRecordingEnabled) {
92
94
  if (e.participant.local)
93
95
  return;
94
96
  call.updateParticipant(e.participant.session_id, {
95
97
  setSubscribedTracks: {
96
98
  audio: true,
97
- video: isVideoRecordingEnabled || isVideoEnabled,
99
+ video: isVideoRecordingEnabled,
98
100
  },
99
101
  });
100
102
  }
@@ -295,7 +297,9 @@ class Vapi extends VapiEventEmitter {
295
297
  timestamp: new Date().toISOString()
296
298
  });
297
299
  const webCallStartTime = Date.now();
298
- const webCall = (await client_1.client.call.callControllerCreateWebCall({
300
+ // An unset option stays undefined, so JSON.stringify drops the key and
301
+ // older servers never see it.
302
+ const createWebCallBody = {
299
303
  assistant: typeof assistant === 'string' ? undefined : assistant,
300
304
  assistantId: typeof assistant === 'string' ? assistant : undefined,
301
305
  assistantOverrides,
@@ -305,7 +309,9 @@ class Vapi extends VapiEventEmitter {
305
309
  workflowId: typeof workflow === 'string' ? workflow : undefined,
306
310
  workflowOverrides,
307
311
  roomDeleteOnUserLeaveEnabled: options?.roomDeleteOnUserLeaveEnabled,
308
- })).data;
312
+ videoRecordingStartMode: options?.videoRecordingStartMode,
313
+ };
314
+ const webCall = (await client_1.client.call.callControllerCreateWebCall(createWebCallBody)).data;
309
315
  const webCallDuration = Date.now() - webCallStartTime;
310
316
  this.emit('call-start-progress', {
311
317
  stage: 'web-call-creation',
@@ -319,12 +325,16 @@ class Vapi extends VapiEventEmitter {
319
325
  }
320
326
  });
321
327
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
322
- const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
323
328
  // When video recording is enabled, the server sends a Daily meeting
324
- // token that auto-starts the cloud recording on join — more reliable
325
- // than calling startRecording() from a weak client network. Older
326
- // servers don't send one; then the client starts the recording itself.
327
- const callToken = webCall?.transport?.callToken;
329
+ // token. In client mode it auto-starts the cloud recording on join. In
330
+ // server mode it starts nothing: Vapi starts the recording from its
331
+ // servers once the user has joined, which a weak client network can't
332
+ // stop. Without a token the SDK emits recording-error and starts
333
+ // nothing.
334
+ const transport = webCall?.transport;
335
+ const callToken = transport?.callToken;
336
+ const isRecordingStartedByServer = transport?.videoRecordingStartMode === 'server';
337
+ const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
328
338
  // Stage 2: Create Daily call object
329
339
  this.emit('call-start-progress', {
330
340
  stage: 'daily-call-object-creation',
@@ -334,7 +344,6 @@ class Vapi extends VapiEventEmitter {
334
344
  audioSource: this.dailyCallObject.audioSource ?? true,
335
345
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
336
346
  isVideoRecordingEnabled,
337
- isVideoEnabled
338
347
  }
339
348
  });
340
349
  const dailyCallStartTime = Date.now();
@@ -435,7 +444,7 @@ class Vapi extends VapiEventEmitter {
435
444
  this.call.on('participant-joined', (e) => {
436
445
  if (!e || !this.call)
437
446
  return;
438
- subscribeToTracks(e, this.call, isVideoRecordingEnabled, isVideoEnabled);
447
+ subscribeToTracks(e, this.call, isVideoRecordingEnabled);
439
448
  });
440
449
  this.call.on('participant-updated', (e) => {
441
450
  if (!e) {
@@ -449,6 +458,10 @@ class Vapi extends VapiEventEmitter {
449
458
  }
450
459
  this.detachAudioPlayer(e.participant.session_id);
451
460
  });
461
+ // Registered before join(), as in reconnect(): Vapi starts a server-mode
462
+ // recording when it sees the user join, so its start-failed message can
463
+ // arrive while join() is still in flight.
464
+ this.call.on('app-message', (e) => this.onAppMessage(e));
452
465
  // Registered before join(): a token-auto-started recording can begin
453
466
  // while join() is still in flight, so a listener added after join
454
467
  // could miss the event.
@@ -543,51 +556,29 @@ class Vapi extends VapiEventEmitter {
543
556
  }
544
557
  // Stage 5: Video recording setup (if enabled)
545
558
  if (isVideoRecordingEnabled && callToken) {
546
- // The meeting token auto-started the recording at join, so there is
547
- // nothing to request from the client.
559
+ // The meeting token auto-started the recording at join, or Vapi
560
+ // starts it from its servers, so there is nothing to request from the
561
+ // client.
548
562
  this.emit('call-start-progress', {
549
563
  stage: 'video-recording-setup',
550
564
  status: 'completed',
551
565
  timestamp: new Date().toISOString(),
552
- metadata: { action: 'auto-started-via-meeting-token' }
566
+ metadata: {
567
+ action: isRecordingStartedByServer
568
+ ? 'started-by-server'
569
+ : 'auto-started-via-meeting-token'
570
+ }
553
571
  });
554
572
  }
555
573
  else if (isVideoRecordingEnabled) {
574
+ // No token: nothing starts the recording. The recording-error is
575
+ // reported once start() has finished.
556
576
  this.emit('call-start-progress', {
557
577
  stage: 'video-recording-setup',
558
- status: 'started',
559
- timestamp: new Date().toISOString()
578
+ status: 'failed',
579
+ timestamp: new Date().toISOString(),
580
+ metadata: { action: 'no-meeting-token' }
560
581
  });
561
- const recordingStartTime = Date.now();
562
- try {
563
- recordingRequestedTime = new Date().getTime();
564
- this.startRecording();
565
- const recordingSetupDuration = Date.now() - recordingStartTime;
566
- this.emit('call-start-progress', {
567
- stage: 'video-recording-setup',
568
- status: 'completed',
569
- duration: recordingSetupDuration,
570
- timestamp: new Date().toISOString()
571
- });
572
- }
573
- catch (error) {
574
- const recordingSetupDuration = Date.now() - recordingStartTime;
575
- const serializedError = serializeError(error);
576
- this.emit('call-start-progress', {
577
- stage: 'video-recording-setup',
578
- status: 'failed',
579
- duration: recordingSetupDuration,
580
- timestamp: new Date().toISOString(),
581
- metadata: { error: serializedError.message }
582
- });
583
- this.emit('error', {
584
- type: 'video-recording-setup-error',
585
- stage: 'video-recording-setup',
586
- error: serializedError,
587
- timestamp: new Date().toISOString()
588
- });
589
- // Don't throw here, video recording is optional
590
- }
591
582
  }
592
583
  else {
593
584
  this.emit('call-start-progress', {
@@ -640,7 +631,6 @@ class Vapi extends VapiEventEmitter {
640
631
  if (e)
641
632
  this.handleLocalAudioLevel(e);
642
633
  });
643
- this.call.on('app-message', (e) => this.onAppMessage(e));
644
634
  this.call.on('nonfatal-error', (e) => {
645
635
  // https://docs.daily.co/reference/daily-js/events/meeting-events#type-audio-processor-error
646
636
  if (e?.type === 'local-audio-level-observer-error') {
@@ -723,6 +713,9 @@ class Vapi extends VapiEventEmitter {
723
713
  callId: webCall?.id || 'unknown',
724
714
  timestamp: new Date().toISOString()
725
715
  });
716
+ if (recordingTokenMissing) {
717
+ this.recordingTokenMissingReportSchedule();
718
+ }
726
719
  return webCall;
727
720
  }
728
721
  catch (e) {
@@ -772,6 +765,12 @@ class Vapi extends VapiEventEmitter {
772
765
  else {
773
766
  try {
774
767
  const parsedMessage = JSON.parse(e.data);
768
+ // Not a Vapi client message: re-emitted as recording-error, and kept
769
+ // out of 'message' because its consumers switch on `type`.
770
+ if (parsedMessage?.type === VIDEO_RECORDING_START_FAILED_MESSAGE_TYPE) {
771
+ this.videoRecordingStartFailedReport(parsedMessage);
772
+ return;
773
+ }
775
774
  this.emit('message', parsedMessage);
776
775
  if (parsedMessage && 'type' in parsedMessage && 'status' in parsedMessage && parsedMessage.type === 'status-update' && parsedMessage.status === 'ended') {
777
776
  this.hasEmittedCallEndedStatus = true;
@@ -939,6 +938,37 @@ class Vapi extends VapiEventEmitter {
939
938
  console.error(`[vapi] ${event} event listener threw`, error);
940
939
  }
941
940
  }
941
+ /**
942
+ * Reported in a later task, like Daily's own recording-error: start() or
943
+ * reconnect() has resolved and the caller's await continuation has run, so a
944
+ * listener registered right after it still hears this, and one that calls
945
+ * stop() can't interrupt call setup. Skipped if that call was stopped or
946
+ * replaced.
947
+ */
948
+ recordingTokenMissingReportSchedule() {
949
+ const call = this.call;
950
+ if (!call)
951
+ return;
952
+ setTimeout(() => {
953
+ if (!this.started || this.call !== call)
954
+ return;
955
+ this.emitToConsumer('recording-error', {
956
+ action: 'recording-error',
957
+ callClientId: call.callClientId,
958
+ errorMsg: 'Vapi returned no recording token, so the recording was not started. Call startRecording() to try starting it from the client.',
959
+ });
960
+ }, 0);
961
+ }
962
+ /** The fields are read defensively: the app message is untrusted JSON. */
963
+ videoRecordingStartFailedReport(message) {
964
+ const outcome = typeof message.outcome === 'string' ? message.outcome : 'unknown';
965
+ const error = typeof message.error === 'string' ? message.error : 'unknown';
966
+ this.emitToConsumer('recording-error', {
967
+ action: 'recording-error',
968
+ callClientId: this.call?.callClientId ?? '',
969
+ errorMsg: `Vapi couldn't start the recording (${outcome}): ${error}`,
970
+ });
971
+ }
942
972
  /** Drops the reference before removing, so nothing hands out a detached element. */
943
973
  detachAudioPlayer(participantId) {
944
974
  if (this.audioPlayer?.dataset.participantId === participantId) {
@@ -1171,11 +1201,14 @@ class Vapi extends VapiEventEmitter {
1171
1201
  });
1172
1202
  }
1173
1203
  const isVideoRecordingEnabled = webCall?.artifactPlan?.videoRecordingEnabled ?? false;
1174
- const isVideoEnabled = webCall?.assistant?.voice?.provider === 'tavus';
1175
- // Same auto-start token as in start(). Rejoining with it while the
1204
+ // Same token as in start(). In client mode, rejoining with it while the
1176
1205
  // recording is still running is safe; if the recording stopped when the
1177
- // user left, the token starts a new one.
1206
+ // user left, the token starts a new one. In server mode it starts
1207
+ // nothing. Without a token the SDK emits recording-error and starts
1208
+ // nothing.
1178
1209
  const callToken = webCall?.transport?.callToken;
1210
+ const isRecordingStartedByServer = webCall?.transport?.videoRecordingStartMode === 'server';
1211
+ const recordingTokenMissing = isVideoRecordingEnabled && !callToken;
1179
1212
  // Stage 1: Create Daily call object
1180
1213
  this.emit('call-start-progress', {
1181
1214
  stage: 'daily-call-object-creation',
@@ -1185,7 +1218,6 @@ class Vapi extends VapiEventEmitter {
1185
1218
  audioSource: this.dailyCallObject.audioSource ?? true,
1186
1219
  videoSource: this.dailyCallObject.videoSource ?? isVideoRecordingEnabled,
1187
1220
  isVideoRecordingEnabled,
1188
- isVideoEnabled
1189
1221
  }
1190
1222
  });
1191
1223
  const dailyCallStartTime = Date.now();
@@ -1267,7 +1299,7 @@ class Vapi extends VapiEventEmitter {
1267
1299
  this.call.on('participant-joined', (e) => {
1268
1300
  if (!e || !this.call)
1269
1301
  return;
1270
- subscribeToTracks(e, this.call, isVideoRecordingEnabled, isVideoEnabled);
1302
+ subscribeToTracks(e, this.call, isVideoRecordingEnabled);
1271
1303
  });
1272
1304
  this.call.on('participant-updated', (e) => {
1273
1305
  if (!e) {
@@ -1384,45 +1416,29 @@ class Vapi extends VapiEventEmitter {
1384
1416
  });
1385
1417
  // Stage 4: Video recording setup (if enabled)
1386
1418
  if (isVideoRecordingEnabled && callToken) {
1387
- // The meeting token auto-started the recording at join, so there is
1388
- // nothing to request from the client.
1419
+ // The meeting token auto-started the recording at join, or Vapi
1420
+ // starts it from its servers, so there is nothing to request from the
1421
+ // client.
1389
1422
  this.emit('call-start-progress', {
1390
1423
  stage: 'video-recording-setup',
1391
1424
  status: 'completed',
1392
1425
  timestamp: new Date().toISOString(),
1393
- metadata: { action: 'auto-started-via-meeting-token' }
1426
+ metadata: {
1427
+ action: isRecordingStartedByServer
1428
+ ? 'started-by-server'
1429
+ : 'auto-started-via-meeting-token'
1430
+ }
1394
1431
  });
1395
1432
  }
1396
1433
  else if (isVideoRecordingEnabled) {
1434
+ // No token: nothing starts the recording. The recording-error is
1435
+ // reported once reconnect() has finished.
1397
1436
  this.emit('call-start-progress', {
1398
1437
  stage: 'video-recording-setup',
1399
- status: 'started',
1400
- timestamp: new Date().toISOString()
1438
+ status: 'failed',
1439
+ timestamp: new Date().toISOString(),
1440
+ metadata: { action: 'no-meeting-token' }
1401
1441
  });
1402
- const recordingStartTime = Date.now();
1403
- try {
1404
- recordingRequestedTime = new Date().getTime();
1405
- this.startRecording();
1406
- const recordingSetupDuration = Date.now() - recordingStartTime;
1407
- this.emit('call-start-progress', {
1408
- stage: 'video-recording-setup',
1409
- status: 'completed',
1410
- duration: recordingSetupDuration,
1411
- timestamp: new Date().toISOString()
1412
- });
1413
- }
1414
- catch (error) {
1415
- const recordingSetupDuration = Date.now() - recordingStartTime;
1416
- const serializedError = serializeError(error);
1417
- this.emit('call-start-progress', {
1418
- stage: 'video-recording-setup',
1419
- status: 'failed',
1420
- duration: recordingSetupDuration,
1421
- timestamp: new Date().toISOString(),
1422
- metadata: { error: serializedError.message }
1423
- });
1424
- // Don't throw here, video recording is optional
1425
- }
1426
1442
  }
1427
1443
  else {
1428
1444
  this.emit('call-start-progress', {
@@ -1516,6 +1532,9 @@ class Vapi extends VapiEventEmitter {
1516
1532
  timestamp: new Date().toISOString()
1517
1533
  });
1518
1534
  this.emit('call-start');
1535
+ if (recordingTokenMissing) {
1536
+ this.recordingTokenMissingReportSchedule();
1537
+ }
1519
1538
  }
1520
1539
  catch (e) {
1521
1540
  const totalDuration = Date.now() - startTime;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vapi-ai/web",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "",
5
5
  "main": "dist/vapi.js",
6
6
  "types": "dist/vapi.d.ts",