@absolutejs/voice 0.0.22-beta.334 → 0.0.22-beta.335

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.
@@ -3821,7 +3821,7 @@ var maxNumber = (values) => {
3821
3821
  return finite.length > 0 ? Math.max(...finite) : undefined;
3822
3822
  };
3823
3823
  var readProofTrendMaxLiveP95 = (report) => report.summary.maxLiveP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.liveLatency?.p95Ms));
3824
- var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms;
3824
+ var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms ?? maxNumber((report.summary.providers ?? []).map((provider) => provider.p95Ms)) ?? maxNumber(report.cycles.flatMap((cycle) => (cycle.providers ?? []).map((provider) => provider.p95Ms)));
3825
3825
  var readProofTrendMaxTurnP95 = (report) => report.summary.maxTurnP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.turnLatency?.p95Ms));
3826
3826
  var readRuntimeChannelMetric = (report, key) => {
3827
3827
  const summaryValue = report.summary.runtimeChannel?.[key];
@@ -3839,6 +3839,91 @@ var readProofTrendRuntimeChannel = (report) => ({
3839
3839
  samples: report.summary.runtimeChannel?.samples ?? maxNumber(report.cycles.map((cycle) => cycle.runtimeChannel?.samples)),
3840
3840
  status: report.summary.runtimeChannel?.status
3841
3841
  });
3842
+ var normalizeProviderStatus = (status) => status === "pass" ? "pass" : status === "fail" ? "fail" : "warn";
3843
+ var providerSortScore = (provider) => [
3844
+ recommendationStatusRank[provider.status],
3845
+ provider.p95Ms ?? Number.POSITIVE_INFINITY,
3846
+ provider.averageMs ?? Number.POSITIVE_INFINITY,
3847
+ provider.samples === undefined ? Number.POSITIVE_INFINITY : -provider.samples,
3848
+ provider.id
3849
+ ];
3850
+ var compareProviders = (left, right) => {
3851
+ const leftScore = providerSortScore(left);
3852
+ const rightScore = providerSortScore(right);
3853
+ for (let index = 0;index < leftScore.length; index += 1) {
3854
+ const leftValue = leftScore[index];
3855
+ const rightValue = rightScore[index];
3856
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
3857
+ if (leftValue !== rightValue) {
3858
+ return leftValue - rightValue;
3859
+ }
3860
+ continue;
3861
+ }
3862
+ const compared = String(leftValue).localeCompare(String(rightValue));
3863
+ if (compared !== 0) {
3864
+ return compared;
3865
+ }
3866
+ }
3867
+ return 0;
3868
+ };
3869
+ var summarizeProofTrendProviders = (report, budgetMs) => {
3870
+ const sourceProviders = report.summary.providers && report.summary.providers.length > 0 ? report.summary.providers : undefined;
3871
+ const providersById = new Map;
3872
+ if (sourceProviders) {
3873
+ for (const provider of sourceProviders) {
3874
+ if (provider.id) {
3875
+ providersById.set(provider.id, provider);
3876
+ }
3877
+ }
3878
+ } else {
3879
+ for (const cycle of report.cycles) {
3880
+ for (const provider of cycle.providers ?? []) {
3881
+ if (!provider.id) {
3882
+ continue;
3883
+ }
3884
+ const existing = providersById.get(provider.id);
3885
+ providersById.set(provider.id, {
3886
+ averageMs: maxNumber([existing?.averageMs, provider.averageMs]),
3887
+ id: provider.id,
3888
+ label: existing?.label ?? provider.label,
3889
+ p50Ms: maxNumber([existing?.p50Ms, provider.p50Ms]),
3890
+ p95Ms: maxNumber([existing?.p95Ms, provider.p95Ms]),
3891
+ role: existing?.role ?? provider.role,
3892
+ samples: (existing?.samples ?? 0) + (provider.samples ?? 0),
3893
+ status: existing?.status === "fail" || provider.status === "fail" ? "fail" : existing?.status === "warn" || provider.status === "warn" ? "warn" : provider.status ?? existing?.status
3894
+ });
3895
+ }
3896
+ }
3897
+ }
3898
+ return [...providersById.values()].map((provider) => {
3899
+ const status = provider.p95Ms === undefined ? normalizeProviderStatus(provider.status) : withinBudget(provider.p95Ms, budgetMs) ? normalizeProviderStatus(provider.status) === "fail" ? "fail" : "pass" : normalizeProviderStatus(provider.status) === "fail" ? "fail" : "warn";
3900
+ return {
3901
+ averageMs: provider.averageMs,
3902
+ id: provider.id,
3903
+ label: provider.label,
3904
+ nextMove: status === "pass" ? "Eligible for latency-sensitive routing based on sustained proof." : provider.p95Ms === undefined ? "Collect provider-specific latency samples before routing latency-sensitive traffic here." : "Keep as fallback or tune provider/model/runtime budgets before using for latency-sensitive routing.",
3905
+ p50Ms: provider.p50Ms,
3906
+ p95Ms: provider.p95Ms,
3907
+ rank: 0,
3908
+ role: provider.role,
3909
+ samples: provider.samples,
3910
+ status
3911
+ };
3912
+ }).sort(compareProviders).map((provider, index) => ({ ...provider, rank: index + 1 }));
3913
+ };
3914
+ var shouldSwitchProvider = (current, best, options) => {
3915
+ if (!current || !best || current.id === best.id || best.status !== "pass") {
3916
+ return false;
3917
+ }
3918
+ if (current.p95Ms === undefined || best.p95Ms === undefined) {
3919
+ return false;
3920
+ }
3921
+ const minImprovementMs = options.providerSwitchMinImprovementMs ?? 100;
3922
+ const minImprovementRatio = options.providerSwitchMinImprovementRatio ?? 0.1;
3923
+ const improvementMs = current.p95Ms - best.p95Ms;
3924
+ const improvementRatio = current.p95Ms > 0 ? improvementMs / current.p95Ms : 0;
3925
+ return improvementMs >= minImprovementMs || improvementRatio >= minImprovementRatio;
3926
+ };
3842
3927
  var evaluateVoiceProofTrendEvidence = (report, input = {}) => {
3843
3928
  const issues = [];
3844
3929
  const requiredStatus = input.requireStatus ?? "pass";
@@ -3953,6 +4038,10 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
3953
4038
  const maxProviderP95Ms = readProofTrendMaxProviderP95(report);
3954
4039
  const maxTurnP95Ms = readProofTrendMaxTurnP95(report);
3955
4040
  const runtimeChannel = readProofTrendRuntimeChannel(report);
4041
+ const providers = summarizeProofTrendProviders(report, budgets.maxProviderP95Ms);
4042
+ const bestProvider = providers.find((provider) => provider.status === "pass") ?? providers[0];
4043
+ const currentProvider = options.currentProviderId ? providers.find((provider) => provider.id === options.currentProviderId) : undefined;
4044
+ const providerSwitchRecommended = shouldSwitchProvider(currentProvider, bestProvider, options);
3956
4045
  const recommendations = [];
3957
4046
  const issues = [];
3958
4047
  if (report.ok !== true) {
@@ -3960,12 +4049,19 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
3960
4049
  }
3961
4050
  recommendations.push({
3962
4051
  evidence: {
4052
+ bestProviderId: bestProvider?.id,
4053
+ bestProviderP95Ms: bestProvider?.p95Ms,
3963
4054
  budgetMs: budgets.maxProviderP95Ms,
4055
+ currentProviderId: currentProvider?.id ?? options.currentProviderId,
4056
+ currentProviderP95Ms: currentProvider?.p95Ms,
4057
+ providerComparisonCount: providers.length,
3964
4058
  providerP95Ms: maxProviderP95Ms
3965
4059
  },
3966
- nextMove: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
3967
- recommendation: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
3968
- status: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
4060
+ nextMove: providers.length > 0 ? providerSwitchRecommended ? `Route latency-sensitive turns to ${bestProvider?.label ?? bestProvider?.id} for this call profile and keep the current path as fallback.` : bestProvider ? `Use ${bestProvider.label ?? bestProvider.id} as the fastest proven provider path for this call profile and keep collecting sustained comparisons.` : "Collect provider-specific sustained samples before making provider-specific routing decisions." : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
4061
+ providerId: bestProvider?.id,
4062
+ recommendation: providers.length > 0 ? providerSwitchRecommended ? `Switch latency-sensitive routing to ${bestProvider?.label ?? bestProvider?.id}` : bestProvider ? `Prefer ${bestProvider.label ?? bestProvider.id} for this call profile` : "Collect provider-specific latency samples" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
4063
+ role: bestProvider?.role,
4064
+ status: providers.length > 0 ? providerSwitchRecommended ? "warn" : bestProvider?.status ?? "fail" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
3969
4065
  surface: "provider-path"
3970
4066
  });
3971
4067
  const runtimePass = withinBudget(runtimeChannel.maxFirstAudioLatencyMs, budgets.maxRuntimeFirstAudioLatencyMs) && withinBudget(runtimeChannel.maxInterruptionP95Ms, budgets.maxRuntimeInterruptionP95Ms) && withinBudget(runtimeChannel.maxJitterMs, budgets.maxRuntimeJitterMs) && withinBudget(runtimeChannel.maxTimestampDriftMs, budgets.maxRuntimeTimestampDriftMs) && withinBudget(runtimeChannel.maxBackpressureEvents, budgets.maxRuntimeBackpressureEvents);
@@ -4008,16 +4104,20 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
4008
4104
  });
