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

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,70 @@ 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
+
1181
1245
  ## Presets
1182
1246
 
1183
1247
  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,7 +46,7 @@ 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';
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 : [
@@ -13978,6 +14047,7 @@ export {
13978
14047
  resolveVoiceTraceRedactionOptions,
13979
14048
  resolveVoiceSTTRoutingStrategy,
13980
14049
  resolveVoiceRuntimePreset,
14050
+ resolveVoiceProviderRoutingPolicyPreset,
13981
14051
  resolveVoiceOutcomeRecipe,
13982
14052
  resolveVoiceOpsTaskPolicy,
13983
14053
  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;
@@ -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.56",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",