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

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
@@ -51,7 +51,7 @@ export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProvid
51
51
  export type { VoiceOpsConsoleLink, VoiceOpsConsoleReport, VoiceOpsConsoleRoutesOptions } from './opsConsoleRoutes';
52
52
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
53
53
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
54
- export type { VoiceIOProviderRouterEvent, VoiceSTTProviderRouterOptions, VoiceTTSProviderRouterOptions } from './providerAdapters';
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
@@ -10953,9 +10953,14 @@ var withTimeout = async (input) => {
10953
10953
  }
10954
10954
  }
10955
10955
  };
10956
+ var isVoiceProviderRoutingPolicyPreset = (policy) => policy === "balanced" || policy === "cost-cap" || policy === "cost-first" || policy === "latency-first" || policy === "quality-first";
10956
10957
  var createResolver = (options) => {
10957
10958
  const providerIds = Object.keys(options.adapters);
10958
10959
  const firstProvider = providerIds[0];
10960
+ const policy = typeof options.policy === "string" ? isVoiceProviderRoutingPolicyPreset(options.policy) ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
10961
+ strategy: options.policy
10962
+ } : options.policy;
10963
+ const strategy = policy?.strategy ?? "prefer-selected";
10959
10964
  const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
10960
10965
  const healthState = new Map;
10961
10966
  const now = () => healthOptions?.now?.() ?? Date.now();
@@ -11019,23 +11024,70 @@ var createResolver = (options) => {
11019
11024
  }
11020
11025
  return cloneHealth(provider);
11021
11026
  };
11027
+ const resolveAllowedProviders = async (input) => {
11028
+ const allowed = typeof policy?.allowProviders === "function" ? await policy.allowProviders(input) : policy?.allowProviders;
11029
+ return new Set(allowed ?? providerIds);
11030
+ };
11031
+ const passesBudgetFilters = (provider) => {
11032
+ const profile = options.providerProfiles?.[provider];
11033
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
11034
+ return false;
11035
+ }
11036
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
11037
+ return false;
11038
+ }
11039
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
11040
+ return false;
11041
+ }
11042
+ return true;
11043
+ };
11044
+ const getBalancedScore = (provider) => {
11045
+ const profile = options.providerProfiles?.[provider];
11046
+ if (policy?.scoreProvider) {
11047
+ return policy.scoreProvider(provider, profile);
11048
+ }
11049
+ const weights = policy?.weights ?? {};
11050
+ 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);
11051
+ };
11052
+ const sortProviders = (providers) => {
11053
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
11054
+ return providers;
11055
+ }
11056
+ return [...providers].sort((left, right) => {
11057
+ const leftProfile = options.providerProfiles?.[left];
11058
+ const rightProfile = options.providerProfiles?.[right];
11059
+ if (strategy === "quality-first") {
11060
+ 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);
11061
+ }
11062
+ if (strategy === "balanced") {
11063
+ return getBalancedScore(left) - getBalancedScore(right);
11064
+ }
11065
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11066
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
11067
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
11068
+ });
11069
+ };
11022
11070
  const resolveOrder = async (input) => {
11023
- const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
11071
+ const requestedProvider = await options.selectProvider?.(input);
11072
+ const selectedProvider = requestedProvider ?? firstProvider;
11073
+ const allowedProviders = await resolveAllowedProviders(input);
11024
11074
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
11025
11075
  const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
11026
11076
  const seen = new Set;
11027
- const rankedOrder = candidates.filter((provider) => {
11077
+ const orderedCandidates = candidates.filter((provider) => {
11028
11078
  if (!provider || seen.has(provider) || !options.adapters[provider]) {
11029
11079
  return false;
11030
11080
  }
11031
11081
  seen.add(provider);
11032
11082
  return true;
11033
11083
  });
11084
+ const rankedOrder = sortProviders(orderedCandidates).filter((provider) => allowedProviders.has(provider)).filter(passesBudgetFilters);
11034
11085
  const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
11035
11086
  const order = healthyOrder.length ? healthyOrder : rankedOrder;
11087
+ const preferred = strategy === "prefer-selected" && selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : order[0];
11036
11088
  return {
11037
11089
  order,
11038
- selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
11090
+ selectedProvider: preferred
11039
11091
  };
11040
11092
  };
11041
11093
  const emit = async (event, input) => {
@@ -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>;
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.57",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",