@absolutejs/voice 0.0.22-beta.670 → 0.0.22-beta.672

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.
@@ -13,6 +13,8 @@ export type OpenAIVoiceAssistantModelOptions = {
13
13
  model?: string;
14
14
  onUsage?: (usage: Record<string, unknown>) => Promise<void> | void;
15
15
  temperature?: number;
16
+ /** Maximum silence between SSE body chunks. Default 10 seconds. */
17
+ streamInactivityMs?: number;
16
18
  /**
17
19
  * Hard cap on a single /responses stream. Aborts via AbortController if
18
20
  * the stream doesn't complete in time. Default 60s — generous for typical
@@ -62,6 +64,9 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
62
64
  suppressedUntil?: number;
63
65
  status: "error" | "fallback" | "success";
64
66
  timedOut?: boolean;
67
+ timeoutKind?: "latency-budget" | "stream-inactivity";
68
+ timeoutMs?: number;
69
+ totalElapsedMs?: number;
65
70
  };
66
71
  export type VoiceProviderRouterFallbackMode = "never" | "provider-error" | "rate-limit";
67
72
  export type VoiceProviderRouterStrategy = "balanced" | "ordered" | "prefer-cheapest" | "prefer-fastest" | "prefer-selected" | "quality-first";
package/dist/index.js CHANGED
@@ -4434,9 +4434,13 @@ var createVoiceSession = (options) => {
4434
4434
  if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
4435
4435
  return;
4436
4436
  }
4437
+ const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
4438
+ if (lastTranscriptAt !== undefined && stuckCloseAfterMs !== undefined && Date.now() - lastTranscriptAt < stuckCloseAfterMs) {
4439
+ kickStuckCloseWatchdog();
4440
+ return;
4441
+ }
4437
4442
  stuckCloseFired = true;
4438
4443
  const reason = `no caller progress for ${stuckCloseAfterMs}ms`;
4439
- const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
4440
4444
  const sttDeafConfirmed = sttDeafConfirmedAt > 0 && (lastTranscriptAt === undefined || sttDeafConfirmedAt > lastTranscriptAt);
4441
4445
  const diagnostic = {
4442
4446
  action: "stuck-call-close",
@@ -41730,13 +41734,14 @@ var buildVoiceIOProviderRouterTraceEvent = (options) => ({
41730
41734
  metadata: options.metadata,
41731
41735
  payload: {
41732
41736
  ...options.event,
41737
+ recoverable: options.event.status === "error" && options.event.fallbackProvider !== undefined,
41733
41738
  ...options.payload ?? {},
41734
41739
  providerStatus: options.event.status
41735
41740
  },
41736
41741
  scenarioId: options.scenarioId,
41737
41742
  sessionId: options.sessionId,
41738
41743
  turnId: options.turnId,
41739
- type: options.type ?? "session.error"
41744
+ type: options.type ?? (options.event.status === "error" ? "session.error" : "provider.decision")
41740
41745
  });
41741
41746
  var buildVoiceProviderRouterTraceEvent = (options) => ({
41742
41747
  at: options.at ?? options.event.at,
@@ -41744,13 +41749,14 @@ var buildVoiceProviderRouterTraceEvent = (options) => ({
41744
41749
  metadata: options.metadata,
41745
41750
  payload: {
41746
41751
  ...options.event,
41752
+ recoverable: options.event.status === "error" && options.event.fallbackProvider !== undefined,
41747
41753
  ...options.payload ?? {},
41748
41754
  providerStatus: options.event.status
41749
41755
  },
41750
41756
  scenarioId: options.scenarioId,
41751
41757
  sessionId: options.sessionId,
41752
41758
  turnId: options.turnId,
41753
- type: options.type ?? "session.error"
41759
+ type: options.type ?? (options.event.status === "error" ? "session.error" : "provider.decision")
41754
41760
  });
41755
41761
  // src/core/workflowContract.ts
41756
41762
  var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
@@ -46363,6 +46369,15 @@ class VoiceProviderTimeoutError extends Error {
46363
46369
  this.timeoutMs = timeoutMs;
46364
46370
  }
46365
46371
  }
46372
+
46373
+ class VoiceProviderStreamInactivityError extends Error {
46374
+ inactivityMs;
46375
+ constructor(inactivityMs) {
46376
+ super(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`);
46377
+ this.name = "VoiceProviderStreamInactivityError";
46378
+ this.inactivityMs = inactivityMs;
46379
+ }
46380
+ }
46366
46381
  var getMessageToolCalls = (message) => {
46367
46382
  const toolCalls = message.metadata?.toolCalls;
46368
46383
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -46609,7 +46624,7 @@ var createVoiceProviderRouter = (options) => {
46609
46624
  }
46610
46625
  }
46611
46626
  };
46612
- const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
46627
+ const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt, routingStartedAt) => {
46613
46628
  let output = await runProvider(provider, model, input);
46614
46629
  let emptyAttempt = 0;
46615
46630
  while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
@@ -46621,7 +46636,8 @@ var createVoiceProviderRouter = (options) => {
46621
46636
  emptyCompletion: true,
46622
46637
  provider,
46623
46638
  selectedProvider,
46624
- status: "success"
46639
+ status: "success",
46640
+ totalElapsedMs: Date.now() - routingStartedAt
46625
46641
  }, input);
46626
46642
  output = await runProvider(provider, model, input);
46627
46643
  }
@@ -46633,6 +46649,7 @@ var createVoiceProviderRouter = (options) => {
46633
46649
  if (!selectedProvider || order.length === 0) {
46634
46650
  throw new Error("Voice provider router has no available providers.");
46635
46651
  }
46652
+ const routingStartedAt = Date.now();
46636
46653
  let lastError;
46637
46654
  for (const [index, provider] of order.entries()) {
46638
46655
  const model = options.providers[provider];
@@ -46641,7 +46658,7 @@ var createVoiceProviderRouter = (options) => {
46641
46658
  }
46642
46659
  const startedAt = Date.now();
46643
46660
  try {
46644
- const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
46661
+ const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt, routingStartedAt);
46645
46662
  const providerHealth = recordProviderSuccess(provider);
46646
46663
  await emit({
46647
46664
  at: Date.now(),
@@ -46653,14 +46670,15 @@ var createVoiceProviderRouter = (options) => {
46653
46670
  providerHealth,
46654
46671
  recovered: provider !== selectedProvider,
46655
46672
  selectedProvider,
46656
- status: provider === selectedProvider ? "success" : "fallback"
46673
+ status: provider === selectedProvider ? "success" : "fallback",
46674
+ totalElapsedMs: Date.now() - routingStartedAt
46657
46675
  }, input);
46658
46676
  return output;
46659
46677
  } catch (error) {
46660
46678
  lastError = error;
46661
46679
  const hasNextProvider = index < order.length - 1;
46662
46680
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
46663
- const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
46681
+ const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
46664
46682
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
46665
46683
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
46666
46684
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
@@ -46679,7 +46697,10 @@ var createVoiceProviderRouter = (options) => {
46679
46697
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
46680
46698
  suppressedUntil: providerHealth?.suppressedUntil,
46681
46699
  status: "error",
46682
- timedOut
46700
+ timedOut,
46701
+ timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
46702
+ timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
46703
+ totalElapsedMs: Date.now() - routingStartedAt
46683
46704
  }, input);
46684
46705
  if (!hasNextProvider || !shouldFallback) {
46685
46706
  throw error;
@@ -46882,7 +46903,7 @@ var readServerSentEvents = async (response, onEvent, abortOptions) => {
46882
46903
  return new Promise((resolve2, reject) => {
46883
46904
  inactivityTimer = setTimeout(() => {
46884
46905
  reader.cancel().catch(() => {});
46885
- reject(new Error(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`));
46906
+ reject(new VoiceProviderStreamInactivityError(inactivityMs));
46886
46907
  }, inactivityMs);
46887
46908
  if (signal) {
46888
46909
  onAbort = () => {
@@ -47071,7 +47092,7 @@ var createOpenAIVoiceAssistantModel = (options) => {
47071
47092
  outputItems
47072
47093
  } = await consumeOpenAIResponsesStream(response, onTextDelta, {
47073
47094
  signal: ac.signal,
47074
- inactivityMs: 1e4
47095
+ inactivityMs: options.streamInactivityMs ?? 1e4
47075
47096
  }));
47076
47097
  stamp("openai.stream-done", {
47077
47098
  textChars: assistantText?.length ?? 0,
@@ -5248,6 +5248,15 @@ class VoiceProviderTimeoutError extends Error {
5248
5248
  this.timeoutMs = timeoutMs;
5249
5249
  }
5250
5250
  }
5251
+
5252
+ class VoiceProviderStreamInactivityError extends Error {
5253
+ inactivityMs;
5254
+ constructor(inactivityMs) {
5255
+ super(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`);
5256
+ this.name = "VoiceProviderStreamInactivityError";
5257
+ this.inactivityMs = inactivityMs;
5258
+ }
5259
+ }
5251
5260
  var getMessageToolCalls = (message) => {
5252
5261
  const toolCalls = message.metadata?.toolCalls;
5253
5262
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -5494,7 +5503,7 @@ var createVoiceProviderRouter = (options) => {
5494
5503
  }
5495
5504
  }
5496
5505
  };
5497
- const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
5506
+ const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt, routingStartedAt) => {
5498
5507
  let output = await runProvider(provider, model, input);
5499
5508
  let emptyAttempt = 0;
5500
5509
  while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
@@ -5506,7 +5515,8 @@ var createVoiceProviderRouter = (options) => {
5506
5515
  emptyCompletion: true,
5507
5516
  provider,
5508
5517
  selectedProvider,
5509
- status: "success"
5518
+ status: "success",
5519
+ totalElapsedMs: Date.now() - routingStartedAt
5510
5520
  }, input);
5511
5521
  output = await runProvider(provider, model, input);
5512
5522
  }
@@ -5518,6 +5528,7 @@ var createVoiceProviderRouter = (options) => {
5518
5528
  if (!selectedProvider || order.length === 0) {
5519
5529
  throw new Error("Voice provider router has no available providers.");
5520
5530
  }
5531
+ const routingStartedAt = Date.now();
5521
5532
  let lastError;
5522
5533
  for (const [index, provider] of order.entries()) {
5523
5534
  const model = options.providers[provider];
@@ -5526,7 +5537,7 @@ var createVoiceProviderRouter = (options) => {
5526
5537
  }
5527
5538
  const startedAt = Date.now();
5528
5539
  try {
5529
- const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
5540
+ const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt, routingStartedAt);
5530
5541
  const providerHealth = recordProviderSuccess(provider);
5531
5542
  await emit({
5532
5543
  at: Date.now(),
@@ -5538,14 +5549,15 @@ var createVoiceProviderRouter = (options) => {
5538
5549
  providerHealth,
5539
5550
  recovered: provider !== selectedProvider,
5540
5551
  selectedProvider,
5541
- status: provider === selectedProvider ? "success" : "fallback"
5552
+ status: provider === selectedProvider ? "success" : "fallback",
5553
+ totalElapsedMs: Date.now() - routingStartedAt
5542
5554
  }, input);
5543
5555
  return output;
5544
5556
  } catch (error) {
5545
5557
  lastError = error;
5546
5558
  const hasNextProvider = index < order.length - 1;
5547
5559
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
5548
- const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
5560
+ const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
5549
5561
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
5550
5562
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
5551
5563
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
@@ -5564,7 +5576,10 @@ var createVoiceProviderRouter = (options) => {
5564
5576
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
5565
5577
  suppressedUntil: providerHealth?.suppressedUntil,
5566
5578
  status: "error",
5567
- timedOut
5579
+ timedOut,
5580
+ timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
5581
+ timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
5582
+ totalElapsedMs: Date.now() - routingStartedAt
5568
5583
  }, input);
5569
5584
  if (!hasNextProvider || !shouldFallback) {
5570
5585
  throw error;
@@ -5767,7 +5782,7 @@ var readServerSentEvents = async (response, onEvent, abortOptions) => {
5767
5782
  return new Promise((resolve2, reject) => {
5768
5783
  inactivityTimer = setTimeout(() => {
5769
5784
  reader.cancel().catch(() => {});
5770
- reject(new Error(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`));
5785
+ reject(new VoiceProviderStreamInactivityError(inactivityMs));
5771
5786
  }, inactivityMs);
5772
5787
  if (signal) {
5773
5788
  onAbort = () => {
@@ -5956,7 +5971,7 @@ var createOpenAIVoiceAssistantModel = (options) => {
5956
5971
  outputItems
5957
5972
  } = await consumeOpenAIResponsesStream(response, onTextDelta, {
5958
5973
  signal: ac.signal,
5959
- inactivityMs: 1e4
5974
+ inactivityMs: options.streamInactivityMs ?? 1e4
5960
5975
  }));
5961
5976
  stamp("openai.stream-done", {
5962
5977
  textChars: assistantText?.length ?? 0,
@@ -7415,9 +7430,13 @@ var createVoiceSession = (options) => {
7415
7430
  if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
7416
7431
  return;
7417
7432
  }
7433
+ const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
7434
+ if (lastTranscriptAt !== undefined && stuckCloseAfterMs !== undefined && Date.now() - lastTranscriptAt < stuckCloseAfterMs) {
7435
+ kickStuckCloseWatchdog();
7436
+ return;
7437
+ }
7418
7438
  stuckCloseFired = true;
7419
7439
  const reason = `no caller progress for ${stuckCloseAfterMs}ms`;
7420
- const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
7421
7440
  const sttDeafConfirmed = sttDeafConfirmedAt > 0 && (lastTranscriptAt === undefined || sttDeafConfirmedAt > lastTranscriptAt);
7422
7441
  const diagnostic = {
7423
7442
  action: "stuck-call-close",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.670",
3
+ "version": "0.0.22-beta.672",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",