@absolutejs/voice 0.0.22-beta.56 → 0.0.22-beta.58

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/README.md CHANGED
@@ -1242,6 +1242,38 @@ const model = createVoiceProviderRouter({
1242
1242
  });
1243
1243
  ```
1244
1244
 
1245
+ The same profile and policy shape also works for STT and TTS provider routers, so a self-hosted app can choose the fastest provider for live calls, cap cost for background work, or require a minimum quality score without hard-coding provider branches.
1246
+
1247
+ ```ts
1248
+ const stt = createVoiceSTTProviderRouter({
1249
+ adapters: {
1250
+ deepgram,
1251
+ assemblyai
1252
+ },
1253
+ providerHealth: { cooldownMs: 30_000 },
1254
+ providerProfiles: {
1255
+ deepgram: { cost: 4, latencyMs: 180, quality: 0.93, timeoutMs: 1500 },
1256
+ assemblyai: { cost: 2, latencyMs: 650, quality: 0.88, timeoutMs: 3000 }
1257
+ },
1258
+ policy: resolveVoiceProviderRoutingPolicyPreset('latency-first')
1259
+ });
1260
+
1261
+ const tts = createVoiceTTSProviderRouter({
1262
+ adapters: {
1263
+ elevenlabs,
1264
+ openai
1265
+ },
1266
+ providerProfiles: {
1267
+ elevenlabs: { cost: 5, latencyMs: 220, quality: 0.94 },
1268
+ openai: { cost: 2, latencyMs: 320, quality: 0.87 }
1269
+ },
1270
+ policy: resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
1271
+ maxCost: 3,
1272
+ minQuality: 0.85
1273
+ })
1274
+ });
1275
+ ```
1276
+
1245
1277
  ## Presets
1246
1278
 
1247
1279
  Voice now ships named runtime presets so apps can start from a useful baseline instead of hand-tuning silence and capture settings every time.
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, cr
13
13
  export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
14
14
  export { buildVoiceOpsConsoleReport, createVoiceOpsConsoleRoutes, renderVoiceOpsConsoleHTML } from './opsConsoleRoutes';
15
15
  export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
16
- export { createVoiceResilienceRoutes, listVoiceRoutingEvents, renderVoiceResilienceHTML } from './resilienceRoutes';
16
+ export { createVoiceResilienceRoutes, createVoiceRoutingDecisionSummary, listVoiceRoutingEvents, renderVoiceResilienceHTML, summarizeVoiceRoutingDecision } from './resilienceRoutes';
17
17
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
18
18
  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';
19
19
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
@@ -50,8 +50,8 @@ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOpti
50
50
  export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
51
51
  export type { VoiceOpsConsoleLink, VoiceOpsConsoleReport, VoiceOpsConsoleRoutesOptions } from './opsConsoleRoutes';
52
52
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
53
- export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
54
- export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
53
+ export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
54
+ export type { VoiceIOProviderRouterEvent, VoiceIOProviderRouterOptions, VoiceIOProviderRouterPolicy, VoiceIOProviderRouterPolicyConfig, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
55
55
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
56
56
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
57
57
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
package/dist/index.js CHANGED
@@ -8493,15 +8493,37 @@ var listVoiceRoutingEvents = (events) => {
8493
8493
  latencyBudgetMs: getNumber4(event.payload.latencyBudgetMs),
8494
8494
  operation: getString7(event.payload.operation),
8495
8495
  provider,
8496
+ routing: getString7(event.payload.routing),
8496
8497
  selectedProvider: getString7(event.payload.selectedProvider),
8497
8498
  sessionId: event.sessionId,
8498
8499
  status: providerStatus,
8500
+ suppressionRemainingMs: getNumber4(event.payload.suppressionRemainingMs),
8499
8501
  timedOut: getBoolean2(event.payload.timedOut),
8500
8502
  turnId: event.turnId
8501
8503
  });
8502
8504
  }
8503
8505
  return routingEvents.sort((left, right) => right.at - left.at);
8504
8506
  };
8507
+ var summarizeVoiceRoutingDecision = (events, options = {}) => {
8508
+ const routingEvents = listVoiceRoutingEvents(events).filter((event) => {
8509
+ if (options.kind && event.kind !== options.kind) {
8510
+ return false;
8511
+ }
8512
+ if (options.sessionId && event.sessionId !== options.sessionId) {
8513
+ return false;
8514
+ }
8515
+ return true;
8516
+ });
8517
+ const limited = typeof options.limit === "number" && options.limit >= 0 ? routingEvents.slice(0, options.limit) : routingEvents;
8518
+ return limited[0] ?? null;
8519
+ };
8520
+ var createVoiceRoutingDecisionSummary = async (options) => {
8521
+ const events = await options.store.list({
8522
+ sessionId: options.sessionId,
8523
+ type: "session.error"
8524
+ });
8525
+ return summarizeVoiceRoutingDecision(events, options);
8526
+ };
8505
8527
  var summarizeRoutingEvents = (events) => {
8506
8528
  const byKind = new Map;
8507
8529
  let errors = 0;
@@ -10953,9 +10975,14 @@ var withTimeout = async (input) => {
10953
10975
  }
10954
10976
  }
10955
10977
  };
10978
+ var isVoiceProviderRoutingPolicyPreset = (policy) => policy === "balanced" || policy === "cost-cap" || policy === "cost-first" || policy === "latency-first" || policy === "quality-first";
10956
10979
  var createResolver = (options) => {
10957
10980
  const providerIds = Object.keys(options.adapters);
10958
10981
  const firstProvider = providerIds[0];
10982
+ const policy = typeof options.policy === "string" ? isVoiceProviderRoutingPolicyPreset(options.policy) ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
10983
+ strategy: options.policy
10984
+ } : options.policy;
10985
+ const strategy = policy?.strategy ?? "prefer-selected";
10959
10986
  const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
10960
10987
  const healthState = new Map;
10961
10988
  const now = () => healthOptions?.now?.() ?? Date.now();
@@ -11019,23 +11046,70 @@ var createResolver = (options) => {
11019
11046
  }
11020
11047
  return cloneHealth(provider);
11021
11048
  };
11049
+ const resolveAllowedProviders = async (input) => {
11050
+ const allowed = typeof policy?.allowProviders === "function" ? await policy.allowProviders(input) : policy?.allowProviders;
11051
+ return new Set(allowed ?? providerIds);
11052
+ };
11053
+ const passesBudgetFilters = (provider) => {
11054
+ const profile = options.providerProfiles?.[provider];
11055
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
11056
+ return false;
11057
+ }
11058
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
11059
+ return false;
11060
+ }
11061
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
11062
+ return false;
11063
+ }
11064
+ return true;
11065
+ };
11066
+ const getBalancedScore = (provider) => {
11067
+ const profile = options.providerProfiles?.[provider];
11068
+ if (policy?.scoreProvider) {
11069
+ return policy.scoreProvider(provider, profile);
11070
+ }
11071
+ const weights = policy?.weights ?? {};
11072
+ return (profile?.cost ?? Number.MAX_SAFE_INTEGER) * (weights.cost ?? 1) + (profile?.latencyMs ?? Number.MAX_SAFE_INTEGER) * (weights.latencyMs ?? 0.005) + (profile?.priority ?? 0) * (weights.priority ?? 1) - (profile?.quality ?? 0) * (weights.quality ?? 10);
11073
+ };
11074
+ const sortProviders = (providers) => {
11075
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
11076
+ return providers;
11077
+ }
11078
+ return [...providers].sort((left, right) => {
11079
+ const leftProfile = options.providerProfiles?.[left];
11080
+ const rightProfile = options.providerProfiles?.[right];
11081
+ if (strategy === "quality-first") {
11082
+ return (rightProfile?.quality ?? Number.MIN_SAFE_INTEGER) - (leftProfile?.quality ?? Number.MIN_SAFE_INTEGER) || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER) || (leftProfile?.cost ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.cost ?? Number.MAX_SAFE_INTEGER);
11083
+ }
11084
+ if (strategy === "balanced") {
11085
+ return getBalancedScore(left) - getBalancedScore(right);
11086
+ }
11087
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11088
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11089
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
11090
+ });
11091
+ };
11022
11092
  const resolveOrder = async (input) => {
11023
- const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
11093
+ const requestedProvider = await options.selectProvider?.(input);
11094
+ const selectedProvider = requestedProvider ?? firstProvider;
11095
+ const allowedProviders = await resolveAllowedProviders(input);
11024
11096
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
11025
11097
  const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
11026
11098
  const seen = new Set;
11027
- const rankedOrder = candidates.filter((provider) => {
11099
+ const orderedCandidates = candidates.filter((provider) => {
11028
11100
  if (!provider || seen.has(provider) || !options.adapters[provider]) {
11029
11101
  return false;
11030
11102
  }
11031
11103
  seen.add(provider);
11032
11104
  return true;
11033
11105
  });
11106
+ const rankedOrder = sortProviders(orderedCandidates).filter((provider) => allowedProviders.has(provider)).filter(passesBudgetFilters);
11034
11107
  const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
11035
11108
  const order = healthyOrder.length ? healthyOrder : rankedOrder;
11109
+ const preferred = strategy === "prefer-selected" && selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : order[0];
11036
11110
  return {
11037
11111
  order,
11038
- selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
11112
+ selectedProvider: preferred
11039
11113
  };
11040
11114
  };
11041
11115
  const emit = async (event, input) => {
@@ -14028,6 +14102,7 @@ export {
14028
14102
  summarizeVoiceTrace,
14029
14103
  summarizeVoiceSessions,
14030
14104
  summarizeVoiceSessionReplay,
14105
+ summarizeVoiceRoutingDecision,
14031
14106
  summarizeVoiceProviderHealth,
14032
14107
  summarizeVoiceOpsTasks,
14033
14108
  summarizeVoiceOpsTaskQueue,
@@ -14142,6 +14217,7 @@ export {
14142
14217
  createVoiceSQLiteIntegrationEventStore,
14143
14218
  createVoiceSQLiteExternalObjectMapStore,
14144
14219
  createVoiceS3ReviewStore,
14220
+ createVoiceRoutingDecisionSummary,
14145
14221
  createVoiceReviewSavedEvent,
14146
14222
  createVoiceResilienceRoutes,
14147
14223
  createVoiceRedisTaskLeaseCoordinator,
@@ -1,5 +1,5 @@
1
1
  import type { STTAdapter, STTAdapterOpenOptions, TTSAdapter, TTSAdapterOpenOptions } from './types';
2
- import type { VoiceProviderRouterHealthOptions, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile } from './modelAdapters';
2
+ import type { VoiceProviderRouterHealthOptions, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceProviderRouterPolicyPreset, VoiceProviderRouterPolicyWeights, VoiceProviderRouterStrategy } from './modelAdapters';
3
3
  type MaybePromise<T> = T | Promise<T>;
4
4
  type VoiceIOProviderKind = 'stt' | 'tts';
5
5
  type VoiceIOProviderStatus = 'error' | 'fallback' | 'success';
@@ -20,11 +20,22 @@ export type VoiceIOProviderRouterEvent<TProvider extends string = string> = {
20
20
  suppressedUntil?: number;
21
21
  timedOut?: boolean;
22
22
  };
23
+ export type VoiceIOProviderRouterPolicyConfig<TOpenOptions = unknown, TProvider extends string = string> = {
24
+ allowProviders?: readonly TProvider[] | ((input: TOpenOptions) => MaybePromise<readonly TProvider[]>);
25
+ maxCost?: number;
26
+ maxLatencyMs?: number;
27
+ minQuality?: number;
28
+ scoreProvider?: (provider: TProvider, profile: VoiceProviderRouterProviderProfile | undefined) => number;
29
+ strategy?: VoiceProviderRouterStrategy;
30
+ weights?: VoiceProviderRouterPolicyWeights;
31
+ };
32
+ export type VoiceIOProviderRouterPolicy<TOpenOptions = unknown, TProvider extends string = string> = VoiceProviderRouterStrategy | VoiceProviderRouterPolicyPreset | VoiceIOProviderRouterPolicyConfig<TOpenOptions, TProvider>;
23
33
  export type VoiceIOProviderRouterOptions<TProvider extends string, TAdapter, TOpenOptions> = {
24
34
  adapters: Partial<Record<TProvider, TAdapter>>;
25
35
  fallback?: readonly TProvider[] | ((input: TOpenOptions) => MaybePromise<readonly TProvider[]>);
26
36
  isProviderError?: (error: unknown, provider: TProvider) => boolean;
27
37
  onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>, input: TOpenOptions) => Promise<void> | void;
38
+ policy?: VoiceIOProviderRouterPolicy<TOpenOptions, TProvider>;
28
39
  providerHealth?: boolean | VoiceProviderRouterHealthOptions;
29
40
  providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
30
41
  selectProvider?: (input: TOpenOptions) => MaybePromise<TProvider | undefined>;
@@ -13,12 +13,21 @@ export type VoiceRoutingEvent = {
13
13
  latencyBudgetMs?: number;
14
14
  operation?: string;
15
15
  provider?: string;
16
+ routing?: string;
16
17
  selectedProvider?: string;
17
18
  sessionId: string;
18
19
  status?: string;
20
+ suppressionRemainingMs?: number;
19
21
  timedOut: boolean;
20
22
  turnId?: string;
21
23
  };
24
+ export type VoiceRoutingDecisionSummary = VoiceRoutingEvent;
25
+ export type VoiceRoutingDecisionSummaryOptions = {
26
+ kind?: VoiceRoutingEventKind;
27
+ limit?: number;
28
+ sessionId?: string;
29
+ store: VoiceTraceEventStore;
30
+ };
22
31
  export type VoiceResilienceLink = {
23
32
  href: string;
24
33
  label: string;
@@ -63,6 +72,8 @@ export type VoiceResilienceRoutesOptions = {
63
72
  ttsSimulation?: VoiceResilienceIOSimulator<string>;
64
73
  };
65
74
  export declare const listVoiceRoutingEvents: (events: StoredVoiceTraceEvent[]) => VoiceRoutingEvent[];
75
+ export declare const summarizeVoiceRoutingDecision: (events: StoredVoiceTraceEvent[], options?: Omit<VoiceRoutingDecisionSummaryOptions, "store">) => VoiceRoutingDecisionSummary | null;
76
+ export declare const createVoiceRoutingDecisionSummary: (options: VoiceRoutingDecisionSummaryOptions) => Promise<VoiceRoutingDecisionSummary | null>;
66
77
  export declare const renderVoiceResilienceHTML: (input: VoiceResiliencePageData) => string;
67
78
  export declare const createVoiceResilienceRoutes: (options: VoiceResilienceRoutesOptions) => Elysia<"", {
68
79
  decorator: {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.56",
3
+ "version": "0.0.22-beta.58",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",