4009
4105
  const status = issues.length > 0 ? "fail" : worstRecommendationStatus(recommendations);
4010
4106
  return {
4107
+ bestProvider,
4011
4108
  generatedAt: new Date().toISOString(),
4012
4109
  issues,
4013
4110
  ok: status !== "fail",
4111
+ providers,
4014
4112
  recommendations,
4015
4113
  source: report.source || report.outputDir || report.runId || "proof-trends",
4016
4114
  status,
4017
4115
  summary: {
4018
- keepCurrentProviderPath: recommendations.find((item) => item.surface === "provider-path")?.status === "pass",
4116
+ keepCurrentProviderPath: !providerSwitchRecommended && recommendations.find((item) => item.surface === "provider-path")?.status !== "fail",
4019
4117
  keepCurrentRuntimeChannel: recommendations.find((item) => item.surface === "runtime-channel")?.status === "pass",
4020
- recommendedActions: recommendations.filter((item) => item.status !== "pass").length
4118
+ providerComparisonCount: providers.length,
4119
+ recommendedActions: recommendations.filter((item) => item.status !== "pass").length,
4120
+ switchRecommended: providerSwitchRecommended
4021
4121
  }
4022
4122
  };
4023
4123
  };
@@ -4028,12 +4128,20 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
4028
4128
  "",
4029
4129
  `- Status: ${report.status}`,
4030
4130
  `- Source: ${report.source}`,
4131
+ `- Best provider: ${report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a"}`,
4132
+ `- Provider comparisons: ${String(report.summary.providerComparisonCount)}`,
4031
4133
  `- Recommended actions: ${String(report.summary.recommendedActions)}`,
4032
4134
  "",
4033
4135
  "| Surface | Status | Recommendation | Next move |",
4034
4136
  "| --- | --- | --- | --- |",
4035
4137
  ...report.recommendations.map((recommendation) => `| ${escapeMarkdown(recommendation.surface)} | ${recommendation.status} | ${escapeMarkdown(recommendation.recommendation)} | ${escapeMarkdown(recommendation.nextMove)} |`),
4036
4138
  "",
4139
+ "## Provider Comparison",
4140
+ "",
4141
+ "| Rank | Provider | Role | Status | P95 | Samples | Next move |",
4142
+ "| ---: | --- | --- | --- | ---: | ---: | --- |",
4143
+ ...report.providers.length ? report.providers.map((provider) => `| ${String(provider.rank)} | ${escapeMarkdown(provider.label ?? provider.id)} | ${escapeMarkdown(provider.role ?? "n/a")} | ${provider.status} | ${provider.p95Ms === undefined ? "n/a" : String(provider.p95Ms)} | ${provider.samples === undefined ? "n/a" : String(provider.samples)} | ${escapeMarkdown(provider.nextMove)} |`) : ["| n/a | n/a | n/a | n/a | n/a | n/a | No provider-specific samples were present. |"],
4144
+ "",
4037
4145
  "## Issues",
4038
4146
  "",
4039
4147
  ...report.issues.length ? report.issues.map((issue) => `- ${issue}`) : ["- None"]
@@ -4042,7 +4150,8 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
4042
4150
  var renderVoiceProofTrendRecommendationHTML = (report, title = "Voice Provider Runtime Recommendations") => {
4043
4151
  const cards = report.recommendations.map((recommendation) => `<article class="${escapeHtml3(recommendation.status)}"><p class="eyebrow">${escapeHtml3(recommendation.surface)} \xB7 ${escapeHtml3(recommendation.status)}</p><h2>${escapeHtml3(recommendation.recommendation)}</h2><p>${escapeHtml3(recommendation.nextMove)}</p><pre>${escapeHtml3(JSON.stringify(recommendation.evidence, null, 2))}</pre></article>`).join("");
4044
4152
  const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml3(issue)}</li>`).join("");
4045
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml3(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml3(title)}</h1><p>Generated ${escapeHtml3(report.generatedAt)} from ${escapeHtml3(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml3(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
4153
+ const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${escapeHtml3(provider.label ?? provider.id)}</strong><span>${escapeHtml3(provider.role ?? "provider")} \xB7 ${escapeHtml3(provider.status)} \xB7 p95 ${escapeHtml3(provider.p95Ms ?? "n/a")}ms \xB7 ${escapeHtml3(provider.samples ?? "n/a")} sample(s)</span><small>${escapeHtml3(provider.nextMove)}</small></li>`).join("");
4154
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml3(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}li{margin:.45rem 0}li span,li small{display:block;color:#c9d3ca}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml3(title)}</h1><p>Generated ${escapeHtml3(report.generatedAt)} from ${escapeHtml3(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml3(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Best ${escapeHtml3(report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a")}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Provider Comparison</h2><ul>${providerRows}</ul></section><section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
4046
4155
  };
4047
4156
  var createVoiceProofTrendRecommendationRoutes = (options) => {
4048
4157
  const path = options.path ?? "/api/voice/proof-trend-recommendations";
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export type { VoicePlatformCoverageAssertionInput, VoicePlatformCoverageAssertio
31
31
  export { assertVoiceProofTrendEvidence, buildEmptyVoiceProofTrendReport, buildVoiceProofTrendRecommendationReport, buildVoiceProofTrendReport, createVoiceProofTrendRecommendationRoutes, createVoiceProofTrendRoutes, DEFAULT_VOICE_PROOF_TRENDS_MAX_AGE_MS, evaluateVoiceProofTrendEvidence, formatVoiceProofTrendAge, normalizeVoiceProofTrendReport, readVoiceProofTrendReportFile, renderVoiceProofTrendRecommendationHTML, renderVoiceProofTrendRecommendationMarkdown } from './proofTrends';
32
32
  export { buildVoiceProviderDecisionTraceReport, createVoiceProviderDecisionTraceEvent, createVoiceProviderDecisionTraceRoutes, listVoiceProviderDecisionTraces, renderVoiceProviderDecisionTraceHTML, renderVoiceProviderDecisionTraceMarkdown } from './providerDecisionTraces';
33
33
  export type { VoiceProviderDecisionStatus, VoiceProviderDecisionSurfaceReport, VoiceProviderDecisionTrace, VoiceProviderDecisionTraceInput, VoiceProviderDecisionTraceIssue, VoiceProviderDecisionTraceReport, VoiceProviderDecisionTraceReportOptions, VoiceProviderDecisionTraceRoutesOptions } from './providerDecisionTraces';
34
- export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendStatus, VoiceProofTrendSummary } from './proofTrends';
34
+ export type { VoiceProofTrendAssertionInput, VoiceProofTrendAssertionReport, VoiceProofTrendCycle, VoiceProofTrendProviderRecommendation, VoiceProofTrendProviderSummary, VoiceProofTrendRecommendation, VoiceProofTrendRecommendationOptions, VoiceProofTrendRecommendationReport, VoiceProofTrendRecommendationRoutesOptions, VoiceProofTrendRecommendationStatus, VoiceProofTrendRecommendationSurface, VoiceProofTrendReport, VoiceProofTrendReportInput, VoiceProofTrendRoutesOptions, VoiceProofTrendStatus, VoiceProofTrendSummary } from './proofTrends';
35
35
  export { assertVoiceSloCalibration, buildVoiceSloCalibrationReport, buildVoiceSloReadinessThresholdReport, createVoiceSloReadinessThresholdOptions, createVoiceSloReadinessThresholdRoutes, createVoiceSloThresholdProfile, createVoiceSloCalibrationRoutes, renderVoiceSloCalibrationMarkdown, renderVoiceSloReadinessThresholdHTML, renderVoiceSloReadinessThresholdMarkdown } from './sloCalibration';
36
36
  export type { VoiceSloCalibrationMetricKey, VoiceSloCalibrationOptions, VoiceSloCalibrationReport, VoiceSloCalibrationRoutesOptions, VoiceSloCalibrationSample, VoiceSloCalibrationStatus, VoiceSloCalibrationThreshold, VoiceSloCalibrationThresholds, VoiceSloReadinessThresholdReport, VoiceSloReadinessThresholdReportOptions, VoiceSloReadinessThresholdOptions, VoiceSloReadinessThresholdRoutesOptions, VoiceSloThresholdProfile } from './sloCalibration';
37
37
  export { assertVoiceLiveOpsControlEvidence, assertVoiceLiveOpsEvidence, buildVoiceLiveOpsControlState, createVoiceLiveOpsController, createVoiceLiveOpsRoutes, createVoiceMemoryLiveOpsControlStore, evaluateVoiceLiveOpsControlEvidence, evaluateVoiceLiveOpsEvidence, getVoiceLiveOpsControlStatus, VOICE_LIVE_OPS_ACTIONS } from './liveOps';
package/dist/index.js CHANGED
@@ -14538,7 +14538,7 @@ var maxNumber = (values) => {
14538
14538
  return finite.length > 0 ? Math.max(...finite) : undefined;
14539
14539
  };
14540
14540
  var readProofTrendMaxLiveP95 = (report) => report.summary.maxLiveP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.liveLatency?.p95Ms));
