@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 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 [quality, providers, sessions, handoffs, carriers, agentSquadContracts] = await Promise.all([
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.125",
3
+ "version": "0.0.22-beta.126",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",