@absolutejs/voice 0.0.22-beta.55 → 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
@@ -1178,6 +1178,102 @@ app.use(
1178
1178
  - `benchmark-results/sessions-cheap-stt-runs-3.json`
1179
1179
  - `benchmark-results/stt-routing-run-manifest.json`
1180
1180
 
1181
+ ## LLM Provider Routing
1182
+
1183
+ Use `createVoiceProviderRouter(...)` when your assistant can run on more than one LLM provider. The router keeps provider choice inside your app: you define the available model adapters, profile each provider, and choose a policy.
1184
+
1185
+ ```ts
1186
+ import {
1187
+ createAnthropicVoiceAssistantModel,
1188
+ createGeminiVoiceAssistantModel,
1189
+ createOpenAIVoiceAssistantModel,
1190
+ createVoiceProviderRouter,
1191
+ resolveVoiceProviderRoutingPolicyPreset
1192
+ } from '@absolutejs/voice';
1193
+
1194
+ const model = createVoiceProviderRouter({
1195
+ providers: {
1196
+ openai: createOpenAIVoiceAssistantModel({ apiKey: process.env.OPENAI_API_KEY! }),
1197
+ anthropic: createAnthropicVoiceAssistantModel({ apiKey: process.env.ANTHROPIC_API_KEY! }),
1198
+ gemini: createGeminiVoiceAssistantModel({ apiKey: process.env.GEMINI_API_KEY! })
1199
+ },
1200
+ providerHealth: {
1201
+ failureThreshold: 1,
1202
+ cooldownMs: 30_000,
1203
+ rateLimitCooldownMs: 120_000
1204
+ },
1205
+ providerProfiles: {
1206
+ openai: { cost: 6, latencyMs: 650, quality: 0.92, timeoutMs: 3500 },
1207
+ anthropic: { cost: 7, latencyMs: 850, quality: 0.95, timeoutMs: 4500 },
1208
+ gemini: { cost: 2, latencyMs: 700, quality: 0.86, timeoutMs: 3500 }
1209
+ },
1210
+ policy: resolveVoiceProviderRoutingPolicyPreset('balanced')
1211
+ });
1212
+ ```
1213
+
1214
+ Built-in policy presets:
1215
+
1216
+ - `quality-first`: rank by `providerProfiles[provider].quality`, then priority, latency, and cost.
1217
+ - `latency-first`: rank by expected latency.
1218
+ - `cost-first`: rank by expected cost.
1219
+ - `cost-cap`: rank by cost and reject providers above `maxCost`.
1220
+ - `balanced`: weighted score using cost, latency, quality, and priority.
1221
+
1222
+ Budget filters are strict. If you pass `maxCost`, `maxLatencyMs`, or `minQuality`, providers outside those limits are removed before ranking, even if they were selected by the request.
1223
+
1224
+ ```ts
1225
+ const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
1226
+ maxCost: 3,
1227
+ minQuality: 0.82
1228
+ });
1229
+ ```
1230
+
1231
+ For full control, pass an object policy:
1232
+
1233
+ ```ts
1234
+ const model = createVoiceProviderRouter({
1235
+ providers,
1236
+ providerProfiles,
1237
+ policy: {
1238
+ strategy: 'balanced',
1239
+ maxLatencyMs: 1000,
1240
+ weights: { cost: 1, latencyMs: 0.004, quality: 12 }
1241
+ }
1242
+ });
1243
+ ```
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
+
1181
1277
  ## Presets
1182
1278
 
1183
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
@@ -9,7 +9,7 @@ export { createVoiceSessionListRoutes, createVoiceSessionReplayHTMLHandler, crea
9
9
  export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from './agent';
10
10
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
11
11
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
12
- export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
12
+ export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, resolveVoiceProviderRoutingPolicyPreset, createVoiceProviderRouter } from './modelAdapters';
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';
@@ -46,12 +46,12 @@ export type { VoiceDiagnosticsRoutesOptions } from './diagnosticsRoutes';
46
46
  export type { VoiceEvalBaselineComparison, VoiceEvalBaselineComparisonOptions, VoiceEvalBaselineStore, VoiceEvalBaselineSummary, VoiceEvalLink, VoiceEvalReport, VoiceEvalRoutesOptions, VoiceEvalSessionReport, VoiceEvalStatus, VoiceEvalTrendBucket, VoiceScenarioEvalDefinition, VoiceScenarioEvalReport, VoiceScenarioEvalResult, VoiceScenarioEvalSessionResult, VoiceScenarioFixture, VoiceScenarioFixtureEvalReport, VoiceScenarioFixtureEvalResult, VoiceScenarioFixtureStore } from './evalRoutes';
47
47
  export type { VoiceWorkflowContract, VoiceWorkflowContractDefinition, VoiceWorkflowContractField, VoiceWorkflowContractFieldMatch, VoiceWorkflowContractPresetName, VoiceWorkflowContractPresetOptions, VoiceWorkflowContractTracePayload, VoiceWorkflowContractValidation, VoiceWorkflowContractValidationIssue, VoiceWorkflowOutcome } from './workflowContract';
48
48
  export type { VoiceSessionListHTMLHandlerOptions, VoiceSessionListItem, VoiceSessionListOptions, VoiceSessionListRoutesOptions, VoiceSessionListStatus, VoiceSessionReplay, VoiceSessionReplayHTMLHandlerOptions, VoiceSessionReplayOptions, VoiceSessionReplayRoutesOptions, VoiceSessionReplayTurn } from './sessionReplay';
49
- export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
49
+ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterPolicyPreset, VoiceProviderRouterPolicyWeights, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceProviderRouterStrategy, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
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
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
@@ -10024,6 +10024,47 @@ var createStoredVoiceExternalObjectMap = (mapping) => createVoiceExternalObjectM
10024
10024
  sourceType: mapping.sourceType
10025
10025
  });