14541
- var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms;
14541
+ var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms ?? maxNumber((report.summary.providers ?? []).map((provider) => provider.p95Ms)) ?? maxNumber(report.cycles.flatMap((cycle) => (cycle.providers ?? []).map((provider) => provider.p95Ms)));
14542
14542
  var readProofTrendMaxTurnP95 = (report) => report.summary.maxTurnP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.turnLatency?.p95Ms));
14543
14543
  var readRuntimeChannelMetric = (report, key) => {
14544
14544
  const summaryValue = report.summary.runtimeChannel?.[key];
@@ -14556,6 +14556,91 @@ var readProofTrendRuntimeChannel = (report) => ({
14556
14556
  samples: report.summary.runtimeChannel?.samples ?? maxNumber(report.cycles.map((cycle) => cycle.runtimeChannel?.samples)),
14557
14557
  status: report.summary.runtimeChannel?.status
14558
14558
  });
14559
+ var normalizeProviderStatus = (status) => status === "pass" ? "pass" : status === "fail" ? "fail" : "warn";
14560
+ var providerSortScore = (provider) => [
14561
+ recommendationStatusRank[provider.status],
14562
+ provider.p95Ms ?? Number.POSITIVE_INFINITY,
14563
+ provider.averageMs ?? Number.POSITIVE_INFINITY,
14564
+ provider.samples === undefined ? Number.POSITIVE_INFINITY : -provider.samples,
14565
+ provider.id
14566
+ ];
14567
+ var compareProviders = (left, right) => {
14568
+ const leftScore = providerSortScore(left);
14569
+ const rightScore = providerSortScore(right);
14570
+ for (let index = 0;index < leftScore.length; index += 1) {
14571
+ const leftValue = leftScore[index];
14572
+ const rightValue = rightScore[index];
14573
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
14574
+ if (leftValue !== rightValue) {
14575
+ return leftValue - rightValue;
14576
+ }
14577
+ continue;
14578
+ }
14579
+ const compared = String(leftValue).localeCompare(String(rightValue));
14580
+ if (compared !== 0) {
14581
+ return compared;
14582
+ }
14583
+ }
14584
+ return 0;
14585
+ };
14586
+ var summarizeProofTrendProviders = (report, budgetMs) => {
14587
+ const sourceProviders = report.summary.providers && report.summary.providers.length > 0 ? report.summary.providers : undefined;
14588
+ const providersById = new Map;
14589
+ if (sourceProviders) {
14590
+ for (const provider of sourceProviders) {
14591
+ if (provider.id) {
14592
+ providersById.set(provider.id, provider);
14593
+ }
14594
+ }
14595
+ } else {
14596
+ for (const cycle of report.cycles) {
14597
+ for (const provider of cycle.providers ?? []) {
14598
+ if (!provider.id) {
14599
+ continue;
14600
+ }
14601
+ const existing = providersById.get(provider.id);
14602
+ providersById.set(provider.id, {
14603
+ averageMs: maxNumber([existing?.averageMs, provider.averageMs]),
14604
+ id: provider.id,
14605
+ label: existing?.label ?? provider.label,
14606
+ p50Ms: maxNumber([existing?.p50Ms, provider.p50Ms]),
14607
+ p95Ms: maxNumber([existing?.p95Ms, provider.p95Ms]),
14608
+ role: existing?.role ?? provider.role,
14609
+ samples: (existing?.samples ?? 0) + (provider.samples ?? 0),
14610
+ status: existing?.status === "fail" || provider.status === "fail" ? "fail" : existing?.status === "warn" || provider.status === "warn" ? "warn" : provider.status ?? existing?.status
14611
+ });
14612
+ }
14613
+ }
14614
+ }
14615
+ return [...providersById.values()].map((provider) => {
14616
+ const status = provider.p95Ms === undefined ? normalizeProviderStatus(provider.status) : withinBudget(provider.p95Ms, budgetMs) ? normalizeProviderStatus(provider.status) === "fail" ? "fail" : "pass" : normalizeProviderStatus(provider.status) === "fail" ? "fail" : "warn";
14617
+ return {
14618
+ averageMs: provider.averageMs,
14619
+ id: provider.id,
14620
+ label: provider.label,
14621
+ nextMove: status === "pass" ? "Eligible for latency-sensitive routing based on sustained proof." : provider.p95Ms === undefined ? "Collect provider-specific latency samples before routing latency-sensitive traffic here." : "Keep as fallback or tune provider/model/runtime budgets before using for latency-sensitive routing.",
14622
+ p50Ms: provider.p50Ms,
14623
+ p95Ms: provider.p95Ms,
14624
+ rank: 0,
14625
+ role: provider.role,
14626
+ samples: provider.samples,
14627
+ status
14628
+ };
14629
+ }).sort(compareProviders).map((provider, index) => ({ ...provider, rank: index + 1 }));
14630
+ };
14631
+ var shouldSwitchProvider = (current, best, options) => {
14632
+ if (!current || !best || current.id === best.id || best.status !== "pass") {
14633
+ return false;
14634
+ }
14635
+ if (current.p95Ms === undefined || best.p95Ms === undefined) {
14636
+ return false;
14637
+ }
14638
+ const minImprovementMs = options.providerSwitchMinImprovementMs ?? 100;
14639
+ const minImprovementRatio = options.providerSwitchMinImprovementRatio ?? 0.1;
14640
+ const improvementMs = current.p95Ms - best.p95Ms;
14641
+ const improvementRatio = current.p95Ms > 0 ? improvementMs / current.p95Ms : 0;
14642
+ return improvementMs >= minImprovementMs || improvementRatio >= minImprovementRatio;
14643
+ };
14559
14644
  var evaluateVoiceProofTrendEvidence = (report, input = {}) => {
14560
14645
  const issues = [];
14561
14646
  const requiredStatus = input.requireStatus ?? "pass";
@@ -14670,6 +14755,10 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
14670
14755
  const maxProviderP95Ms = readProofTrendMaxProviderP95(report);
14671
14756
  const maxTurnP95Ms = readProofTrendMaxTurnP95(report);
14672
14757
  const runtimeChannel = readProofTrendRuntimeChannel(report);
14758
+ const providers = summarizeProofTrendProviders(report, budgets.maxProviderP95Ms);
14759
+ const bestProvider = providers.find((provider) => provider.status === "pass") ?? providers[0];
14760
+ const currentProvider = options.currentProviderId ? providers.find((provider) => provider.id === options.currentProviderId) : undefined;
14761
+ const providerSwitchRecommended = shouldSwitchProvider(currentProvider, bestProvider, options);
14673
14762
  const recommendations = [];
14674
14763
  const issues = [];
14675
14764
  if (report.ok !== true) {
@@ -14677,12 +14766,19 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
14677
14766
  }
14678
14767
  recommendations.push({
14679
14768
  evidence: {
14769
+ bestProviderId: bestProvider?.id,
14770
+ bestProviderP95Ms: bestProvider?.p95Ms,
14680
14771
  budgetMs: budgets.maxProviderP95Ms,
14772
+ currentProviderId: currentProvider?.id ?? options.currentProviderId,
14773
+ currentProviderP95Ms: currentProvider?.p95Ms,
14774
+ providerComparisonCount: providers.length,
14681
14775
  providerP95Ms: maxProviderP95Ms
14682
14776
  },
14683
- nextMove: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
14684
- recommendation: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
14685
- status: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
14777
+ nextMove: providers.length > 0 ? providerSwitchRecommended ? `Route latency-sensitive turns to ${bestProvider?.label ?? bestProvider?.id} for this call profile and keep the current path as fallback.` : bestProvider ? `Use ${bestProvider.label ?? bestProvider.id} as the fastest proven provider path for this call profile and keep collecting sustained comparisons.` : "Collect provider-specific sustained samples before making provider-specific routing decisions." : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
14778
+ providerId: bestProvider?.id,
14779
+ recommendation: providers.length > 0 ? providerSwitchRecommended ? `Switch latency-sensitive routing to ${bestProvider?.label ?? bestProvider?.id}` : bestProvider ? `Prefer ${bestProvider.label ?? bestProvider.id} for this call profile` : "Collect provider-specific latency samples" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
14780
+ role: bestProvider?.role,
14781
+ status: providers.length > 0 ? providerSwitchRecommended ? "warn" : bestProvider?.status ?? "fail" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
14686
14782
  surface: "provider-path"
14687
14783
  });
14688
14784
  const runtimePass = withinBudget(runtimeChannel.maxFirstAudioLatencyMs, budgets.maxRuntimeFirstAudioLatencyMs) && withinBudget(runtimeChannel.maxInterruptionP95Ms, budgets.maxRuntimeInterruptionP95Ms) && withinBudget(runtimeChannel.maxJitterMs, budgets.maxRuntimeJitterMs) && withinBudget(runtimeChannel.maxTimestampDriftMs, budgets.maxRuntimeTimestampDriftMs) && withinBudget(runtimeChannel.maxBackpressureEvents, budgets.maxRuntimeBackpressureEvents);
@@ -14725,16 +14821,20 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
14725
14821
  });
