@absolutejs/voice 0.0.22-beta.125 → 0.0.22-beta.126
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 +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +80 -2
- package/dist/productionReadiness.d.ts +12 -0
- package/dist/providerRoutingContract.d.ts +38 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1534,6 +1534,40 @@ const policy = resolveVoiceProviderRoutingPolicyPreset('cost-cap', {
|
|
|
1534
1534
|
});
|
|
1535
1535
|
```
|
|
1536
1536
|
|
|
1537
|
+
Use `runVoiceProviderRoutingContract(...)` when provider fallback needs to be certified before production. The contract reads provider routing trace events and verifies the expected selected provider, fallback provider, status, and kind in order.
|
|
1538
|
+
|
|
1539
|
+
```ts
|
|
1540
|
+
import { runVoiceProviderRoutingContract } from '@absolutejs/voice';
|
|
1541
|
+
|
|
1542
|
+
const report = await runVoiceProviderRoutingContract({
|
|
1543
|
+
store: runtime.traces,
|
|
1544
|
+
contract: {
|
|
1545
|
+
id: 'openai-to-anthropic-fallback',
|
|
1546
|
+
expect: [
|
|
1547
|
+
{
|
|
1548
|
+
kind: 'llm',
|
|
1549
|
+
provider: 'openai',
|
|
1550
|
+
selectedProvider: 'openai',
|
|
1551
|
+
fallbackProvider: 'anthropic',
|
|
1552
|
+
status: 'error'
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
kind: 'llm',
|
|
1556
|
+
provider: 'anthropic',
|
|
1557
|
+
selectedProvider: 'openai',
|
|
1558
|
+
status: 'fallback'
|
|
1559
|
+
}
|
|
1560
|
+
]
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
if (!report.pass) {
|
|
1565
|
+
throw new Error(report.issues.map((issue) => issue.message).join('\n'));
|
|
1566
|
+
}
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
Pass provider routing contract reports into production readiness through `providerRoutingContracts`. Readiness fails when a fallback contract fails, so model-routing regressions become deploy blockers instead of dashboard-only surprises.
|
|
1570
|
+
|
|
1537
1571
|
For full control, pass an object policy:
|
|
1538
1572
|
|
|
1539
1573
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, cr
|
|
|
26
26
|
export { createOpenAIVoiceTTS } from './openaiTTS';
|
|
27
27
|
export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
|
|
28
28
|
export { createVoiceProviderCapabilityHTMLHandler, createVoiceProviderCapabilityJSONHandler, createVoiceProviderCapabilityRoutes, renderVoiceProviderCapabilityHTML, summarizeVoiceProviderCapabilities } from './providerCapabilities';
|
|
29
|
+
export { assertVoiceProviderRoutingContract, runVoiceProviderRoutingContract } from './providerRoutingContract';
|
|
29
30
|
export { buildVoiceProductionReadinessReport, createVoiceProductionReadinessRoutes, renderVoiceProductionReadinessHTML } from './productionReadiness';
|
|
30
31
|
export { buildVoiceOpsConsoleReport, createVoiceOpsConsoleRoutes, renderVoiceOpsConsoleHTML } from './opsConsoleRoutes';
|
|
31
32
|
export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML } from './qualityRoutes';
|
|
@@ -71,6 +72,7 @@ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOpti
|
|
|
71
72
|
export type { OpenAIVoiceTTSOptions, OpenAIVoiceTTSVoice } from './openaiTTS';
|
|
72
73
|
export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
|
|
73
74
|
export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
|
|
75
|
+
export type { VoiceProviderRoutingContractDefinition, VoiceProviderRoutingContractIssue, VoiceProviderRoutingContractReport, VoiceProviderRoutingContractRunOptions, VoiceProviderRoutingExpectation, VoiceProviderRoutingStatus } from './providerRoutingContract';
|
|
74
76
|
export type { VoiceTurnLatencyHTMLHandlerOptions, VoiceTurnLatencyItem, VoiceTurnLatencyOptions, VoiceTurnLatencyReport, VoiceTurnLatencyRoutesOptions, VoiceTurnLatencyStage, VoiceTurnLatencyStatus } from './turnLatency';
|
|
75
77
|
export type { VoiceLiveLatencyOptions, VoiceLiveLatencyReport, VoiceLiveLatencyRoutesOptions, VoiceLiveLatencySample, VoiceLiveLatencyStatus } from './liveLatency';
|
|
76
78
|
export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
|
package/dist/index.js
CHANGED
|
@@ -9837,6 +9837,12 @@ var resolveAgentSquadContracts = async (options, input) => {
|
|
|
9837
9837
|
}
|
|
9838
9838
|
return typeof options.agentSquadContracts === "function" ? await options.agentSquadContracts(input) : options.agentSquadContracts;
|
|
9839
9839
|
};
|
|
9840
|
+
var resolveProviderRoutingContracts = async (options, input) => {
|
|
9841
|
+
if (options.providerRoutingContracts === false || options.providerRoutingContracts === undefined) {
|
|
9842
|
+
return;
|
|
9843
|
+
}
|
|
9844
|
+
return typeof options.providerRoutingContracts === "function" ? await options.providerRoutingContracts(input) : options.providerRoutingContracts;
|
|
9845
|
+
};
|
|
9840
9846
|
var summarizeLiveLatency = (events, options) => {
|
|
9841
9847
|
const warnAfterMs = options.liveLatencyWarnAfterMs ?? 1800;
|
|
9842
9848
|
const failAfterMs = options.liveLatencyFailAfterMs ?? 3200;
|
|
@@ -9859,7 +9865,15 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9859
9865
|
const routingEvents = listVoiceRoutingEvents(events);
|
|
9860
9866
|
const routingSessions = summarizeVoiceRoutingSessions(routingEvents);
|
|
9861
9867
|
const liveLatency = summarizeLiveLatency(events, options);
|
|
9862
|
-
const [
|
|
9868
|
+
const [
|
|
9869
|
+
quality,
|
|
9870
|
+
providers,
|
|
9871
|
+
sessions,
|
|
9872
|
+
handoffs,
|
|
9873
|
+
carriers,
|
|
9874
|
+
agentSquadContracts,
|
|
9875
|
+
providerRoutingContracts
|
|
9876
|
+
] = await Promise.all([
|
|
9863
9877
|
evaluateVoiceQuality({ events }),
|
|
9864
9878
|
Promise.all([
|
|
9865
9879
|
summarizeVoiceProviderHealth({
|
|
@@ -9878,7 +9892,8 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9878
9892
|
summarizeVoiceSessions({ events, status: "all" }),
|
|
9879
9893
|
summarizeVoiceHandoffHealth({ events }),
|
|
9880
9894
|
resolveCarriers(options, { query, request }),
|
|
9881
|
-
resolveAgentSquadContracts(options, { query, request })
|
|
9895
|
+
resolveAgentSquadContracts(options, { query, request }),
|
|
9896
|
+
resolveProviderRoutingContracts(options, { query, request })
|
|
9882
9897
|
]);
|
|
9883
9898
|
const degradedProviders = providers.filter((provider) => provider.status === "degraded" || provider.status === "rate-limited" || provider.status === "suppressed").length;
|
|
9884
9899
|
const failedSessions = sessions.filter((session) => session.status === "failed").length;
|
|
@@ -9993,6 +10008,12 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
9993
10008
|
status: agentSquadContracts.some((report) => !report.pass) ? "fail" : agentSquadContracts.length === 0 ? "warn" : "pass",
|
|
9994
10009
|
total: agentSquadContracts.length
|
|
9995
10010
|
} : undefined;
|
|
10011
|
+
const providerRoutingContractSummary = providerRoutingContracts ? {
|
|
10012
|
+
failed: providerRoutingContracts.filter((report) => !report.pass).length,
|
|
10013
|
+
passed: providerRoutingContracts.filter((report) => report.pass).length,
|
|
10014
|
+
status: providerRoutingContracts.some((report) => !report.pass) ? "fail" : providerRoutingContracts.length === 0 ? "warn" : "pass",
|
|
10015
|
+
total: providerRoutingContracts.length
|
|
10016
|
+
} : undefined;
|
|
9996
10017
|
if (agentSquadContractSummary) {
|
|
9997
10018
|
checks.push({
|
|
9998
10019
|
detail: agentSquadContractSummary.status === "pass" ? `${agentSquadContractSummary.passed} agent squad contract(s) are passing.` : agentSquadContractSummary.total === 0 ? "No agent squad contracts are configured." : `${agentSquadContractSummary.failed} agent squad contract(s) failed.`,
|
|
@@ -10009,6 +10030,22 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
10009
10030
|
]
|
|
10010
10031
|
});
|
|
10011
10032
|
}
|
|
10033
|
+
if (providerRoutingContractSummary) {
|
|
10034
|
+
checks.push({
|
|
10035
|
+
detail: providerRoutingContractSummary.status === "pass" ? `${providerRoutingContractSummary.passed} provider routing contract(s) are passing.` : providerRoutingContractSummary.total === 0 ? "No provider routing contracts are configured." : `${providerRoutingContractSummary.failed} provider routing contract(s) failed.`,
|
|
10036
|
+
href: options.links?.providerRoutingContracts ?? options.links?.resilience ?? "/resilience",
|
|
10037
|
+
label: "Provider routing contracts",
|
|
10038
|
+
status: providerRoutingContractSummary.status,
|
|
10039
|
+
value: `${providerRoutingContractSummary.passed}/${providerRoutingContractSummary.total}`,
|
|
10040
|
+
actions: providerRoutingContractSummary.status === "pass" ? [] : [
|
|
10041
|
+
{
|
|
10042
|
+
description: "Open provider routing evidence and inspect failed fallback expectations.",
|
|
10043
|
+
href: options.links?.providerRoutingContracts ?? options.links?.resilience ?? "/resilience",
|
|
10044
|
+
label: "Open provider routing contracts"
|
|
10045
|
+
}
|
|
10046
|
+
]
|
|
10047
|
+
});
|
|
10048
|
+
}
|
|
10012
10049
|
if (carriers && carrierSummary) {
|
|
10013
10050
|
checks.push({
|
|
10014
10051
|
detail: carrierSummary.status === "pass" ? "Configured carrier setup and contract checks are passing." : `${carrierSummary.failing} carrier(s) failing, ${carrierSummary.warnings} warning(s).`,
|
|
@@ -10034,6 +10071,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
10034
10071
|
handoffs: "/handoffs",
|
|
10035
10072
|
handoffRetry: "/api/voice-handoffs/retry",
|
|
10036
10073
|
liveLatency: "/traces",
|
|
10074
|
+
providerRoutingContracts: "/resilience",
|
|
10037
10075
|
quality: "/quality",
|
|
10038
10076
|
resilience: "/resilience",
|
|
10039
10077
|
sessions: "/sessions",
|
|
@@ -10052,6 +10090,7 @@ var buildVoiceProductionReadinessReport = async (options, input = {}) => {
|
|
|
10052
10090
|
degraded: degradedProviders,
|
|
10053
10091
|
total: providers.length
|
|
10054
10092
|
},
|
|
10093
|
+
providerRoutingContracts: providerRoutingContractSummary,
|
|
10055
10094
|
quality: {
|
|
10056
10095
|
status: quality.status
|
|
10057
10096
|
},
|
|
@@ -17066,6 +17105,43 @@ var createOpenAIVoiceTTS = (options) => {
|
|
|
17066
17105
|
}
|
|
17067
17106
|
};
|
|
17068
17107
|
};
|
|
17108
|
+
// src/providerRoutingContract.ts
|
|
17109
|
+
var isRoutingEvent = (event) => Boolean(event && typeof event === "object" && "status" in event && "kind" in event && "sessionId" in event);
|
|
17110
|
+
var normalizeEvents = (events) => (events.every(isRoutingEvent) ? [...events] : listVoiceRoutingEvents(events)).sort((left, right) => left.at - right.at);
|
|
17111
|
+
var matchesExpectation = (event, expectation) => (expectation.kind === undefined || event.kind === expectation.kind) && (expectation.operation === undefined || event.operation === expectation.operation) && (expectation.provider === undefined || event.provider === expectation.provider) && (expectation.selectedProvider === undefined || event.selectedProvider === expectation.selectedProvider) && (expectation.fallbackProvider === undefined || event.fallbackProvider === expectation.fallbackProvider) && (expectation.status === undefined || event.status === expectation.status);
|
|
17112
|
+
var describeExpectation = (expectation) => Object.entries(expectation).map(([key, value]) => `${key}=${String(value)}`).join(", ");
|
|
17113
|
+
var runVoiceProviderRoutingContract = async (options) => {
|
|
17114
|
+
const rawEvents = options.events ?? await options.store?.list() ?? [];
|
|
17115
|
+
const events = normalizeEvents(rawEvents).filter((event) => (!options.contract.sessionId || event.sessionId === options.contract.sessionId) && (!options.contract.scenarioId || event.sessionId === options.contract.scenarioId || rawEvents.some((rawEvent) => !isRoutingEvent(rawEvent) && rawEvent.sessionId === event.sessionId && rawEvent.scenarioId === options.contract.scenarioId)));
|
|
17116
|
+
const issues = [];
|
|
17117
|
+
let searchFrom = 0;
|
|
17118
|
+
for (const [index, expectation] of options.contract.expect.entries()) {
|
|
17119
|
+
const matchIndex = events.findIndex((event, eventIndex) => eventIndex >= searchFrom && matchesExpectation(event, expectation));
|
|
17120
|
+
if (matchIndex === -1) {
|
|
17121
|
+
issues.push({
|
|
17122
|
+
code: "provider_routing.expected_event_missing",
|
|
17123
|
+
message: `Expected provider routing event ${index + 1}: ${describeExpectation(expectation)}.`
|
|
17124
|
+
});
|
|
17125
|
+
continue;
|
|
17126
|
+
}
|
|
17127
|
+
searchFrom = matchIndex + 1;
|
|
17128
|
+
}
|
|
17129
|
+
return {
|
|
17130
|
+
contractId: options.contract.id,
|
|
17131
|
+
events,
|
|
17132
|
+
issues,
|
|
17133
|
+
pass: issues.length === 0,
|
|
17134
|
+
scenarioId: options.contract.scenarioId,
|
|
17135
|
+
sessionId: options.contract.sessionId
|
|
17136
|
+
};
|
|
17137
|
+
};
|
|
17138
|
+
var assertVoiceProviderRoutingContract = async (options) => {
|
|
17139
|
+
const report = await runVoiceProviderRoutingContract(options);
|
|
17140
|
+
if (!report.pass) {
|
|
17141
|
+
throw new Error(`Voice provider routing contract ${report.contractId} failed: ${report.issues.map((issue) => issue.message).join(" ")}`);
|
|
17142
|
+
}
|
|
17143
|
+
return report;
|
|
17144
|
+
};
|
|
17069
17145
|
// src/providerAdapters.ts
|
|
17070
17146
|
class VoiceIOProviderTimeoutError extends Error {
|
|
17071
17147
|
provider;
|
|
@@ -19965,6 +20041,7 @@ export {
|
|
|
19965
20041
|
runVoiceSessionEvals,
|
|
19966
20042
|
runVoiceScenarioFixtureEvals,
|
|
19967
20043
|
runVoiceScenarioEvals,
|
|
20044
|
+
runVoiceProviderRoutingContract,
|
|
19968
20045
|
runVoiceOutcomeContractSuite,
|
|
19969
20046
|
runVoiceCampaignProof,
|
|
19970
20047
|
runVoiceCampaignDialerProof,
|
|
@@ -20246,6 +20323,7 @@ export {
|
|
|
20246
20323
|
buildVoiceDiagnosticsMarkdown,
|
|
20247
20324
|
buildVoiceCampaignObservabilityReport,
|
|
20248
20325
|
assignVoiceOpsTask,
|
|
20326
|
+
assertVoiceProviderRoutingContract,
|
|
20249
20327
|
assertVoiceAgentSquadContract,
|
|
20250
20328
|
applyVoiceTelephonyOutcome,
|
|
20251
20329
|
applyVoiceOpsTaskPolicy,
|
|
@@ -2,6 +2,7 @@ import { Elysia } from 'elysia';
|
|
|
2
2
|
import { type VoiceTelephonyCarrierMatrixInput } from './telephony/matrix';
|
|
3
3
|
import type { VoiceTraceEventStore } from './trace';
|
|
4
4
|
import type { VoiceAgentSquadContractReport } from './agentSquadContract';
|
|
5
|
+
import type { VoiceProviderRoutingContractReport } from './providerRoutingContract';
|
|
5
6
|
export type VoiceProductionReadinessStatus = 'fail' | 'pass' | 'warn';
|
|
6
7
|
export type VoiceProductionReadinessAction = {
|
|
7
8
|
description?: string;
|
|
@@ -26,6 +27,7 @@ export type VoiceProductionReadinessReport = {
|
|
|
26
27
|
handoffs?: string;
|
|
27
28
|
handoffRetry?: string;
|
|
28
29
|
liveLatency?: string;
|
|
30
|
+
providerRoutingContracts?: string;
|
|
29
31
|
quality?: string;
|
|
30
32
|
resilience?: string;
|
|
31
33
|
sessions?: string;
|
|
@@ -60,6 +62,12 @@ export type VoiceProductionReadinessReport = {
|
|
|
60
62
|
degraded: number;
|
|
61
63
|
total: number;
|
|
62
64
|
};
|
|
65
|
+
providerRoutingContracts?: {
|
|
66
|
+
failed: number;
|
|
67
|
+
passed: number;
|
|
68
|
+
status: VoiceProductionReadinessStatus;
|
|
69
|
+
total: number;
|
|
70
|
+
};
|
|
63
71
|
quality: {
|
|
64
72
|
status: 'fail' | 'pass';
|
|
65
73
|
};
|
|
@@ -88,6 +96,10 @@ export type VoiceProductionReadinessRoutesOptions = {
|
|
|
88
96
|
llmProviders?: readonly string[];
|
|
89
97
|
name?: string;
|
|
90
98
|
path?: string;
|
|
99
|
+
providerRoutingContracts?: false | readonly VoiceProviderRoutingContractReport[] | ((input: {
|
|
100
|
+
query: Record<string, unknown>;
|
|
101
|
+
request: Request;
|
|
102
|
+
}) => Promise<readonly VoiceProviderRoutingContractReport[]> | readonly VoiceProviderRoutingContractReport[]);
|
|
91
103
|
render?: (report: VoiceProductionReadinessReport) => string | Promise<string>;
|
|
92
104
|
store: VoiceTraceEventStore;
|
|
93
105
|
sttProviders?: readonly string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type VoiceRoutingEvent, type VoiceRoutingEventKind } from './resilienceRoutes';
|
|
2
|
+
import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
|
|
3
|
+
export type VoiceProviderRoutingStatus = 'error' | 'fallback' | 'success';
|
|
4
|
+
export type VoiceProviderRoutingExpectation = {
|
|
5
|
+
fallbackProvider?: string;
|
|
6
|
+
kind?: VoiceRoutingEventKind;
|
|
7
|
+
operation?: string;
|
|
8
|
+
provider?: string;
|
|
9
|
+
selectedProvider?: string;
|
|
10
|
+
status?: VoiceProviderRoutingStatus;
|
|
11
|
+
};
|
|
12
|
+
export type VoiceProviderRoutingContractDefinition = {
|
|
13
|
+
description?: string;
|
|
14
|
+
expect: VoiceProviderRoutingExpectation[];
|
|
15
|
+
id: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
scenarioId?: string;
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
};
|
|
20
|
+
export type VoiceProviderRoutingContractIssue = {
|
|
21
|
+
code: string;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
export type VoiceProviderRoutingContractReport = {
|
|
25
|
+
contractId: string;
|
|
26
|
+
events: VoiceRoutingEvent[];
|
|
27
|
+
issues: VoiceProviderRoutingContractIssue[];
|
|
28
|
+
pass: boolean;
|
|
29
|
+
scenarioId?: string;
|
|
30
|
+
sessionId?: string;
|
|
31
|
+
};
|
|
32
|
+
export type VoiceProviderRoutingContractRunOptions = {
|
|
33
|
+
contract: VoiceProviderRoutingContractDefinition;
|
|
34
|
+
events?: StoredVoiceTraceEvent[] | VoiceRoutingEvent[];
|
|
35
|
+
store?: VoiceTraceEventStore;
|
|
36
|
+
};
|
|
37
|
+
export declare const runVoiceProviderRoutingContract: (options: VoiceProviderRoutingContractRunOptions) => Promise<VoiceProviderRoutingContractReport>;
|
|
38
|
+
export declare const assertVoiceProviderRoutingContract: (options: VoiceProviderRoutingContractRunOptions) => Promise<VoiceProviderRoutingContractReport>;
|