@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.32

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/index.js CHANGED
@@ -6552,7 +6552,8 @@ var summarizeVoiceProviderHealth = async (input) => {
6552
6552
  rateLimited: false,
6553
6553
  recommended: false,
6554
6554
  runCount: 0,
6555
- status: "idle"
6555
+ status: "idle",
6556
+ timeoutCount: 0
6556
6557
  };
6557
6558
  entries.set(provider, entry);
6558
6559
  return entry;
@@ -6629,6 +6630,9 @@ var summarizeVoiceProviderHealth = async (input) => {
6629
6630
  }
6630
6631
  const entry = applyProviderHealth();
6631
6632
  entry.errorCount += 1;
6633
+ if (event.payload.timedOut === true) {
6634
+ entry.timeoutCount += 1;
6635
+ }
6632
6636
  entry.lastError = getString(event.payload.error);
6633
6637
  entry.lastErrorAt = event.at;
6634
6638
  entry.rateLimited ||= event.payload.rateLimited === true;
@@ -6653,7 +6657,8 @@ var summarizeVoiceProviderHealth = async (input) => {
6653
6657
  runCount: entry.runCount,
6654
6658
  status,
6655
6659
  suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
6656
- suppressedUntil: entry.suppressedUntil
6660
+ suppressedUntil: entry.suppressedUntil,
6661
+ timeoutCount: entry.timeoutCount
6657
6662
  };
6658
6663
  });
6659
6664
  const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
@@ -6677,6 +6682,7 @@ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p
6677
6682
  `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
6678
6683
  `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