14726
14822
  const status = issues.length > 0 ? "fail" : worstRecommendationStatus(recommendations);
14727
14823
  return {
14824
+ bestProvider,
14728
14825
  generatedAt: new Date().toISOString(),
14729
14826
  issues,
14730
14827
  ok: status !== "fail",
14828
+ providers,
14731
14829
  recommendations,
14732
14830
  source: report.source || report.outputDir || report.runId || "proof-trends",
14733
14831
  status,
14734
14832
  summary: {
14735
- keepCurrentProviderPath: recommendations.find((item) => item.surface === "provider-path")?.status === "pass",
14833
+ keepCurrentProviderPath: !providerSwitchRecommended && recommendations.find((item) => item.surface === "provider-path")?.status !== "fail",
14736
14834
  keepCurrentRuntimeChannel: recommendations.find((item) => item.surface === "runtime-channel")?.status === "pass",
14737
- recommendedActions: recommendations.filter((item) => item.status !== "pass").length
14835
+ providerComparisonCount: providers.length,
14836
+ recommendedActions: recommendations.filter((item) => item.status !== "pass").length,
14837
+ switchRecommended: providerSwitchRecommended
14738
14838
  }
14739
14839
  };
14740
14840
  };
@@ -14745,12 +14845,20 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
14745
14845
  "",
14746
14846
  `- Status: ${report.status}`,
14747
14847
  `- Source: ${report.source}`,
14848
+ `- Best provider: ${report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a"}`,
14849
+ `- Provider comparisons: ${String(report.summary.providerComparisonCount)}`,
14748
14850
  `- Recommended actions: ${String(report.summary.recommendedActions)}`,
14749
14851
  "",
14750
14852
  "| Surface | Status | Recommendation | Next move |",
14751
14853
  "| --- | --- | --- | --- |",
14752
14854
  ...report.recommendations.map((recommendation) => `| ${escapeMarkdown2(recommendation.surface)} | ${recommendation.status} | ${escapeMarkdown2(recommendation.recommendation)} | ${escapeMarkdown2(recommendation.nextMove)} |`),
14753
14855
  "",
14856
+ "## Provider Comparison",
14857
+ "",
14858
+ "| Rank | Provider | Role | Status | P95 | Samples | Next move |",
14859
+ "| ---: | --- | --- | --- | ---: | ---: | --- |",
14860
+ ...report.providers.length ? report.providers.map((provider) => `| ${String(provider.rank)} | ${escapeMarkdown2(provider.label ?? provider.id)} | ${escapeMarkdown2(provider.role ?? "n/a")} | ${provider.status} | ${provider.p95Ms === undefined ? "n/a" : String(provider.p95Ms)} | ${provider.samples === undefined ? "n/a" : String(provider.samples)} | ${escapeMarkdown2(provider.nextMove)} |`) : ["| n/a | n/a | n/a | n/a | n/a | n/a | No provider-specific samples were present. |"],
14861
+ "",
14754
14862
  "## Issues",
14755
14863
  "",
14756
14864
  ...report.issues.length ? report.issues.map((issue) => `- ${issue}`) : ["- None"]
@@ -14759,7 +14867,8 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
14759
14867
  var renderVoiceProofTrendRecommendationHTML = (report, title = "Voice Provider Runtime Recommendations") => {
14760
14868
  const cards = report.recommendations.map((recommendation) => `<article class="${escapeHtml22(recommendation.status)}"><p class="eyebrow">${escapeHtml22(recommendation.surface)} \xB7 ${escapeHtml22(recommendation.status)}</p><h2>${escapeHtml22(recommendation.recommendation)}</h2><p>${escapeHtml22(recommendation.nextMove)}</p><pre>${escapeHtml22(JSON.stringify(recommendation.evidence, null, 2))}</pre></article>`).join("");
14761
14869
  const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml22(issue)}</li>`).join("");
14762
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml22(title)}</h1><p>Generated ${escapeHtml22(report.generatedAt)} from ${escapeHtml22(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml22(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
14870
+ const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${escapeHtml22(provider.label ?? provider.id)}</strong><span>${escapeHtml22(provider.role ?? "provider")} \xB7 ${escapeHtml22(provider.status)} \xB7 p95 ${escapeHtml22(provider.p95Ms ?? "n/a")}ms \xB7 ${escapeHtml22(provider.samples ?? "n/a")} sample(s)</span><small>${escapeHtml22(provider.nextMove)}</small></li>`).join("");
14871
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml22(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}li{margin:.45rem 0}li span,li small{display:block;color:#c9d3ca}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml22(title)}</h1><p>Generated ${escapeHtml22(report.generatedAt)} from ${escapeHtml22(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml22(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Best ${escapeHtml22(report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a")}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Provider Comparison</h2><ul>${providerRows}</ul></section><section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
14763
14872
  };
