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

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.d.ts CHANGED
@@ -7,6 +7,7 @@ export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap
7
7
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
8
8
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
9
9
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
10
+ export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
10
11
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
11
12
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
12
13
  export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
@@ -36,6 +37,7 @@ export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssi
36
37
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
37
38
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
38
39
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
40
+ export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
39
41
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
40
42
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
41
43
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
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;
@@ -8812,6 +8858,288 @@ var createGeminiVoiceAssistantModel = (options) => {
8812
8858
  }
8813
8859
  };
8814
8860
  };
8861
+ // src/providerAdapters.ts
8862
+ class VoiceIOProviderTimeoutError extends Error {
8863
+ provider;
8864
+ timeoutMs;
8865
+ constructor(kind, provider, timeoutMs) {
8866
+ super(`Voice ${kind} provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
8867
+ this.name = "VoiceIOProviderTimeoutError";
8868
+ this.provider = provider;
8869
+ this.timeoutMs = timeoutMs;
8870
+ }
8871
+ }
8872
+ var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
8873
+ var createEmitter = () => {
8874
+ const listeners = new Map;
8875
+ return {
8876
+ emit: async (event, payload) => {
8877
+ await Promise.all([...listeners.get(event) ?? []].map((handler) => Promise.resolve(handler(payload))));
8878
+ },
8879
+ on: (event, handler) => {
8880
+ const set = listeners.get(event) ?? new Set;
8881
+ set.add(handler);
8882
+ listeners.set(event, set);
8883
+ return () => {
8884
+ set.delete(handler);
8885
+ };
8886
+ }
8887
+ };
8888
+ };
8889
+ var getTimeoutMs = (options, provider) => {
8890
+ const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
8891
+ return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
8892
+ };
8893
+ var withTimeout = async (input) => {
8894
+ if (!input.timeoutMs) {
8895
+ return input.run();
8896
+ }
8897
+ let timeout;
8898
+ try {
8899
+ return await Promise.race([
8900
+ Promise.resolve(input.run()),
8901
+ new Promise((_, reject) => {
8902
+ timeout = setTimeout(() => reject(new VoiceIOProviderTimeoutError(input.kind, input.provider, input.timeoutMs)), input.timeoutMs);
8903
+ })
8904
+ ]);
8905
+ } finally {
8906
+ if (timeout) {
8907
+ clearTimeout(timeout);
8908
+ }
8909
+ }
8910
+ };
8911
+ var createResolver = (options) => {
8912
+ const providerIds = Object.keys(options.adapters);
8913
+ const firstProvider = providerIds[0];
8914
+ const resolveOrder = async (input) => {
8915
+ const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
8916
+ const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
8917
+ const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
8918
+ const seen = new Set;
8919
+ const order = candidates.filter((provider) => {
8920
+ if (!provider || seen.has(provider) || !options.adapters[provider]) {
8921
+ return false;
8922
+ }
8923
+ seen.add(provider);
8924
+ return true;
8925
+ });
8926
+ return {
8927
+ order,
8928
+ selectedProvider
8929
+ };
8930
+ };
8931
+ const emit = async (event, input) => {
8932
+ await options.onProviderEvent?.(event, input);
8933
+ };
8934
+ return {
8935
+ emit,
8936
+ providerIds,
8937
+ resolveOrder
8938
+ };
8939
+ };
8940
+ var createVoiceSTTProviderRouter = (options) => {
8941
+ const resolver = createResolver(options);
8942
+ return {
8943
+ kind: "stt",
8944
+ open: async (input) => {
8945
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
8946
+ if (!selectedProvider || order.length === 0) {
8947
+ throw new Error("Voice STT provider router has no available providers.");
8948
+ }
8949
+ let lastError;
8950
+ for (const [index, provider] of order.entries()) {
8951
+ const adapter = options.adapters[provider];
8952
+ if (!adapter) {
8953
+ continue;
8954
+ }
8955
+ const startedAt = Date.now();
8956
+ try {
8957
+ const session = await withTimeout({
8958
+ kind: "stt",
8959
+ operation: "open",
8960
+ provider,
8961
+ run: () => adapter.open(input),
8962
+ timeoutMs: getTimeoutMs(options, provider)
8963
+ });
8964
+ await resolver.emit({
8965
+ at: Date.now(),
8966
+ attempt: index + 1,
8967
+ elapsedMs: Date.now() - startedAt,
8968
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
8969
+ kind: "stt",
8970
+ latencyBudgetMs: getTimeoutMs(options, provider),
8971
+ operation: "open",
8972
+ provider,
8973
+ selectedProvider,
8974
+ status: provider === selectedProvider ? "success" : "fallback"
8975
+ }, input);
8976
+ return session;
8977
+ } catch (error) {
8978
+ lastError = error;
8979
+ const hasNextProvider = index < order.length - 1;
8980
+ const shouldFallback = options.isProviderError?.(error, provider) ?? true;
8981
+ await resolver.emit({
8982
+ at: Date.now(),
8983
+ attempt: index + 1,
8984
+ elapsedMs: Date.now() - startedAt,
8985
+ error: errorMessage2(error),
8986
+ fallbackProvider: shouldFallback ? order[index + 1] : undefined,
8987
+ kind: "stt",
8988
+ latencyBudgetMs: getTimeoutMs(options, provider),
8989
+ operation: "open",
8990
+ provider,
8991
+ selectedProvider,
8992
+ status: "error",
8993
+ timedOut: error instanceof VoiceIOProviderTimeoutError
8994
+ }, input);
8995
+ if (!hasNextProvider || !shouldFallback) {
8996
+ throw error;
8997
+ }
8998
+ }
8999
+ }
9000
+ throw lastError ?? new Error("Voice STT provider router did not open a provider.");
9001
+ }
9002
+ };
9003
+ };
9004
+ var createVoiceTTSProviderRouter = (options) => {
9005
+ const resolver = createResolver(options);
9006
+ return {
9007
+ kind: "tts",
9008
+ open: async (input) => {
9009
+ const { order, selectedProvider } = await resolver.resolveOrder(input);
9010
+ if (!selectedProvider || order.length === 0) {
9011
+ throw new Error("Voice TTS provider router has no available providers.");
9012
+ }
9013
+ const emitter = createEmitter();
9014
+ let activeSession;
9015
+ let activeProvider;
9016
+ let nextProviderIndex = 0;
9017
+ const attach = (session) => {
9018
+ session.on("audio", (event) => emitter.emit("audio", event));
9019
+ session.on("error", (event) => emitter.emit("error", event));
9020
+ session.on("close", (event) => emitter.emit("close", event));
9021
+ };
9022
+ const openProvider = async (provider, attempt) => {
9023
+ const adapter = options.adapters[provider];
9024
+ if (!adapter) {
9025
+ throw new Error(`Voice TTS provider ${provider} is not configured.`);
9026
+ }
9027
+ const startedAt = Date.now();
9028
+ const session = await withTimeout({
9029
+ kind: "tts",
9030
+ operation: "open",
9031
+ provider,
9032
+ run: () => adapter.open(input),
9033
+ timeoutMs: getTimeoutMs(options, provider)
9034
+ });
9035
+ attach(session);
9036
+ activeSession = session;
9037
+ activeProvider = provider;
9038
+ await resolver.emit({
9039
+ at: Date.now(),
9040
+ attempt,
9041
+ elapsedMs: Date.now() - startedAt,
9042
+ fallbackProvider: provider === selectedProvider ? undefined : provider,
9043
+ kind: "tts",
9044
+ latencyBudgetMs: getTimeoutMs(options, provider),
9045
+ operation: "open",
9046
+ provider,
9047
+ selectedProvider,
9048
+ status: provider === selectedProvider ? "success" : "fallback"
9049
+ }, input);
9050
+ return session;
9051
+ };
9052
+ const failProvider = async (inputEvent) => {
9053
+ const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
9054
+ await resolver.emit({
9055
+ at: Date.now(),
9056
+ attempt: inputEvent.attempt,
9057
+ elapsedMs: Date.now() - inputEvent.startedAt,
9058
+ error: errorMessage2(inputEvent.error),
9059
+ fallbackProvider: shouldFallback ? order[nextProviderIndex] : undefined,
9060
+ kind: "tts",
9061
+ latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
9062
+ operation: inputEvent.operation,
9063
+ provider: inputEvent.provider,
9064
+ selectedProvider,
9065
+ status: "error",
9066
+ timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
9067
+ }, input);
9068
+ return shouldFallback;
9069
+ };
9070
+ for (const [index, provider] of order.entries()) {
9071
+ nextProviderIndex = index + 1;
9072
+ const startedAt = Date.now();
9073
+ try {
9074
+ await openProvider(provider, index + 1);
9075
+ break;
9076
+ } catch (error) {
9077
+ const shouldFallback = await failProvider({
9078
+ attempt: index + 1,
9079
+ error,
9080
+ operation: "open",
9081
+ provider,
9082
+ startedAt
9083
+ });
9084
+ if (!shouldFallback || index >= order.length - 1) {
9085
+ throw error;
9086
+ }
9087
+ }
9088
+ }
9089
+ if (!activeSession || !activeProvider) {
9090
+ throw new Error("Voice TTS provider router did not open a provider.");
9091
+ }
9092
+ const sendWithFallback = async (text) => {
9093
+ for (;; ) {
9094
+ const session = activeSession;
9095
+ const provider = activeProvider;
9096
+ if (!session || !provider) {
9097
+ throw new Error("Voice TTS provider router has no active provider.");
9098
+ }
9099
+ const startedAt = Date.now();
9100
+ try {
9101
+ await withTimeout({
9102
+ kind: "tts",
9103
+ operation: "send",
9104
+ provider,
9105
+ run: () => session.send(text),
9106
+ timeoutMs: getTimeoutMs(options, provider)
9107
+ });
9108
+ return;
9109
+ } catch (error) {
9110
+ const shouldFallback = await failProvider({
9111
+ attempt: nextProviderIndex,
9112
+ error,
9113
+ operation: "send",
9114
+ provider,
9115
+ startedAt
9116
+ });
9117
+ const nextProvider = order[nextProviderIndex];
9118
+ if (!shouldFallback || !nextProvider) {
9119
+ throw error;
9120
+ }
9121
+ nextProviderIndex += 1;
9122
+ await session.close("tts-provider-fallback").catch(() => {});
9123
+ await openProvider(nextProvider, nextProviderIndex);
9124
+ }
9125
+ }
9126
+ };
9127
+ return {
9128
+ close: async (reason) => {
9129
+ await activeSession?.close(reason);
9130
+ activeSession = undefined;
9131
+ activeProvider = undefined;
9132
+ await emitter.emit("close", {
9133
+ reason,
9134
+ type: "close"
9135
+ });
9136
+ },
9137
+ on: emitter.on,
9138
+ send: sendWithFallback
9139
+ };
9140
+ }
9141
+ };
9142
+ };
8815
9143
  // src/sqliteStore.ts
8816
9144
  import { Database } from "bun:sqlite";
8817
9145
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -10510,10 +10838,10 @@ var createVoiceOpsTaskProcessorWorker = (options) => ({
10510
10838
  result.completed += 1;
10511
10839
  } catch (error) {
10512
10840
  await options.onError?.(error, task);
10513
- const errorMessage2 = error instanceof Error ? error.message : String(error);
10841
+ const errorMessage3 = error instanceof Error ? error.message : String(error);
10514
10842
  const failedTask = failVoiceOpsTask(task, {
10515
10843
  actor: task.claimedBy ?? "ops-worker",
10516
- error: errorMessage2
10844
+ error: errorMessage3
10517
10845
  });
10518
10846
  if (shouldDeadLetterTask(failedTask, options.maxFailures)) {
10519
10847
  const deadLetterTask = deadLetterVoiceOpsTask(failedTask, {
@@ -11840,6 +12168,7 @@ export {
11840
12168
  createVoiceTaskUpdatedEvent,
11841
12169
  createVoiceTaskSLABreachedEvent,
11842
12170
  createVoiceTaskCreatedEvent,
12171
+ createVoiceTTSProviderRouter,
11843
12172
  createVoiceSessionsJSONHandler,
11844
12173
  createVoiceSessionsHTMLHandler,
11845
12174
  createVoiceSessionReplayRoutes,
@@ -11849,6 +12178,7 @@ export {
11849
12178
  createVoiceSessionListRoutes,
11850
12179
  createVoiceSession,
11851
12180
  createVoiceSTTRoutingCorrectionHandler,
12181
+ createVoiceSTTProviderRouter,
11852
12182
  createVoiceSQLiteTraceSinkDeliveryStore,
11853
12183
  createVoiceSQLiteTraceEventStore,
11854
12184
  createVoiceSQLiteTaskStore,
@@ -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
  };
@@ -0,0 +1,33 @@
1
+ import type { STTAdapter, STTAdapterOpenOptions, TTSAdapter, TTSAdapterOpenOptions } from './types';
2
+ import type { VoiceProviderRouterProviderProfile } from './modelAdapters';
3
+ type MaybePromise<T> = T | Promise<T>;
4
+ type VoiceIOProviderKind = 'stt' | 'tts';
5
+ type VoiceIOProviderStatus = 'error' | 'fallback' | 'success';
6
+ export type VoiceIOProviderRouterEvent<TProvider extends string = string> = {
7
+ at: number;
8
+ attempt: number;
9
+ elapsedMs: number;
10
+ error?: string;
11
+ fallbackProvider?: TProvider;
12
+ kind: VoiceIOProviderKind;
13
+ latencyBudgetMs?: number;
14
+ operation: 'open' | 'send';
15
+ provider: TProvider;
16
+ selectedProvider: TProvider;
17
+ status: VoiceIOProviderStatus;
18
+ timedOut?: boolean;
19
+ };
20
+ export type VoiceIOProviderRouterOptions<TProvider extends string, TAdapter, TOpenOptions> = {
21
+ adapters: Partial<Record<TProvider, TAdapter>>;
22
+ fallback?: readonly TProvider[] | ((input: TOpenOptions) => MaybePromise<readonly TProvider[]>);
23
+ isProviderError?: (error: unknown, provider: TProvider) => boolean;
24
+ onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>, input: TOpenOptions) => Promise<void> | void;
25
+ providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
26
+ selectProvider?: (input: TOpenOptions) => MaybePromise<TProvider | undefined>;
27
+ timeoutMs?: number;
28
+ };
29
+ export type VoiceSTTProviderRouterOptions<TProvider extends string = string, TOptions extends STTAdapterOpenOptions = STTAdapterOpenOptions> = VoiceIOProviderRouterOptions<TProvider, STTAdapter<TOptions>, TOptions>;
30
+ export type VoiceTTSProviderRouterOptions<TProvider extends string = string, TOptions extends TTSAdapterOpenOptions = TTSAdapterOpenOptions> = VoiceIOProviderRouterOptions<TProvider, TTSAdapter<TOptions>, TOptions>;
31
+ export declare const createVoiceSTTProviderRouter: <TProvider extends string = string, TOptions extends STTAdapterOpenOptions = STTAdapterOpenOptions>(options: VoiceSTTProviderRouterOptions<TProvider, TOptions>) => STTAdapter<TOptions>;
32
+ export declare const createVoiceTTSProviderRouter: <TProvider extends string = string, TOptions extends TTSAdapterOpenOptions = TTSAdapterOpenOptions>(options: VoiceTTSProviderRouterOptions<TProvider, TOptions>) => TTSAdapter<TOptions>;
33
+ export {};
@@ -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.33",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",