6679
6684
  `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
6685
+ `<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
6680
6686
  `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
6681
6687
  "</dl>",
6682
6688
  suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
@@ -8132,6 +8138,17 @@ var parseJSONValue = (value) => {
8132
8138
  return value;
8133
8139
  }
8134
8140
  };
8141
+
8142
+ class VoiceProviderTimeoutError extends Error {
8143
+ provider;
8144
+ timeoutMs;
8145
+ constructor(provider, timeoutMs) {
8146
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
8147
+ this.name = "VoiceProviderTimeoutError";
8148
+ this.provider = provider;
8149
+ this.timeoutMs = timeoutMs;
8150
+ }
8151
+ }
8135
8152
  var getMessageToolCalls = (message) => {
8136
8153
  const toolCalls = message.metadata?.toolCalls;
8137
8154
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -8209,6 +8226,10 @@ var createVoiceProviderRouter = (options) => {
8209
8226
  const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
8210
8227
  const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
8211
8228
  const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
8229
+ const getProviderTimeoutMs = (provider) => {
8230
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
8231
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
8232
+ };
8212
8233
  const getHealth = (provider) => {
8213
8234
  const existing = healthState.get(provider);
8214
8235
  if (existing) {
@@ -8320,6 +8341,25 @@ var createVoiceProviderRouter = (options) => {
8320
8341
  const emit = async (event, input) => {
8321
8342
  await options.onProviderEvent?.(event, input);
8322
8343
  };
8344
+ const runProvider = async (provider, model, input) => {
8345
+ const timeoutMs = getProviderTimeoutMs(provider);
8346
+ if (!timeoutMs) {
8347
+ return model.generate(input);
8348
+ }
8349
+ let timeout;
8350
+ try {
8351
+ return await Promise.race([
8352
+ model.generate(input),
8353
+ new Promise((_, reject) => {
8354
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
8355
+ })
8356
+ ]);
8357
+ } finally {
8358
+ if (timeout) {
8359
+ clearTimeout(timeout);
8360
+ }
8361
+ }
8362
+ };
8323
8363
  return {
8324
8364
  generate: async (input) => {
8325
8365
  const { order, selectedProvider } = await resolveOrder(input);
@@ -8334,12 +8374,14 @@ var createVoiceProviderRouter = (options) => {
8334
8374
  }
8335
8375
  const startedAt = Date.now();
8336
8376
  try {
8337
- const output = await model.generate(input);
8377
+ const output = await runProvider(provider, model, input);
8338
8378
  const providerHealth = recordProviderSuccess(provider);
8339
8379
  await emit({
8340
8380
  at: Date.now(),
8381
+ attempt: index + 1,
8341
8382
  elapsedMs: Date.now() - startedAt,
8342
8383
  fallbackProvider: provider === selectedProvider ? undefined : provider,
8384
+ latencyBudgetMs: getProviderTimeoutMs(provider),
8343
8385
  provider,
8344
8386
  providerHealth,
8345
8387
  recovered: provider !== selectedProvider,
@@ -8351,22 +8393,26 @@ var createVoiceProviderRouter = (options) => {
8351
8393
  lastError = error;
8352
8394
  const hasNextProvider = index < order.length - 1;
8353
8395
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
8396
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
8354
8397
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
8355
8398
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
8356
8399
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
8357
8400
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
8358
8401
  await emit({
8359
8402
  at: Date.now(),
8403
+ attempt: index + 1,
8360
8404
  elapsedMs: Date.now() - startedAt,
8361
8405
  error: errorMessage(error),
8362
8406
  fallbackProvider: shouldFallback ? nextProvider : undefined,
8407
+ latencyBudgetMs: getProviderTimeoutMs(provider),
8363
8408
  provider,
8364
8409
  providerHealth,
8365
8410
  rateLimited,
8366
8411
  selectedProvider,
8367
8412
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
8368
8413
  suppressedUntil: providerHealth?.suppressedUntil,
8369
- status: "error"
8414
+ status: "error",
8415
+ timedOut
8370
8416
  }, input);
8371
8417
  if (!hasNextProvider || !shouldFallback) {
8372
8418
  throw error;
@@ -36,9 +36,11 @@ export type GeminiVoiceAssistantModelOptions = {
36
36
  };
37
37
  export type VoiceProviderRouterEvent<TProvider extends string = string> = {
38
38
  at: number;
39
+ attempt: number;
39
40
  elapsedMs: number;
40
41
  error?: string;
41
42
  fallbackProvider?: TProvider;
43
+ latencyBudgetMs?: number;
42
44
  provider: TProvider;
43
45
  providerHealth?: VoiceProviderRouterProviderHealth<TProvider>;
44
46
  rateLimited?: boolean;
@@ -47,6 +49,7 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
47
49
  suppressionRemainingMs?: number;
48
50
  suppressedUntil?: number;
49
51
  status: 'error' | 'fallback' | 'success';
52
+ timedOut?: boolean;
50
53
  };
51
54
  export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
52
55
  export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | {
@@ -58,6 +61,7 @@ export type VoiceProviderRouterProviderProfile = {
58
61
  cost?: number;
59
62
  latencyMs?: number;
60
63
  priority?: number;
64
+ timeoutMs?: number;
61
65
  };
62
66
  export type VoiceProviderRouterHealthOptions = {
63
67
  cooldownMs?: number;
@@ -79,10 +83,12 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
79
83
  fallbackMode?: VoiceProviderRouterFallbackMode;
80
84
  isProviderError?: (error: unknown, provider: TProvider) => boolean;
81
85
  isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
86
+ isTimeoutError?: (error: unknown, provider: TProvider) => boolean;
82
87
  onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
83
88
  policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
84
89
  providerHealth?: boolean | VoiceProviderRouterHealthOptions;
85
90
  providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
91
+ timeoutMs?: number;
86
92
  providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
87
93
  selectProvider?: (input: VoiceAgentModelInput<TContext, TSession>) => TProvider | undefined | Promise<TProvider | undefined>;
88
94
  };
@@ -15,6 +15,7 @@ export type VoiceProviderHealthSummary<TProvider extends string = string> = {
15
15
  status: VoiceProviderHealthStatus;
16
16
  suppressionRemainingMs?: number;
17
17
  suppressedUntil?: number;
18
+ timeoutCount: number;
18
19
  };
19
20
  export type VoiceProviderHealthSummaryOptions<TProvider extends string = string> = {
20
21
  events?: StoredVoiceTraceEvent[];
@@ -3601,6 +3601,17 @@ var parseJSONValue = (value) => {
3601
3601
  return value;
3602
3602
  }
3603
3603
  };
3604
+
3605
+ class VoiceProviderTimeoutError extends Error {
3606
+ provider;
3607
+ timeoutMs;
3608
+ constructor(provider, timeoutMs) {
3609
+ super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
3610
+ this.name = "VoiceProviderTimeoutError";
3611
+ this.provider = provider;
3612
+ this.timeoutMs = timeoutMs;
3613
+ }
3614
+ }
3604
3615
  var getMessageToolCalls = (message) => {
3605
3616
  const toolCalls = message.metadata?.toolCalls;
3606
3617
  return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
@@ -3678,6 +3689,10 @@ var createVoiceProviderRouter = (options) => {
3678
3689
  const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
3679
3690
  const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
3680
3691
  const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
3692
+ const getProviderTimeoutMs = (provider) => {
3693
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
3694
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
3695
+ };
3681
3696
  const getHealth = (provider) => {
3682
3697
  const existing = healthState.get(provider);
3683
3698
  if (existing) {
@@ -3789,6 +3804,25 @@ var createVoiceProviderRouter = (options) => {
3789
3804
  const emit = async (event, input) => {
3790
3805
  await options.onProviderEvent?.(event, input);
3791
3806
  };
3807
+ const runProvider = async (provider, model, input) => {
3808
+ const timeoutMs = getProviderTimeoutMs(provider);
3809
+ if (!timeoutMs) {
3810
+ return model.generate(input);
3811
+ }
3812
+ let timeout;
3813
+ try {
3814
+ return await Promise.race([
3815
+ model.generate(input),
3816
+ new Promise((_, reject) => {
3817
+ timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
3818
+ })
3819
+ ]);
3820
+ } finally {
3821
+ if (timeout) {
3822
+ clearTimeout(timeout);
3823
+ }
3824
+ }
3825
+ };
3792
3826
  return {
3793
3827
  generate: async (input) => {
3794
3828
  const { order, selectedProvider } = await resolveOrder(input);
@@ -3803,12 +3837,14 @@ var createVoiceProviderRouter = (options) => {
3803
3837
  }
3804
3838
  const startedAt = Date.now();
3805
3839
  try {
3806
- const output = await model.generate(input);
3840
+ const output = await runProvider(provider, model, input);
3807
3841
  const providerHealth = recordProviderSuccess(provider);
3808
3842
  await emit({
3809
3843
  at: Date.now(),
3844
+ attempt: index + 1,
3810
3845
  elapsedMs: Date.now() - startedAt,
3811
3846
  fallbackProvider: provider === selectedProvider ? undefined : provider,
3847
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3812
3848
  provider,
3813
3849
  providerHealth,
3814
3850
  recovered: provider !== selectedProvider,
@@ -3820,22 +3856,26 @@ var createVoiceProviderRouter = (options) => {
3820
3856
  lastError = error;
3821
3857
  const hasNextProvider = index < order.length - 1;
3822
3858
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
3859
+ const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
3823
3860
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
3824
3861
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
3825
3862
  const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
3826
3863
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
3827
3864
  await emit({
3828
3865
  at: Date.now(),
3866
+ attempt: index + 1,
3829
3867
  elapsedMs: Date.now() - startedAt,
3830
3868
  error: errorMessage(error),
3831
3869
  fallbackProvider: shouldFallback ? nextProvider : undefined,
3870
+ latencyBudgetMs: getProviderTimeoutMs(provider),
3832
3871
  provider,
3833
3872
  providerHealth,
3834
3873
  rateLimited,
3835
3874
  selectedProvider,
3836
3875
  suppressionRemainingMs: getSuppressionRemainingMs(provider),
3837
3876
  suppressedUntil: providerHealth?.suppressedUntil,
3838
- status: "error"
3877
+ status: "error",
3878
+ timedOut
3839
3879
  }, input);
3840
3880
  if (!hasNextProvider || !shouldFallback) {
3841
3881
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.31",
3
+ "version": "0.0.22-beta.32",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",