14764
14873
  var createVoiceProofTrendRecommendationRoutes = (options) => {
14765
14874
  const path = options.path ?? "/api/voice/proof-trend-recommendations";
@@ -4,9 +4,20 @@ export type VoiceProofTrendSummary = {
4
4
  cycles?: number;
5
5
  maxLiveP95Ms?: number;
6
6
  maxProviderP95Ms?: number;
7
+ providers?: VoiceProofTrendProviderSummary[];
7
8
  runtimeChannel?: VoiceProofTrendRuntimeChannelSummary;
8
9
  maxTurnP95Ms?: number;
9
10
  };
11
+ export type VoiceProofTrendProviderSummary = {
12
+ averageMs?: number;
13
+ id: string;
14
+ label?: string;
15
+ p50Ms?: number;
16
+ p95Ms?: number;
17
+ role?: string;
18
+ samples?: number;
19
+ status?: string;
20
+ };
10
21
  export type VoiceProofTrendRuntimeChannelSummary = {
11
22
  maxBackpressureEvents?: number;
12
23
  maxFirstAudioLatencyMs?: number;
@@ -36,6 +47,7 @@ export type VoiceProofTrendCycle = {
36
47
  eventsWithLatency?: number;
37
48
  status?: string;
38
49
  };
50
+ providers?: VoiceProofTrendProviderSummary[];
39
51
  runtimeChannel?: VoiceProofTrendRuntimeChannelSummary;
40
52
  turnLatency?: {
41
53
  p95Ms?: number;
@@ -113,24 +125,43 @@ export type VoiceProofTrendRecommendationSurface = 'live-latency' | 'provider-pa
113
125
  export type VoiceProofTrendRecommendation = {
114
126
  evidence: Record<string, number | string | undefined>;
115
127
  nextMove: string;
128
+ providerId?: string;
129
+ role?: string;
116
130
  recommendation: string;
117
131
  status: VoiceProofTrendRecommendationStatus;
118
132
  surface: VoiceProofTrendRecommendationSurface;
119
133
  };
134
+ export type VoiceProofTrendProviderRecommendation = {
135
+ averageMs?: number;
136
+ id: string;
137
+ label?: string;
138
+ nextMove: string;
139
+ p50Ms?: number;
140
+ p95Ms?: number;
141
+ rank: number;
142
+ role?: string;
143
+ samples?: number;
144
+ status: VoiceProofTrendRecommendationStatus;
145
+ };
120
146
  export type VoiceProofTrendRecommendationReport = {
147
+ bestProvider?: VoiceProofTrendProviderRecommendation;
121
148
  generatedAt: string;
122
149
  issues: string[];
123
150
  ok: boolean;
151
+ providers: VoiceProofTrendProviderRecommendation[];
124
152
  recommendations: VoiceProofTrendRecommendation[];
125
153
  source: string;
126
154
  status: VoiceProofTrendRecommendationStatus;
127
155
  summary: {
128
156
  keepCurrentProviderPath: boolean;
129
157
  keepCurrentRuntimeChannel: boolean;
158
+ providerComparisonCount: number;
130
159
  recommendedActions: number;
160
+ switchRecommended: boolean;
131
161
  };
132
162
  };
133
163
  export type VoiceProofTrendRecommendationOptions = {
164
+ currentProviderId?: string;
134
165
  maxLiveP95Ms?: number;
135
166
  maxProviderP95Ms?: number;
136
167
  maxRuntimeBackpressureEvents?: number;
@@ -139,6 +170,8 @@ export type VoiceProofTrendRecommendationOptions = {
139
170
  maxRuntimeJitterMs?: number;
140
171
  maxRuntimeTimestampDriftMs?: number;
141
172
  maxTurnP95Ms?: number;
173
+ providerSwitchMinImprovementMs?: number;
174
+ providerSwitchMinImprovementRatio?: number;
142
175
  };
143
176
  export type VoiceProofTrendRecommendationRoutesOptions = VoiceProofTrendRecommendationOptions & {
144
177
  headers?: HeadersInit;
@@ -1569,7 +1569,7 @@ var maxNumber = (values) => {
1569
1569
  return finite.length > 0 ? Math.max(...finite) : undefined;
1570
1570
  };
1571
1571
  var readProofTrendMaxLiveP95 = (report) => report.summary.maxLiveP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.liveLatency?.p95Ms));
1572
- var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms;
1572
+ var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms ?? maxNumber((report.summary.providers ?? []).map((provider) => provider.p95Ms)) ?? maxNumber(report.cycles.flatMap((cycle) => (cycle.providers ?? []).map((provider) => provider.p95Ms)));
1573
1573
  var readProofTrendMaxTurnP95 = (report) => report.summary.maxTurnP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.turnLatency?.p95Ms));
1574
1574
  var readRuntimeChannelMetric = (report, key) => {
1575
1575
  const summaryValue = report.summary.runtimeChannel?.[key];
@@ -1587,6 +1587,91 @@ var readProofTrendRuntimeChannel = (report) => ({
1587
1587
  samples: report.summary.runtimeChannel?.samples ?? maxNumber(report.cycles.map((cycle) => cycle.runtimeChannel?.samples)),
1588
1588
  status: report.summary.runtimeChannel?.status
1589
1589
  });
1590
+ var normalizeProviderStatus = (status) => status === "pass" ? "pass" : status === "fail" ? "fail" : "warn";
1591
+ var providerSortScore = (provider) => [
1592
+ recommendationStatusRank[provider.status],
1593
+ provider.p95Ms ?? Number.POSITIVE_INFINITY,
1594
+ provider.averageMs ?? Number.POSITIVE_INFINITY,
1595
+ provider.samples === undefined ? Number.POSITIVE_INFINITY : -provider.samples,
1596
+ provider.id
1597
+ ];
1598
+ var compareProviders = (left, right) => {
1599
+ const leftScore = providerSortScore(left);
1600
+ const rightScore = providerSortScore(right);
1601
+ for (let index = 0;index < leftScore.length; index += 1) {
1602
+ const leftValue = leftScore[index];
1603
+ const rightValue = rightScore[index];
1604
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
1605
+ if (leftValue !== rightValue) {
1606
+ return leftValue - rightValue;
1607
+ }
1608
+ continue;
1609
+ }
1610
+ const compared = String(leftValue).localeCompare(String(rightValue));
1611
+ if (compared !== 0) {
1612
+ return compared;
1613
+ }
1614
+ }
1615
+ return 0;
1616
+ };
1617
+ var summarizeProofTrendProviders = (report, budgetMs) => {
1618
+ const sourceProviders = report.summary.providers && report.summary.providers.length > 0 ? report.summary.providers : undefined;
1619
+ const providersById = new Map;
1620
+ if (sourceProviders) {
1621
+ for (const provider of sourceProviders) {
1622
+ if (provider.id) {
1623
+ providersById.set(provider.id, provider);
1624
+ }
1625
+ }
1626
+ } else {
1627
+ for (const cycle of report.cycles) {
1628
+ for (const provider of cycle.providers ?? []) {
1629
+ if (!provider.id) {
1630
+ continue;
1631
+ }
1632
+ const existing = providersById.get(provider.id);
1633
+ providersById.set(provider.id, {
1634
+ averageMs: maxNumber([existing?.averageMs, provider.averageMs]),
1635
+ id: provider.id,
1636
+ label: existing?.label ?? provider.label,
1637
+ p50Ms: maxNumber([existing?.p50Ms, provider.p50Ms]),
1638
+ p95Ms: maxNumber([existing?.p95Ms, provider.p95Ms]),
1639
+ role: existing?.role ?? provider.role,
1640
+ samples: (existing?.samples ?? 0) + (provider.samples ?? 0),
1641
+ status: existing?.status === "fail" || provider.status === "fail" ? "fail" : existing?.status === "warn" || provider.status === "warn" ? "warn" : provider.status ?? existing?.status
1642
+ });
1643
+ }
1644
+ }
1645
+ }
1646
+ return [...providersById.values()].map((provider) => {
1647
+ const status = provider.p95Ms === undefined ? normalizeProviderStatus(provider.status) : withinBudget(provider.p95Ms, budgetMs) ? normalizeProviderStatus(provider.status) === "fail" ? "fail" : "pass" : normalizeProviderStatus(provider.status) === "fail" ? "fail" : "warn";
1648
+ return {
1649
+ averageMs: provider.averageMs,
1650
+ id: provider.id,
1651
+ label: provider.label,
1652
+ nextMove: status === "pass" ? "Eligible for latency-sensitive routing based on sustained proof." : provider.p95Ms === undefined ? "Collect provider-specific latency samples before routing latency-sensitive traffic here." : "Keep as fallback or tune provider/model/runtime budgets before using for latency-sensitive routing.",
1653
+ p50Ms: provider.p50Ms,
1654
+ p95Ms: provider.p95Ms,
1655
+ rank: 0,
1656
+ role: provider.role,
1657
+ samples: provider.samples,
1658
+ status
1659
+ };
1660
+ }).sort(compareProviders).map((provider, index) => ({ ...provider, rank: index + 1 }));
1661
+ };
1662
+ var shouldSwitchProvider = (current, best, options) => {
1663
+ if (!current || !best || current.id === best.id || best.status !== "pass") {
1664
+ return false;
1665
+ }
1666
+ if (current.p95Ms === undefined || best.p95Ms === undefined) {
1667
+ return false;
1668
+ }
1669
+ const minImprovementMs = options.providerSwitchMinImprovementMs ?? 100;
1670
+ const minImprovementRatio = options.providerSwitchMinImprovementRatio ?? 0.1;
1671
+ const improvementMs = current.p95Ms - best.p95Ms;
1672
+ const improvementRatio = current.p95Ms > 0 ? improvementMs / current.p95Ms : 0;
1673
+ return improvementMs >= minImprovementMs || improvementRatio >= minImprovementRatio;
1674
+ };
1590
1675
  var evaluateVoiceProofTrendEvidence = (report, input = {}) => {
1591
1676
  const issues = [];
1592
1677
  const requiredStatus = input.requireStatus ?? "pass";
@@ -1701,6 +1786,10 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1701
1786
  const maxProviderP95Ms = readProofTrendMaxProviderP95(report);
1702
1787
  const maxTurnP95Ms = readProofTrendMaxTurnP95(report);
1703
1788
  const runtimeChannel = readProofTrendRuntimeChannel(report);
1789
+ const providers = summarizeProofTrendProviders(report, budgets.maxProviderP95Ms);
1790
+ const bestProvider = providers.find((provider) => provider.status === "pass") ?? providers[0];
1791
+ const currentProvider = options.currentProviderId ? providers.find((provider) => provider.id === options.currentProviderId) : undefined;
1792
+ const providerSwitchRecommended = shouldSwitchProvider(currentProvider, bestProvider, options);
1704
1793
  const recommendations = [];
1705
1794
  const issues = [];
1706
1795
  if (report.ok !== true) {
@@ -1708,12 +1797,19 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1708
1797
  }
1709
1798
  recommendations.push({
1710
1799
  evidence: {
1800
+ bestProviderId: bestProvider?.id,
1801
+ bestProviderP95Ms: bestProvider?.p95Ms,
1711
1802
  budgetMs: budgets.maxProviderP95Ms,
1803
+ currentProviderId: currentProvider?.id ?? options.currentProviderId,
1804
+ currentProviderP95Ms: currentProvider?.p95Ms,
1805
+ providerComparisonCount: providers.length,
1712
1806
  providerP95Ms: maxProviderP95Ms
1713
1807
  },
1714
- nextMove: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
1715
- recommendation: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
1716
- status: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
1808
+ nextMove: providers.length > 0 ? providerSwitchRecommended ? `Route latency-sensitive turns to ${bestProvider?.label ?? bestProvider?.id} for this call profile and keep the current path as fallback.` : bestProvider ? `Use ${bestProvider.label ?? bestProvider.id} as the fastest proven provider path for this call profile and keep collecting sustained comparisons.` : "Collect provider-specific sustained samples before making provider-specific routing decisions." : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
1809
+ providerId: bestProvider?.id,
1810
+ recommendation: providers.length > 0 ? providerSwitchRecommended ? `Switch latency-sensitive routing to ${bestProvider?.label ?? bestProvider?.id}` : bestProvider ? `Prefer ${bestProvider.label ?? bestProvider.id} for this call profile` : "Collect provider-specific latency samples" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
1811
+ role: bestProvider?.role,
1812
+ status: providers.length > 0 ? providerSwitchRecommended ? "warn" : bestProvider?.status ?? "fail" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
1717
1813
  surface: "provider-path"
1718
1814
  });
1719
1815
  const runtimePass = withinBudget(runtimeChannel.maxFirstAudioLatencyMs, budgets.maxRuntimeFirstAudioLatencyMs) && withinBudget(runtimeChannel.maxInterruptionP95Ms, budgets.maxRuntimeInterruptionP95Ms) && withinBudget(runtimeChannel.maxJitterMs, budgets.maxRuntimeJitterMs) && withinBudget(runtimeChannel.maxTimestampDriftMs, budgets.maxRuntimeTimestampDriftMs) && withinBudget(runtimeChannel.maxBackpressureEvents, budgets.maxRuntimeBackpressureEvents);
@@ -1756,16 +1852,20 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1756
1852
  });