10026
10026
  // src/modelAdapters.ts
10027
+ var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
10028
+ switch (preset) {
10029
+ case "balanced":
10030
+ return {
10031
+ fallbackMode: "provider-error",
10032
+ strategy: "balanced",
10033
+ weights: {
10034
+ cost: 1,
10035
+ latencyMs: 0.005,
10036
+ priority: 1,
10037
+ quality: 10,
10038
+ ...options.weights
10039
+ },
10040
+ ...options
10041
+ };
10042
+ case "cost-cap":
10043
+ return {
10044
+ fallbackMode: "provider-error",
10045
+ strategy: "prefer-cheapest",
10046
+ ...options
10047
+ };
10048
+ case "cost-first":
10049
+ return {
10050
+ fallbackMode: "provider-error",
10051
+ strategy: "prefer-cheapest",
10052
+ ...options
10053
+ };
10054
+ case "latency-first":
10055
+ return {
10056
+ fallbackMode: "provider-error",
10057
+ strategy: "prefer-fastest",
10058
+ ...options
10059
+ };
10060
+ case "quality-first":
10061
+ return {
10062
+ fallbackMode: "provider-error",
10063
+ strategy: "quality-first",
10064
+ ...options
10065
+ };
10066
+ }
10067
+ };
10027
10068
  var OUTPUT_SCHEMA = {
10028
10069
  additionalProperties: false,
10029
10070
  properties: {
@@ -10191,7 +10232,7 @@ var createJSONVoiceAssistantModel = (options) => ({
10191
10232
  var createVoiceProviderRouter = (options) => {
10192
10233
  const providerIds = Object.keys(options.providers);
10193
10234
  const firstProvider = providerIds[0];
10194
- const policy = typeof options.policy === "string" ? {
10235
+ const policy = typeof options.policy === "string" ? options.policy === "balanced" || options.policy === "cost-cap" || options.policy === "cost-first" || options.policy === "latency-first" || options.policy === "quality-first" ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
10195
10236
  strategy: options.policy
10196
10237
  } : options.policy;
10197
10238
  const strategy = policy?.strategy ?? "prefer-selected";
@@ -10273,13 +10314,40 @@ var createVoiceProviderRouter = (options) => {
10273
10314
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
10274
10315
  return new Set(allowed ?? providerIds);
10275
10316
  };
10317
+ const passesBudgetFilters = (provider) => {
10318
+ const profile = options.providerProfiles?.[provider];
10319
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
10320
+ return false;
10321
+ }
10322
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
10323
+ return false;
10324
+ }
10325
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
10326
+ return false;
10327
+ }
10328
+ return true;
10329
+ };
10330
+ const getBalancedScore = (provider) => {
10331
+ const profile = options.providerProfiles?.[provider];
10332
+ if (policy?.scoreProvider) {
10333
+ return policy.scoreProvider(provider, profile);
10334
+ }
10335
+ const weights = policy?.weights ?? {};
10336
+ 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);
10337
+ };
10276
10338
  const sortProviders = (providers) => {
10277
- if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
10339
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
10278
10340
  return providers;
10279
10341
  }
10280
10342
  return [...providers].sort((left, right) => {
10281
10343
  const leftProfile = options.providerProfiles?.[left];
10282
10344
  const rightProfile = options.providerProfiles?.[right];
10345
+ if (strategy === "quality-first") {
10346
+ 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);
10347
+ }
10348
+ if (strategy === "balanced") {
10349
+ return getBalancedScore(left) - getBalancedScore(right);
10350
+ }
10283
10351
  const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
10284
10352
  const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
10285
10353
  return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
@@ -10289,12 +10357,13 @@ var createVoiceProviderRouter = (options) => {
10289
10357
  const selectedProvider = await options.selectProvider?.(input);
10290
10358
  const allowedProviders = await resolveAllowedProviders(input);
10291
10359
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
10292
- const rankedProviders = sortProviders([
10360
+ const allowedRankedProviders = sortProviders([
10293
10361
  ...fallbackOrder ?? providerIds
10294
10362
  ]).filter((provider) => allowedProviders.has(provider));
10363
+ const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
10295
10364
  const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
10296
10365
  const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
10297
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
10366
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
10298
10367
  const seen = new Set;
10299
10368
  const order = [];
10300
10369
  const candidates = strategy === "ordered" ? candidateRankedProviders : [
@@ -10884,9 +10953,14 @@ var withTimeout = async (input) => {
10884
10953
  }
10885
10954
  }
10886
10955
  };
10956
+ var isVoiceProviderRoutingPolicyPreset = (policy) => policy === "balanced" || policy === "cost-cap" || policy === "cost-first" || policy === "latency-first" || policy === "quality-first";
10887
10957
  var createResolver = (options) => {
10888
10958
  const providerIds = Object.keys(options.adapters);
10889
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";
10890
10964
  const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
10891
10965
  const healthState = new Map;
10892
10966
  const now = () => healthOptions?.now?.() ?? Date.now();
@@ -10950,23 +11024,70 @@ var createResolver = (options) => {
10950
11024
  }
10951
11025
  return cloneHealth(provider);
10952
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
+ };
10953
11070
  const resolveOrder = async (input) => {
10954
- 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);
10955
11074
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
10956
11075
  const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
10957
11076
  const seen = new Set;
10958
- const rankedOrder = candidates.filter((provider) => {
11077
+ const orderedCandidates = candidates.filter((provider) => {
10959
11078
  if (!provider || seen.has(provider) || !options.adapters[provider]) {
10960
11079
  return false;
10961
11080
  }
10962
11081
  seen.add(provider);
10963
11082
  return true;
10964
11083
  });
11084
+ const rankedOrder = sortProviders(orderedCandidates).filter((provider) => allowedProviders.has(provider)).filter(passesBudgetFilters);
10965
11085
  const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
10966
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];
10967
11088
  return {
10968
11089
  order,
10969
- selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
11090
+ selectedProvider: preferred
10970
11091
  };
10971
11092
  };
10972
11093
  const emit = async (event, input) => {
@@ -13978,6 +14099,7 @@ export {
13978
14099
  resolveVoiceTraceRedactionOptions,
13979
14100
  resolveVoiceSTTRoutingStrategy,
13980
14101
  resolveVoiceRuntimePreset,
14102
+ resolveVoiceProviderRoutingPolicyPreset,
13981
14103
  resolveVoiceOutcomeRecipe,
13982
14104
  resolveVoiceOpsTaskPolicy,
13983
14105
  resolveVoiceOpsTaskAssignment,
@@ -52,17 +52,32 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
52
52
  timedOut?: boolean;
53
53
  };
54
54
  export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
55
- export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | {
55
+ export type VoiceProviderRouterStrategy = 'balanced' | 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | 'quality-first';
56
+ export type VoiceProviderRouterPolicyPreset = 'balanced' | 'cost-cap' | 'cost-first' | 'latency-first' | 'quality-first';
57
+ export type VoiceProviderRouterPolicyWeights = {
58
+ cost?: number;
59
+ latencyMs?: number;
60
+ priority?: number;
61
+ quality?: number;
62
+ };
63
+ export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = VoiceProviderRouterStrategy | VoiceProviderRouterPolicyPreset | {
56
64
  allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
57
65
  fallbackMode?: VoiceProviderRouterFallbackMode;
58
- strategy?: 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected';
66
+ maxCost?: number;
67
+ maxLatencyMs?: number;
68
+ minQuality?: number;
69
+ scoreProvider?: (provider: TProvider, profile: VoiceProviderRouterProviderProfile | undefined) => number;
70
+ strategy?: VoiceProviderRouterStrategy;
71
+ weights?: VoiceProviderRouterPolicyWeights;
59
72
  };
60
73
  export type VoiceProviderRouterProviderProfile = {
61
74
  cost?: number;
62
75
  latencyMs?: number;
63
76
  priority?: number;
77
+ quality?: number;
64
78
  timeoutMs?: number;
65
79
  };
80
+ export declare const resolveVoiceProviderRoutingPolicyPreset: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string>(preset: VoiceProviderRouterPolicyPreset, options?: Omit<Extract<VoiceProviderRouterPolicy<TContext, TSession, TProvider>, Record<string, unknown>>, "strategy">) => Extract<VoiceProviderRouterPolicy<TContext, TSession, TProvider>, Record<string, unknown>>;
66
81
  export type VoiceProviderRouterHealthOptions = {
67
82
  cooldownMs?: number;
68
83
  failureThreshold?: number;
@@ -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>;
@@ -3628,6 +3628,47 @@ var createVoiceIOProviderFailureSimulator = (options) => {
3628
3628
  };
3629
3629
  };
3630
3630
  // src/modelAdapters.ts
3631
+ var resolveVoiceProviderRoutingPolicyPreset = (preset, options = {}) => {
3632
+ switch (preset) {
3633
+ case "balanced":
3634
+ return {
3635
+ fallbackMode: "provider-error",
3636
+ strategy: "balanced",
3637
+ weights: {
3638
+ cost: 1,
3639
+ latencyMs: 0.005,
3640
+ priority: 1,
3641
+ quality: 10,
3642
+ ...options.weights
3643
+ },
3644
+ ...options
3645
+ };
3646
+ case "cost-cap":
3647
+ return {
3648
+ fallbackMode: "provider-error",
3649
+ strategy: "prefer-cheapest",
3650
+ ...options
3651
+ };
3652
+ case "cost-first":
3653
+ return {
3654
+ fallbackMode: "provider-error",
3655
+ strategy: "prefer-cheapest",
3656
+ ...options
3657
+ };
3658
+ case "latency-first":
3659
+ return {
3660
+ fallbackMode: "provider-error",
3661
+ strategy: "prefer-fastest",
3662
+ ...options
3663
+ };
3664
+ case "quality-first":
3665
+ return {
3666
+ fallbackMode: "provider-error",
3667
+ strategy: "quality-first",
3668
+ ...options
3669
+ };
3670
+ }
3671
+ };
3631
3672
  var OUTPUT_SCHEMA = {
3632
3673
  additionalProperties: false,
3633
3674
  properties: {
@@ -3795,7 +3836,7 @@ var createJSONVoiceAssistantModel = (options) => ({
3795
3836
  var createVoiceProviderRouter = (options) => {
3796
3837
  const providerIds = Object.keys(options.providers);
3797
3838
  const firstProvider = providerIds[0];
3798
- const policy = typeof options.policy === "string" ? {
3839
+ const policy = typeof options.policy === "string" ? options.policy === "balanced" || options.policy === "cost-cap" || options.policy === "cost-first" || options.policy === "latency-first" || options.policy === "quality-first" ? resolveVoiceProviderRoutingPolicyPreset(options.policy) : {
3799
3840
  strategy: options.policy
3800
3841
  } : options.policy;
3801
3842
  const strategy = policy?.strategy ?? "prefer-selected";
@@ -3877,13 +3918,40 @@ var createVoiceProviderRouter = (options) => {
3877
3918
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
3878
3919
  return new Set(allowed ?? providerIds);
3879
3920
  };
3921
+ const passesBudgetFilters = (provider) => {
3922
+ const profile = options.providerProfiles?.[provider];
3923
+ if (typeof policy?.maxCost === "number" && typeof profile?.cost === "number" && profile.cost > policy.maxCost) {
3924
+ return false;
3925
+ }
3926
+ if (typeof policy?.maxLatencyMs === "number" && typeof profile?.latencyMs === "number" && profile.latencyMs > policy.maxLatencyMs) {
3927
+ return false;
3928
+ }
3929
+ if (typeof policy?.minQuality === "number" && typeof profile?.quality === "number" && profile.quality < policy.minQuality) {
3930
+ return false;
3931
+ }
3932
+ return true;
3933
+ };
3934
+ const getBalancedScore = (provider) => {
3935
+ const profile = options.providerProfiles?.[provider];
3936
+ if (policy?.scoreProvider) {
3937
+ return policy.scoreProvider(provider, profile);
3938
+ }
3939
+ const weights = policy?.weights ?? {};
3940
+ 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);
3941
+ };
3880
3942
  const sortProviders = (providers) => {
3881
- if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
3943
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest" && strategy !== "quality-first" && strategy !== "balanced") {
3882
3944
  return providers;
3883
3945
  }
3884
3946
  return [...providers].sort((left, right) => {
3885
3947
  const leftProfile = options.providerProfiles?.[left];
3886
3948
  const rightProfile = options.providerProfiles?.[right];
3949
+ if (strategy === "quality-first") {
3950
+ 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);
3951
+ }
3952
+ if (strategy === "balanced") {
3953
+ return getBalancedScore(left) - getBalancedScore(right);
3954
+ }
3887
3955
  const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3888
3956
  const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
3889
3957
  return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
@@ -3893,12 +3961,13 @@ var createVoiceProviderRouter = (options) => {
3893
3961
  const selectedProvider = await options.selectProvider?.(input);
3894
3962
  const allowedProviders = await resolveAllowedProviders(input);
3895
3963
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
3896
- const rankedProviders = sortProviders([
3964
+ const allowedRankedProviders = sortProviders([
3897
3965
  ...fallbackOrder ?? providerIds
3898
3966
  ]).filter((provider) => allowedProviders.has(provider));
3967
+ const rankedProviders = allowedRankedProviders.filter(passesBudgetFilters);
3899
3968
  const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
3900
3969
  const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
3901
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
3970
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && passesBudgetFilters(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
3902
3971
  const seen = new Set;
3903
3972
  const order = [];
3904
3973
  const candidates = strategy === "ordered" ? candidateRankedProviders : [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.55",
3
+ "version": "0.0.22-beta.57",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",