@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 +64 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +74 -4
- package/dist/modelAdapters.d.ts +17 -2
- package/dist/testing/index.js +73 -4
- package/package.json +1 -1
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
|
|
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,
|
package/dist/modelAdapters.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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;
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
|
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 : [
|