1757
1853
  const status = issues.length > 0 ? "fail" : worstRecommendationStatus(recommendations);
1758
1854
  return {
1855
+ bestProvider,
1759
1856
  generatedAt: new Date().toISOString(),
1760
1857
  issues,
1761
1858
  ok: status !== "fail",
1859
+ providers,
1762
1860
  recommendations,
1763
1861
  source: report.source || report.outputDir || report.runId || "proof-trends",
1764
1862
  status,
1765
1863
  summary: {
1766
- keepCurrentProviderPath: recommendations.find((item) => item.surface === "provider-path")?.status === "pass",
1864
+ keepCurrentProviderPath: !providerSwitchRecommended && recommendations.find((item) => item.surface === "provider-path")?.status !== "fail",
1767
1865
  keepCurrentRuntimeChannel: recommendations.find((item) => item.surface === "runtime-channel")?.status === "pass",
1768
- recommendedActions: recommendations.filter((item) => item.status !== "pass").length
1866
+ providerComparisonCount: providers.length,
1867
+ recommendedActions: recommendations.filter((item) => item.status !== "pass").length,
1868
+ switchRecommended: providerSwitchRecommended
1769
1869
  }
1770
1870
  };
1771
1871
  };
@@ -1776,12 +1876,20 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
1776
1876
  "",
1777
1877
  `- Status: ${report.status}`,
1778
1878
  `- Source: ${report.source}`,
1879
+ `- Best provider: ${report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a"}`,
1880
+ `- Provider comparisons: ${String(report.summary.providerComparisonCount)}`,
1779
1881
  `- Recommended actions: ${String(report.summary.recommendedActions)}`,
1780
1882
  "",
1781
1883
  "| Surface | Status | Recommendation | Next move |",
1782
1884
  "| --- | --- | --- | --- |",
1783
1885
  ...report.recommendations.map((recommendation) => `| ${escapeMarkdown(recommendation.surface)} | ${recommendation.status} | ${escapeMarkdown(recommendation.recommendation)} | ${escapeMarkdown(recommendation.nextMove)} |`),
1784
1886
  "",
1887
+ "## Provider Comparison",
1888
+ "",
1889
+ "| Rank | Provider | Role | Status | P95 | Samples | Next move |",
1890
+ "| ---: | --- | --- | --- | ---: | ---: | --- |",
1891
+ ...report.providers.length ? report.providers.map((provider) => `| ${String(provider.rank)} | ${escapeMarkdown(provider.label ?? provider.id)} | ${escapeMarkdown(provider.role ?? "n/a")} | ${provider.status} | ${provider.p95Ms === undefined ? "n/a" : String(provider.p95Ms)} | ${provider.samples === undefined ? "n/a" : String(provider.samples)} | ${escapeMarkdown(provider.nextMove)} |`) : ["| n/a | n/a | n/a | n/a | n/a | n/a | No provider-specific samples were present. |"],
1892
+ "",
1785
1893
  "## Issues",
1786
1894
  "",
1787
1895
  ...report.issues.length ? report.issues.map((issue) => `- ${issue}`) : ["- None"]
@@ -1790,7 +1898,8 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
1790
1898
  var renderVoiceProofTrendRecommendationHTML = (report, title = "Voice Provider Runtime Recommendations") => {
1791
1899
  const cards = report.recommendations.map((recommendation) => `<article class="${escapeHtml5(recommendation.status)}"><p class="eyebrow">${escapeHtml5(recommendation.surface)} \xB7 ${escapeHtml5(recommendation.status)}</p><h2>${escapeHtml5(recommendation.recommendation)}</h2><p>${escapeHtml5(recommendation.nextMove)}</p><pre>${escapeHtml5(JSON.stringify(recommendation.evidence, null, 2))}</pre></article>`).join("");
1792
1900
  const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml5(issue)}</li>`).join("");
1793
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml5(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml5(title)}</h1><p>Generated ${escapeHtml5(report.generatedAt)} from ${escapeHtml5(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml5(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
1901
+ const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${escapeHtml5(provider.label ?? provider.id)}</strong><span>${escapeHtml5(provider.role ?? "provider")} \xB7 ${escapeHtml5(provider.status)} \xB7 p95 ${escapeHtml5(provider.p95Ms ?? "n/a")}ms \xB7 ${escapeHtml5(provider.samples ?? "n/a")} sample(s)</span><small>${escapeHtml5(provider.nextMove)}</small></li>`).join("");
1902
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml5(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}li{margin:.45rem 0}li span,li small{display:block;color:#c9d3ca}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml5(title)}</h1><p>Generated ${escapeHtml5(report.generatedAt)} from ${escapeHtml5(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml5(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Best ${escapeHtml5(report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a")}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Provider Comparison</h2><ul>${providerRows}</ul></section><section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
1794
1903
  };
1795
1904
  var createVoiceProofTrendRecommendationRoutes = (options) => {
1796
1905
  const path = options.path ?? "/api/voice/proof-trend-recommendations";
package/dist/vue/index.js CHANGED
@@ -1490,7 +1490,7 @@ var maxNumber = (values) => {
1490
1490
  return finite.length > 0 ? Math.max(...finite) : undefined;
1491
1491
  };
1492
1492
  var readProofTrendMaxLiveP95 = (report) => report.summary.maxLiveP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.liveLatency?.p95Ms));
1493
- var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms;
1493
+ var readProofTrendMaxProviderP95 = (report) => report.summary.maxProviderP95Ms ?? maxNumber((report.summary.providers ?? []).map((provider) => provider.p95Ms)) ?? maxNumber(report.cycles.flatMap((cycle) => (cycle.providers ?? []).map((provider) => provider.p95Ms)));
1494
1494
  var readProofTrendMaxTurnP95 = (report) => report.summary.maxTurnP95Ms ?? maxNumber(report.cycles.map((cycle) => cycle.turnLatency?.p95Ms));
1495
1495
  var readRuntimeChannelMetric = (report, key) => {
1496
1496
  const summaryValue = report.summary.runtimeChannel?.[key];
@@ -1508,6 +1508,91 @@ var readProofTrendRuntimeChannel = (report) => ({
1508
1508
  samples: report.summary.runtimeChannel?.samples ?? maxNumber(report.cycles.map((cycle) => cycle.runtimeChannel?.samples)),
1509
1509
  status: report.summary.runtimeChannel?.status
1510
1510
  });
1511
+ var normalizeProviderStatus = (status) => status === "pass" ? "pass" : status === "fail" ? "fail" : "warn";
1512
+ var providerSortScore = (provider) => [
1513
+ recommendationStatusRank[provider.status],
1514
+ provider.p95Ms ?? Number.POSITIVE_INFINITY,
1515
+ provider.averageMs ?? Number.POSITIVE_INFINITY,
1516
+ provider.samples === undefined ? Number.POSITIVE_INFINITY : -provider.samples,
1517
+ provider.id
1518
+ ];
1519
+ var compareProviders = (left, right) => {
1520
+ const leftScore = providerSortScore(left);
1521
+ const rightScore = providerSortScore(right);
1522
+ for (let index = 0;index < leftScore.length; index += 1) {
1523
+ const leftValue = leftScore[index];
1524
+ const rightValue = rightScore[index];
1525
+ if (typeof leftValue === "number" && typeof rightValue === "number") {
1526
+ if (leftValue !== rightValue) {
1527
+ return leftValue - rightValue;
1528
+ }
1529
+ continue;
1530
+ }
1531
+ const compared = String(leftValue).localeCompare(String(rightValue));
1532
+ if (compared !== 0) {
1533
+ return compared;
1534
+ }
1535
+ }
1536
+ return 0;
1537
+ };
1538
+ var summarizeProofTrendProviders = (report, budgetMs) => {
1539
+ const sourceProviders = report.summary.providers && report.summary.providers.length > 0 ? report.summary.providers : undefined;
1540
+ const providersById = new Map;
1541
+ if (sourceProviders) {
1542
+ for (const provider of sourceProviders) {
1543
+ if (provider.id) {
1544
+ providersById.set(provider.id, provider);
1545
+ }
1546
+ }
1547
+ } else {
1548
+ for (const cycle of report.cycles) {
1549
+ for (const provider of cycle.providers ?? []) {
1550
+ if (!provider.id) {
1551
+ continue;
1552
+ }
1553
+ const existing = providersById.get(provider.id);
1554
+ providersById.set(provider.id, {
1555
+ averageMs: maxNumber([existing?.averageMs, provider.averageMs]),
1556
+ id: provider.id,
1557
+ label: existing?.label ?? provider.label,
1558
+ p50Ms: maxNumber([existing?.p50Ms, provider.p50Ms]),
1559
+ p95Ms: maxNumber([existing?.p95Ms, provider.p95Ms]),
1560
+ role: existing?.role ?? provider.role,
1561
+ samples: (existing?.samples ?? 0) + (provider.samples ?? 0),
1562
+ status: existing?.status === "fail" || provider.status === "fail" ? "fail" : existing?.status === "warn" || provider.status === "warn" ? "warn" : provider.status ?? existing?.status
1563
+ });
1564
+ }
1565
+ }
1566
+ }
1567
+ return [...providersById.values()].map((provider) => {
1568
+ const status = provider.p95Ms === undefined ? normalizeProviderStatus(provider.status) : withinBudget(provider.p95Ms, budgetMs) ? normalizeProviderStatus(provider.status) === "fail" ? "fail" : "pass" : normalizeProviderStatus(provider.status) === "fail" ? "fail" : "warn";
1569
+ return {
1570
+ averageMs: provider.averageMs,
1571
+ id: provider.id,
1572
+ label: provider.label,
1573
+ nextMove: status === "pass" ? "Eligible for latency-sensitive routing based on sustained proof." : provider.p95Ms === undefined ? "Collect provider-specific latency samples before routing latency-sensitive traffic here." : "Keep as fallback or tune provider/model/runtime budgets before using for latency-sensitive routing.",
1574
+ p50Ms: provider.p50Ms,
1575
+ p95Ms: provider.p95Ms,
1576
+ rank: 0,
1577
+ role: provider.role,
1578
+ samples: provider.samples,
1579
+ status
1580
+ };
1581
+ }).sort(compareProviders).map((provider, index) => ({ ...provider, rank: index + 1 }));
1582
+ };
1583
+ var shouldSwitchProvider = (current, best, options) => {
1584
+ if (!current || !best || current.id === best.id || best.status !== "pass") {
1585
+ return false;
1586
+ }
1587
+ if (current.p95Ms === undefined || best.p95Ms === undefined) {
1588
+ return false;
1589
+ }
1590
+ const minImprovementMs = options.providerSwitchMinImprovementMs ?? 100;
1591
+ const minImprovementRatio = options.providerSwitchMinImprovementRatio ?? 0.1;
1592
+ const improvementMs = current.p95Ms - best.p95Ms;
1593
+ const improvementRatio = current.p95Ms > 0 ? improvementMs / current.p95Ms : 0;
1594
+ return improvementMs >= minImprovementMs || improvementRatio >= minImprovementRatio;
1595
+ };
1511
1596
  var evaluateVoiceProofTrendEvidence = (report, input = {}) => {
1512
1597
  const issues = [];
1513
1598
  const requiredStatus = input.requireStatus ?? "pass";
@@ -1622,6 +1707,10 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1622
1707
  const maxProviderP95Ms = readProofTrendMaxProviderP95(report);
1623
1708
  const maxTurnP95Ms = readProofTrendMaxTurnP95(report);
1624
1709
  const runtimeChannel = readProofTrendRuntimeChannel(report);
1710
+ const providers = summarizeProofTrendProviders(report, budgets.maxProviderP95Ms);
1711
+ const bestProvider = providers.find((provider) => provider.status === "pass") ?? providers[0];
1712
+ const currentProvider = options.currentProviderId ? providers.find((provider) => provider.id === options.currentProviderId) : undefined;
1713
+ const providerSwitchRecommended = shouldSwitchProvider(currentProvider, bestProvider, options);
1625
1714
  const recommendations = [];
1626
1715
  const issues = [];
1627
1716
  if (report.ok !== true) {
@@ -1629,12 +1718,19 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1629
1718
  }
1630
1719
  recommendations.push({
1631
1720
  evidence: {
1721
+ bestProviderId: bestProvider?.id,
1722
+ bestProviderP95Ms: bestProvider?.p95Ms,
1632
1723
  budgetMs: budgets.maxProviderP95Ms,
1724
+ currentProviderId: currentProvider?.id ?? options.currentProviderId,
1725
+ currentProviderP95Ms: currentProvider?.p95Ms,
1726
+ providerComparisonCount: providers.length,
1633
1727
  providerP95Ms: maxProviderP95Ms
1634
1728
  },
1635
- nextMove: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
1636
- recommendation: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
1637
- status: withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
1729
+ nextMove: providers.length > 0 ? providerSwitchRecommended ? `Route latency-sensitive turns to ${bestProvider?.label ?? bestProvider?.id} for this call profile and keep the current path as fallback.` : bestProvider ? `Use ${bestProvider.label ?? bestProvider.id} as the fastest proven provider path for this call profile and keep collecting sustained comparisons.` : "Collect provider-specific sustained samples before making provider-specific routing decisions." : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep the current provider route for latency-sensitive turns and keep collecting sustained proof." : "Route latency-sensitive turns to a faster provider profile or tighten fallback/circuit-breaker budgets before promotion.",
1730
+ providerId: bestProvider?.id,
1731
+ recommendation: providers.length > 0 ? providerSwitchRecommended ? `Switch latency-sensitive routing to ${bestProvider?.label ?? bestProvider?.id}` : bestProvider ? `Prefer ${bestProvider.label ?? bestProvider.id} for this call profile` : "Collect provider-specific latency samples" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "Keep current provider path" : "Change provider routing for latency-sensitive traffic",
1732
+ role: bestProvider?.role,
1733
+ status: providers.length > 0 ? providerSwitchRecommended ? "warn" : bestProvider?.status ?? "fail" : withinBudget(maxProviderP95Ms, budgets.maxProviderP95Ms) ? "pass" : maxProviderP95Ms === undefined ? "fail" : "warn",
1638
1734
  surface: "provider-path"
1639
1735
  });
1640
1736
  const runtimePass = withinBudget(runtimeChannel.maxFirstAudioLatencyMs, budgets.maxRuntimeFirstAudioLatencyMs) && withinBudget(runtimeChannel.maxInterruptionP95Ms, budgets.maxRuntimeInterruptionP95Ms) && withinBudget(runtimeChannel.maxJitterMs, budgets.maxRuntimeJitterMs) && withinBudget(runtimeChannel.maxTimestampDriftMs, budgets.maxRuntimeTimestampDriftMs) && withinBudget(runtimeChannel.maxBackpressureEvents, budgets.maxRuntimeBackpressureEvents);
@@ -1677,16 +1773,20 @@ var buildVoiceProofTrendRecommendationReport = (report, options = {}) => {
1677
1773
  });
1678
1774
  const status = issues.length > 0 ? "fail" : worstRecommendationStatus(recommendations);
1679
1775
  return {
1776
+ bestProvider,
1680
1777
  generatedAt: new Date().toISOString(),
1681
1778
  issues,
1682
1779
  ok: status !== "fail",
1780
+ providers,
1683
1781
  recommendations,
1684
1782
  source: report.source || report.outputDir || report.runId || "proof-trends",
1685
1783
  status,
1686
1784
  summary: {
1687
- keepCurrentProviderPath: recommendations.find((item) => item.surface === "provider-path")?.status === "pass",
1785
+ keepCurrentProviderPath: !providerSwitchRecommended && recommendations.find((item) => item.surface === "provider-path")?.status !== "fail",
1688
1786
  keepCurrentRuntimeChannel: recommendations.find((item) => item.surface === "runtime-channel")?.status === "pass",
1689
- recommendedActions: recommendations.filter((item) => item.status !== "pass").length
1787
+ providerComparisonCount: providers.length,
1788
+ recommendedActions: recommendations.filter((item) => item.status !== "pass").length,
1789
+ switchRecommended: providerSwitchRecommended
1690
1790
  }
1691
1791
  };
1692
1792
  };
@@ -1697,12 +1797,20 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
1697
1797
  "",
1698
1798
  `- Status: ${report.status}`,
1699
1799
  `- Source: ${report.source}`,
1800
+ `- Best provider: ${report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a"}`,
1801
+ `- Provider comparisons: ${String(report.summary.providerComparisonCount)}`,
1700
1802
  `- Recommended actions: ${String(report.summary.recommendedActions)}`,
1701
1803
  "",
1702
1804
  "| Surface | Status | Recommendation | Next move |",
1703
1805
  "| --- | --- | --- | --- |",
1704
1806
  ...report.recommendations.map((recommendation) => `| ${escapeMarkdown(recommendation.surface)} | ${recommendation.status} | ${escapeMarkdown(recommendation.recommendation)} | ${escapeMarkdown(recommendation.nextMove)} |`),
1705
1807
  "",
1808
+ "## Provider Comparison",
1809
+ "",
1810
+ "| Rank | Provider | Role | Status | P95 | Samples | Next move |",
1811
+ "| ---: | --- | --- | --- | ---: | ---: | --- |",
1812
+ ...report.providers.length ? report.providers.map((provider) => `| ${String(provider.rank)} | ${escapeMarkdown(provider.label ?? provider.id)} | ${escapeMarkdown(provider.role ?? "n/a")} | ${provider.status} | ${provider.p95Ms === undefined ? "n/a" : String(provider.p95Ms)} | ${provider.samples === undefined ? "n/a" : String(provider.samples)} | ${escapeMarkdown(provider.nextMove)} |`) : ["| n/a | n/a | n/a | n/a | n/a | n/a | No provider-specific samples were present. |"],
1813
+ "",
1706
1814
  "## Issues",
1707
1815
  "",
1708
1816
  ...report.issues.length ? report.issues.map((issue) => `- ${issue}`) : ["- None"]
@@ -1711,7 +1819,8 @@ var renderVoiceProofTrendRecommendationMarkdown = (report, title = "Voice Provid
1711
1819
  var renderVoiceProofTrendRecommendationHTML = (report, title = "Voice Provider Runtime Recommendations") => {
1712
1820
  const cards = report.recommendations.map((recommendation) => `<article class="${escapeHtml5(recommendation.status)}"><p class="eyebrow">${escapeHtml5(recommendation.surface)} \xB7 ${escapeHtml5(recommendation.status)}</p><h2>${escapeHtml5(recommendation.recommendation)}</h2><p>${escapeHtml5(recommendation.nextMove)}</p><pre>${escapeHtml5(JSON.stringify(recommendation.evidence, null, 2))}</pre></article>`).join("");
1713
1821
  const issues = report.issues.length === 0 ? "<li>None</li>" : report.issues.map((issue) => `<li>${escapeHtml5(issue)}</li>`).join("");
1714
- return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml5(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml5(title)}</h1><p>Generated ${escapeHtml5(report.generatedAt)} from ${escapeHtml5(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml5(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
1822
+ const providerRows = report.providers.length === 0 ? "<li>No provider-specific samples were present.</li>" : report.providers.map((provider) => `<li><strong>#${String(provider.rank)} ${escapeHtml5(provider.label ?? provider.id)}</strong><span>${escapeHtml5(provider.role ?? "provider")} \xB7 ${escapeHtml5(provider.status)} \xB7 p95 ${escapeHtml5(provider.p95Ms ?? "n/a")}ms \xB7 ${escapeHtml5(provider.samples ?? "n/a")} sample(s)</span><small>${escapeHtml5(provider.nextMove)}</small></li>`).join("");
1823
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>${escapeHtml5(title)}</title><style>body{background:#101418;color:#f7f3e8;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero,article{background:#17201d;border:1px solid #2e3d36;border-radius:24px;margin-bottom:16px;padding:22px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.18),rgba(245,158,11,.12))}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.1em;text-transform:uppercase}h1{font-size:clamp(2.2rem,6vw,4.7rem);letter-spacing:-.06em;line-height:.92;margin:.2rem 0 1rem}.summary{display:flex;flex-wrap:wrap;gap:10px}.pill{border:1px solid #42534a;border-radius:999px;padding:8px 12px}.pass{border-color:rgba(34,197,94,.55)}.warn{border-color:rgba(245,158,11,.7)}.fail{border-color:rgba(239,68,68,.75)}pre{background:#0b1110;border-radius:14px;overflow:auto;padding:12px}a{color:#5eead4}li{margin:.45rem 0}li span,li small{display:block;color:#c9d3ca}</style></head><body><main><section class="hero"><p class="eyebrow">Sustained proof recommendations</p><h1>${escapeHtml5(title)}</h1><p>Generated ${escapeHtml5(report.generatedAt)} from ${escapeHtml5(report.source)}.</p><div class="summary"><span class="pill">Status ${escapeHtml5(report.status)}</span><span class="pill">Provider ${report.summary.keepCurrentProviderPath ? "keep" : "change"}</span><span class="pill">Best ${escapeHtml5(report.bestProvider?.label ?? report.bestProvider?.id ?? "n/a")}</span><span class="pill">Runtime ${report.summary.keepCurrentRuntimeChannel ? "keep" : "tune"}</span><span class="pill">${String(report.summary.recommendedActions)} action(s)</span></div></section>${cards}<section class="hero"><h2>Provider Comparison</h2><ul>${providerRows}</ul></section><section class="hero"><h2>Issues</h2><ul>${issues}</ul></section></main></body></html>`;
1715
1824
  };
1716
1825
  var createVoiceProofTrendRecommendationRoutes = (options) => {
1717
1826
  const path = options.path ?? "/api/voice/proof-trend-recommendations";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.334",
3
+ "version": "0.0.22-beta.335",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",