@absolutejs/voice 0.0.22-beta.14 → 0.0.22-beta.16

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.
@@ -1,2 +1,3 @@
1
1
  export { VoiceStreamService } from './voice-stream.service';
2
2
  export { VoiceControllerService } from './voice-controller.service';
3
+ export { VoiceProviderStatusService } from './voice-provider-status.service';
@@ -1288,7 +1288,131 @@ VoiceControllerService = __decorateElement(_init, 0, "VoiceControllerService", _
1288
1288
  __runInitializers(_init, 1, VoiceControllerService);
1289
1289
  __decoratorMetadata(_init, VoiceControllerService);
1290
1290
  let _VoiceControllerService = VoiceControllerService;
1291
+ // src/angular/voice-provider-status.service.ts
1292
+ import { computed as computed3, Injectable as Injectable3, signal as signal3 } from "@angular/core";
1293
+
1294
+ // src/client/providerStatus.ts
1295
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
1296
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1297
+ const response = await fetchImpl(path);
1298
+ if (!response.ok) {
1299
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
1300
+ }
1301
+ return await response.json();
1302
+ };
1303
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
1304
+ const listeners = new Set;
1305
+ let closed = false;
1306
+ let timer;
1307
+ let snapshot = {
1308
+ error: null,
1309
+ isLoading: false,
1310
+ providers: []
1311
+ };
1312
+ const emit = () => {
1313
+ for (const listener of listeners) {
1314
+ listener();
1315
+ }
1316
+ };
1317
+ const refresh = async () => {
1318
+ if (closed) {
1319
+ return snapshot.providers;
1320
+ }
1321
+ snapshot = {
1322
+ ...snapshot,
1323
+ error: null,
1324
+ isLoading: true
1325
+ };
1326
+ emit();
1327
+ try {
1328
+ const providers = await fetchVoiceProviderStatus(path, options);
1329
+ snapshot = {
1330
+ error: null,
1331
+ isLoading: false,
1332
+ providers,
1333
+ updatedAt: Date.now()
1334
+ };
1335
+ emit();
1336
+ return providers;
1337
+ } catch (error) {
1338
+ snapshot = {
1339
+ ...snapshot,
1340
+ error: error instanceof Error ? error.message : String(error),
1341
+ isLoading: false
1342
+ };
1343
+ emit();
1344
+ throw error;
1345
+ }
1346
+ };
1347
+ const close = () => {
1348
+ closed = true;
1349
+ if (timer) {
1350
+ clearInterval(timer);
1351
+ timer = undefined;
1352
+ }
1353
+ listeners.clear();
1354
+ };
1355
+ if (options.intervalMs && options.intervalMs > 0) {
1356
+ timer = setInterval(() => {
1357
+ refresh().catch(() => {});
1358
+ }, options.intervalMs);
1359
+ }
1360
+ return {
1361
+ close,
1362
+ getServerSnapshot: () => snapshot,
1363
+ getSnapshot: () => snapshot,
1364
+ refresh,
1365
+ subscribe: (listener) => {
1366
+ listeners.add(listener);
1367
+ return () => {
1368
+ listeners.delete(listener);
1369
+ };
1370
+ }
1371
+ };
1372
+ };
1373
+
1374
+ // src/angular/voice-provider-status.service.ts
1375
+ var _dec = [
1376
+ Injectable3({ providedIn: "root" })
1377
+ ];
1378
+ var _init = __decoratorStart(undefined);
1379
+
1380
+ class VoiceProviderStatusService {
1381
+ connect(path = "/api/provider-status", options = {}) {
1382
+ const store = createVoiceProviderStatusStore(path, options);
1383
+ const errorSignal = signal3(null);
1384
+ const isLoadingSignal = signal3(false);
1385
+ const providersSignal = signal3([]);
1386
+ const updatedAtSignal = signal3(undefined);
1387
+ const sync = () => {
1388
+ const snapshot = store.getSnapshot();
1389
+ errorSignal.set(snapshot.error);
1390
+ isLoadingSignal.set(snapshot.isLoading);
1391
+ providersSignal.set([...snapshot.providers]);
1392
+ updatedAtSignal.set(snapshot.updatedAt);
1393
+ };
1394
+ const unsubscribe = store.subscribe(sync);
1395
+ sync();
1396
+ store.refresh().catch(() => {});
1397
+ return {
1398
+ close: () => {
1399
+ unsubscribe();
1400
+ store.close();
1401
+ },
1402
+ error: computed3(() => errorSignal()),
1403
+ isLoading: computed3(() => isLoadingSignal()),
1404
+ providers: computed3(() => providersSignal()),
1405
+ refresh: store.refresh,
1406
+ updatedAt: computed3(() => updatedAtSignal())
1407
+ };
1408
+ }
1409
+ }
1410
+ VoiceProviderStatusService = __decorateElement(_init, 0, "VoiceProviderStatusService", _dec, VoiceProviderStatusService);
1411
+ __runInitializers(_init, 1, VoiceProviderStatusService);
1412
+ __decoratorMetadata(_init, VoiceProviderStatusService);
1413
+ let _VoiceProviderStatusService = VoiceProviderStatusService;
1291
1414
  export {
1292
1415
  VoiceStreamService,
1416
+ VoiceProviderStatusService,
1293
1417
  VoiceControllerService
1294
1418
  };
@@ -0,0 +1,12 @@
1
+ import { type VoiceProviderStatusClientOptions } from '../client/providerStatus';
2
+ import type { VoiceProviderHealthSummary } from '../providerHealth';
3
+ export declare class VoiceProviderStatusService {
4
+ connect<TProvider extends string = string>(path?: string, options?: VoiceProviderStatusClientOptions): {
5
+ close: () => void;
6
+ error: import("@angular/core").Signal<string | null>;
7
+ isLoading: import("@angular/core").Signal<boolean>;
8
+ providers: import("@angular/core").Signal<VoiceProviderHealthSummary<TProvider>[]>;
9
+ refresh: () => Promise<VoiceProviderHealthSummary<TProvider>[]>;
10
+ updatedAt: import("@angular/core").Signal<number | undefined>;
11
+ };
12
+ }
@@ -5,3 +5,5 @@ export { createVoiceController } from './controller';
5
5
  export { bindVoiceBargeIn, createVoiceDuplexController } from './duplex';
6
6
  export { bindVoiceHTMX } from './htmx';
7
7
  export { createMicrophoneCapture } from './microphone';
8
+ export { createVoiceProviderStatusStore, fetchVoiceProviderStatus } from './providerStatus';
9
+ export type { VoiceProviderStatusClientOptions, VoiceProviderStatusSnapshot } from './providerStatus';
@@ -1581,9 +1581,90 @@ var createVoiceDuplexController = (path, options = {}) => {
1581
1581
  }
1582
1582
  };
1583
1583
  };
1584
+ // src/client/providerStatus.ts
1585
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
1586
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1587
+ const response = await fetchImpl(path);
1588
+ if (!response.ok) {
1589
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
1590
+ }
1591
+ return await response.json();
1592
+ };
1593
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
1594
+ const listeners = new Set;
1595
+ let closed = false;
1596
+ let timer;
1597
+ let snapshot = {
1598
+ error: null,
1599
+ isLoading: false,
1600
+ providers: []
1601
+ };
1602
+ const emit = () => {
1603
+ for (const listener of listeners) {
1604
+ listener();
1605
+ }
1606
+ };
1607
+ const refresh = async () => {
1608
+ if (closed) {
1609
+ return snapshot.providers;
1610
+ }
1611
+ snapshot = {
1612
+ ...snapshot,
1613
+ error: null,
1614
+ isLoading: true
1615
+ };
1616
+ emit();
1617
+ try {
1618
+ const providers = await fetchVoiceProviderStatus(path, options);
1619
+ snapshot = {
1620
+ error: null,
1621
+ isLoading: false,
1622
+ providers,
1623
+ updatedAt: Date.now()
1624
+ };
1625
+ emit();
1626
+ return providers;
1627
+ } catch (error) {
1628
+ snapshot = {
1629
+ ...snapshot,
1630
+ error: error instanceof Error ? error.message : String(error),
1631
+ isLoading: false
1632
+ };
1633
+ emit();
1634
+ throw error;
1635
+ }
1636
+ };
1637
+ const close = () => {
1638
+ closed = true;
1639
+ if (timer) {
1640
+ clearInterval(timer);
1641
+ timer = undefined;
1642
+ }
1643
+ listeners.clear();
1644
+ };
1645
+ if (options.intervalMs && options.intervalMs > 0) {
1646
+ timer = setInterval(() => {
1647
+ refresh().catch(() => {});
1648
+ }, options.intervalMs);
1649
+ }
1650
+ return {
1651
+ close,
1652
+ getServerSnapshot: () => snapshot,
1653
+ getSnapshot: () => snapshot,
1654
+ refresh,
1655
+ subscribe: (listener) => {
1656
+ listeners.add(listener);
1657
+ return () => {
1658
+ listeners.delete(listener);
1659
+ };
1660
+ }
1661
+ };
1662
+ };
1584
1663
  export {
1664
+ fetchVoiceProviderStatus,
1585
1665
  decodeVoiceAudioChunk,
1586
1666
  createVoiceStream,
1667
+ createVoiceProviderStatusStore,
1587
1668
  createVoiceDuplexController,
1588
1669
  createVoiceController,
1589
1670
  createVoiceConnection,
@@ -0,0 +1,19 @@
1
+ import type { VoiceProviderHealthSummary } from '../providerHealth';
2
+ export type VoiceProviderStatusClientOptions = {
3
+ fetch?: typeof fetch;
4
+ intervalMs?: number;
5
+ };
6
+ export type VoiceProviderStatusSnapshot<TProvider extends string = string> = {
7
+ error: string | null;
8
+ isLoading: boolean;
9
+ providers: VoiceProviderHealthSummary<TProvider>[];
10
+ updatedAt?: number;
11
+ };
12
+ export declare const fetchVoiceProviderStatus: <TProvider extends string = string>(path?: string, options?: Pick<VoiceProviderStatusClientOptions, "fetch">) => Promise<VoiceProviderHealthSummary<TProvider>[]>;
13
+ export declare const createVoiceProviderStatusStore: <TProvider extends string = string>(path?: string, options?: VoiceProviderStatusClientOptions) => {
14
+ close: () => void;
15
+ getServerSnapshot: () => VoiceProviderStatusSnapshot<TProvider>;
16
+ getSnapshot: () => VoiceProviderStatusSnapshot<TProvider>;
17
+ refresh: () => Promise<VoiceProviderHealthSummary<TProvider>[]>;
18
+ subscribe: (listener: () => void) => () => void;
19
+ };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { createVoiceAgent, createVoiceAgentSquad, createVoiceAgentTool } from '.
4
4
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
5
5
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
6
6
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, createVoiceProviderRouter } from './modelAdapters';
7
+ export { createVoiceProviderHealthHTMLHandler, createVoiceProviderHealthJSONHandler, createVoiceProviderHealthRoutes, renderVoiceProviderHealthHTML, summarizeVoiceProviderHealth } from './providerHealth';
7
8
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
8
9
  export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
9
10
  export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
@@ -27,6 +28,7 @@ export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewReco
27
28
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
28
29
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
29
30
  export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
31
+ export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProviderHealthSummaryOptions } from './providerHealth';
30
32
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
31
33
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
32
34
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
package/dist/index.js CHANGED
@@ -7855,6 +7855,190 @@ var createGeminiVoiceAssistantModel = (options) => {
7855
7855
  }
7856
7856
  };
7857
7857
  };
7858
+ // src/providerHealth.ts
7859
+ import { Elysia as Elysia2 } from "elysia";
7860
+ var getString = (value) => typeof value === "string" ? value : undefined;
7861
+ var getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
7862
+ var isProviderStatus = (value) => value === "success" || value === "fallback" || value === "error";
7863
+ var summarizeVoiceProviderHealth = async (input) => {
7864
+ const options = Array.isArray(input) ? { events: input } : input;
7865
+ const events = options.events ?? await options.store?.list() ?? [];
7866
+ const providers = options.providers ?? [];
7867
+ const providerSet = new Set(providers);
7868
+ const now = options.now ?? Date.now();
7869
+ const entries = new Map;
7870
+ const isAllowedProvider = (value) => typeof value === "string" && (providerSet.size === 0 || providerSet.has(value));
7871
+ const getEntry = (provider) => {
7872
+ const existing = entries.get(provider);
7873
+ if (existing) {
7874
+ return existing;
7875
+ }
7876
+ const entry = {
7877
+ elapsedCount: 0,
7878
+ elapsedTotal: 0,
7879
+ errorCount: 0,
7880
+ fallbackCount: 0,
7881
+ provider,
7882
+ rateLimited: false,
7883
+ recommended: false,
7884
+ runCount: 0,
7885
+ status: "idle"
7886
+ };
7887
+ entries.set(provider, entry);
7888
+ return entry;
7889
+ };
7890
+ for (const provider of providers) {
7891
+ getEntry(provider);
7892
+ }
7893
+ const hasProviderRouterEvents = events.some((event) => event.type === "session.error" && isAllowedProvider(event.payload.provider) && isProviderStatus(event.payload.providerStatus));
7894
+ for (const event of events) {
7895
+ if (event.type === "assistant.run") {
7896
+ if (hasProviderRouterEvents) {
7897
+ continue;
7898
+ }
7899
+ const provider2 = event.payload.variantId;
7900
+ if (!isAllowedProvider(provider2)) {
7901
+ continue;
7902
+ }
7903
+ const entry2 = getEntry(provider2);
7904
+ entry2.runCount += 1;
7905
+ const elapsedMs = getNumber(event.payload.elapsedMs);
7906
+ if (elapsedMs !== undefined) {
7907
+ entry2.elapsedCount += 1;
7908
+ entry2.elapsedTotal += elapsedMs;
7909
+ }
7910
+ continue;
7911
+ }
7912
+ if (event.type !== "session.error") {
7913
+ continue;
7914
+ }
7915
+ const provider = event.payload.provider;
7916
+ if (!isAllowedProvider(provider)) {
7917
+ continue;
7918
+ }
7919
+ const providerStatus = isProviderStatus(event.payload.providerStatus) ? event.payload.providerStatus : undefined;
7920
+ const applyProviderHealth = () => {
7921
+ const entry2 = getEntry(provider);
7922
+ const providerHealth = event.payload.providerHealth;
7923
+ if (providerHealth && typeof providerHealth === "object") {
7924
+ const suppressedUntil2 = getNumber(providerHealth.suppressedUntil);
7925
+ if (suppressedUntil2 !== undefined) {
7926
+ entry2.suppressedUntil = suppressedUntil2;
7927
+ }
7928
+ }
7929
+ const suppressedUntil = getNumber(event.payload.suppressedUntil);
7930
+ if (suppressedUntil !== undefined) {
7931
+ entry2.suppressedUntil = suppressedUntil;
7932
+ }
7933
+ const suppressionRemainingMs = getNumber(event.payload.suppressionRemainingMs);
7934
+ if (suppressionRemainingMs !== undefined) {
7935
+ entry2.suppressionRemainingMs = suppressionRemainingMs;
7936
+ }
7937
+ return entry2;
7938
+ };
7939
+ if (providerStatus === "success" || providerStatus === "fallback") {
7940
+ const entry2 = applyProviderHealth();
7941
+ entry2.runCount += 1;
7942
+ entry2.lastSuccessAt = event.at;
7943
+ if (providerStatus === "success") {
7944
+ entry2.lastError = undefined;
7945
+ entry2.rateLimited = false;
7946
+ entry2.suppressedUntil = undefined;
7947
+ entry2.suppressionRemainingMs = undefined;
7948
+ }
7949
+ const elapsedMs = getNumber(event.payload.elapsedMs);
7950
+ if (elapsedMs !== undefined) {
7951
+ entry2.elapsedCount += 1;
7952
+ entry2.elapsedTotal += elapsedMs;
7953
+ }
7954
+ const selectedProvider = event.payload.selectedProvider;
7955
+ if (providerStatus === "fallback" && isAllowedProvider(selectedProvider) && selectedProvider !== provider) {
7956
+ getEntry(selectedProvider).fallbackCount += 1;
7957
+ }
7958
+ continue;
7959
+ }
7960
+ const entry = applyProviderHealth();
7961
+ entry.errorCount += 1;
7962
+ entry.lastError = getString(event.payload.error);
7963
+ entry.lastErrorAt = event.at;
7964
+ entry.rateLimited ||= event.payload.rateLimited === true;
7965
+ }
7966
+ const summaries = [...entries.values()].map((entry) => {
7967
+ const hadSuppression = typeof entry.suppressedUntil === "number" || typeof entry.suppressionRemainingMs === "number";
7968
+ const suppressionRemainingMs = typeof entry.suppressedUntil === "number" ? Math.max(0, entry.suppressedUntil - now) : entry.suppressionRemainingMs;
7969
+ const activeSuppression = typeof suppressionRemainingMs === "number" && suppressionRemainingMs > 0;
7970
+ const recoverable = hadSuppression && !activeSuppression;
7971
+ const averageElapsedMs = entry.elapsedCount > 0 ? Math.round(entry.elapsedTotal / entry.elapsedCount) : undefined;
7972
+ const status = activeSuppression ? "suppressed" : recoverable ? "recoverable" : entry.rateLimited ? "rate-limited" : entry.errorCount > 0 && (!entry.lastSuccessAt || !entry.lastErrorAt || entry.lastErrorAt > entry.lastSuccessAt) ? "degraded" : entry.runCount > 0 ? "healthy" : "idle";
7973
+ return {
7974
+ averageElapsedMs,
7975
+ errorCount: entry.errorCount,
7976
+ fallbackCount: entry.fallbackCount,
7977
+ lastError: entry.lastError,
7978
+ lastErrorAt: entry.lastErrorAt,
7979
+ lastSuccessAt: entry.lastSuccessAt,
7980
+ provider: entry.provider,
7981
+ rateLimited: entry.rateLimited,
7982
+ recommended: false,
7983
+ runCount: entry.runCount,
7984
+ status,
7985
+ suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
7986
+ suppressedUntil: entry.suppressedUntil
7987
+ };
7988
+ });
7989
+ const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
7990
+ if (recommended) {
7991
+ recommended.recommended = true;
7992
+ }
7993
+ return summaries;
7994
+ };
7995
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
7996
+ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p class="voice-provider-empty">No provider status yet.</p>' : [
7997
+ '<div class="voice-provider-health">',
7998
+ ...providers.map((provider) => {
7999
+ const suppressionSeconds = typeof provider.suppressionRemainingMs === "number" ? Math.ceil(provider.suppressionRemainingMs / 1000) : undefined;
8000
+ return [
8001
+ `<article class="voice-provider-card ${escapeHtml4(provider.status)}">`,
8002
+ '<div class="voice-provider-card-header">',
8003
+ `<strong>${escapeHtml4(provider.provider)}</strong>`,
8004
+ `<span>${escapeHtml4(provider.status)}${provider.recommended ? " \xB7 recommended" : ""}</span>`,
8005
+ "</div>",
8006
+ "<dl>",
8007
+ `<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
8008
+ `<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
8009
+ `<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
8010
+ `<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
8011
+ "</dl>",
8012
+ suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
8013
+ provider.lastError ? `<p>${escapeHtml4(provider.lastError)}</p>` : "",
8014
+ "</article>"
8015
+ ].join("");
8016
+ }),
8017
+ "</div>"
8018
+ ].join("");
8019
+ var createVoiceProviderHealthJSONHandler = (options) => async () => summarizeVoiceProviderHealth(options);
8020
+ var createVoiceProviderHealthHTMLHandler = (options) => async () => {
8021
+ const providers = await summarizeVoiceProviderHealth(options);
8022
+ const render = options.render ?? renderVoiceProviderHealthHTML;
8023
+ const body = await render(providers);
8024
+ return new Response(body, {
8025
+ headers: {
8026
+ "Content-Type": "text/html; charset=utf-8",
8027
+ ...options.headers
8028
+ }
8029
+ });
8030
+ };
8031
+ var createVoiceProviderHealthRoutes = (options) => {
8032
+ const path = options.path ?? "/api/provider-status";
8033
+ const htmlPath = options.htmlPath === undefined ? `${path}/htmx` : options.htmlPath;
8034
+ const routes = new Elysia2({
8035
+ name: options.name ?? "absolutejs-voice-provider-health"
8036
+ }).get(path, createVoiceProviderHealthJSONHandler(options));
8037
+ if (htmlPath) {
8038
+ routes.get(htmlPath, createVoiceProviderHealthHTMLHandler(options));
8039
+ }
8040
+ return routes;
8041
+ };
7858
8042
  // src/sqliteStore.ts
7859
8043
  import { Database } from "bun:sqlite";
7860
8044
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
@@ -10305,6 +10489,7 @@ export {
10305
10489
  transcodePCMToTwilioOutboundPayload,
10306
10490
  summarizeVoiceTraceSinkDeliveries,
10307
10491
  summarizeVoiceTrace,
10492
+ summarizeVoiceProviderHealth,
10308
10493
  summarizeVoiceOpsTasks,
10309
10494
  summarizeVoiceOpsTaskQueue,
10310
10495
  summarizeVoiceOpsTaskAnalytics,
@@ -10328,6 +10513,7 @@ export {
10328
10513
  reopenVoiceOpsTask,
10329
10514
  renderVoiceTraceMarkdown,
10330
10515
  renderVoiceTraceHTML,
10516
+ renderVoiceProviderHealthHTML,
10331
10517
  renderVoiceCallReviewMarkdown,
10332
10518
  renderVoiceCallReviewHTML,
10333
10519
  redactVoiceTraceText,
@@ -10383,6 +10569,9 @@ export {
10383
10569
  createVoiceRedisTaskLeaseCoordinator,
10384
10570
  createVoiceRedisIdempotencyStore,
10385
10571
  createVoiceProviderRouter,
10572
+ createVoiceProviderHealthRoutes,
10573
+ createVoiceProviderHealthJSONHandler,
10574
+ createVoiceProviderHealthHTMLHandler,
10386
10575
  createVoicePostgresTraceSinkDeliveryStore,
10387
10576
  createVoicePostgresTraceEventStore,
10388
10577
  createVoicePostgresTaskStore,
@@ -0,0 +1,78 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceTraceEvent, VoiceTraceEventStore } from './trace';
3
+ export type VoiceProviderHealthStatus = 'healthy' | 'idle' | 'rate-limited' | 'degraded' | 'recoverable' | 'suppressed';
4
+ export type VoiceProviderHealthSummary<TProvider extends string = string> = {
5
+ averageElapsedMs?: number;
6
+ errorCount: number;
7
+ fallbackCount: number;
8
+ lastError?: string;
9
+ lastErrorAt?: number;
10
+ lastSuccessAt?: number;
11
+ provider: TProvider;
12
+ rateLimited: boolean;
13
+ recommended: boolean;
14
+ runCount: number;
15
+ status: VoiceProviderHealthStatus;
16
+ suppressionRemainingMs?: number;
17
+ suppressedUntil?: number;
18
+ };
19
+ export type VoiceProviderHealthSummaryOptions<TProvider extends string = string> = {
20
+ events?: StoredVoiceTraceEvent[];
21
+ now?: number;
22
+ providers?: readonly TProvider[];
23
+ store?: VoiceTraceEventStore;
24
+ };
25
+ export type VoiceProviderHealthHandlerOptions<TProvider extends string = string> = VoiceProviderHealthSummaryOptions<TProvider>;
26
+ export type VoiceProviderHealthHTMLHandlerOptions<TProvider extends string = string> = VoiceProviderHealthHandlerOptions<TProvider> & {
27
+ headers?: HeadersInit;
28
+ render?: (providers: VoiceProviderHealthSummary<TProvider>[]) => string | Promise<string>;
29
+ };
30
+ export type VoiceProviderHealthRoutesOptions<TProvider extends string = string> = VoiceProviderHealthHTMLHandlerOptions<TProvider> & {
31
+ htmlPath?: false | string;
32
+ name?: string;
33
+ path?: string;
34
+ };
35
+ export declare const summarizeVoiceProviderHealth: <TProvider extends string = string>(input: StoredVoiceTraceEvent[] | VoiceProviderHealthSummaryOptions<TProvider>) => Promise<VoiceProviderHealthSummary<TProvider>[]>;
36
+ export declare const renderVoiceProviderHealthHTML: (providers: VoiceProviderHealthSummary[]) => string;
37
+ export declare const createVoiceProviderHealthJSONHandler: <TProvider extends string = string>(options: VoiceProviderHealthHandlerOptions<TProvider>) => () => Promise<VoiceProviderHealthSummary<TProvider>[]>;
38
+ export declare const createVoiceProviderHealthHTMLHandler: <TProvider extends string = string>(options: VoiceProviderHealthHTMLHandlerOptions<TProvider>) => () => Promise<Response>;
39
+ export declare const createVoiceProviderHealthRoutes: <TProvider extends string = string>(options: VoiceProviderHealthRoutesOptions<TProvider>) => Elysia<"", {
40
+ decorator: {};
41
+ store: {};
42
+ derive: {};
43
+ resolve: {};
44
+ }, {
45
+ typebox: {};
46
+ error: {};
47
+ }, {
48
+ schema: {};
49
+ standaloneSchema: {};
50
+ macro: {};
51
+ macroFn: {};
52
+ parser: {};
53
+ response: {};
54
+ }, {
55
+ [x: string]: {
56
+ get: {
57
+ body: unknown;
58
+ params: {};
59
+ query: unknown;
60
+ headers: unknown;
61
+ response: {
62
+ 200: VoiceProviderHealthSummary<TProvider>[];
63
+ };
64
+ };
65
+ };
66
+ }, {
67
+ derive: {};
68
+ resolve: {};
69
+ schema: {};
70
+ standaloneSchema: {};
71
+ response: {};
72
+ }, {
73
+ derive: {};
74
+ resolve: {};
75
+ schema: {};
76
+ standaloneSchema: {};
77
+ response: {};
78
+ }>;
@@ -1,2 +1,3 @@
1
1
  export { useVoiceStream } from './useVoiceStream';
2
2
  export { useVoiceController } from './useVoiceController';
3
+ export { useVoiceProviderStatus } from './useVoiceProviderStatus';
@@ -1234,7 +1234,107 @@ var useVoiceController = (path, options = {}) => {
1234
1234
  toggleRecording: () => controller.toggleRecording()
1235
1235
  };
1236
1236
  };
1237
+ // src/react/useVoiceProviderStatus.tsx
1238
+ import { useEffect as useEffect3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore3 } from "react";
1239
+
1240
+ // src/client/providerStatus.ts
1241
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
1242
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1243
+ const response = await fetchImpl(path);
1244
+ if (!response.ok) {
1245
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
1246
+ }
1247
+ return await response.json();
1248
+ };
1249
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
1250
+ const listeners = new Set;
1251
+ let closed = false;
1252
+ let timer;
1253
+ let snapshot = {
1254
+ error: null,
1255
+ isLoading: false,
1256
+ providers: []
1257
+ };
1258
+ const emit = () => {
1259
+ for (const listener of listeners) {
1260
+ listener();
1261
+ }
1262
+ };
1263
+ const refresh = async () => {
1264
+ if (closed) {
1265
+ return snapshot.providers;
1266
+ }
1267
+ snapshot = {
1268
+ ...snapshot,
1269
+ error: null,
1270
+ isLoading: true
1271
+ };
1272
+ emit();
1273
+ try {
1274
+ const providers = await fetchVoiceProviderStatus(path, options);
1275
+ snapshot = {
1276
+ error: null,
1277
+ isLoading: false,
1278
+ providers,
1279
+ updatedAt: Date.now()
1280
+ };
1281
+ emit();
1282
+ return providers;
1283
+ } catch (error) {
1284
+ snapshot = {
1285
+ ...snapshot,
1286
+ error: error instanceof Error ? error.message : String(error),
1287
+ isLoading: false
1288
+ };
1289
+ emit();
1290
+ throw error;
1291
+ }
1292
+ };
1293
+ const close = () => {
1294
+ closed = true;
1295
+ if (timer) {
1296
+ clearInterval(timer);
1297
+ timer = undefined;
1298
+ }
1299
+ listeners.clear();
1300
+ };
1301
+ if (options.intervalMs && options.intervalMs > 0) {
1302
+ timer = setInterval(() => {
1303
+ refresh().catch(() => {});
1304
+ }, options.intervalMs);
1305
+ }
1306
+ return {
1307
+ close,
1308
+ getServerSnapshot: () => snapshot,
1309
+ getSnapshot: () => snapshot,
1310
+ refresh,
1311
+ subscribe: (listener) => {
1312
+ listeners.add(listener);
1313
+ return () => {
1314
+ listeners.delete(listener);
1315
+ };
1316
+ }
1317
+ };
1318
+ };
1319
+
1320
+ // src/react/useVoiceProviderStatus.tsx
1321
+ var useVoiceProviderStatus = (path = "/api/provider-status", options = {}) => {
1322
+ const storeRef = useRef3(null);
1323
+ if (!storeRef.current) {
1324
+ storeRef.current = createVoiceProviderStatusStore(path, options);
1325
+ }
1326
+ const store = storeRef.current;
1327
+ useEffect3(() => {
1328
+ store.refresh().catch(() => {});
1329
+ return () => store.close();
1330
+ }, [store]);
1331
+ return {
1332
+ ...useSyncExternalStore3(store.subscribe, store.getSnapshot, store.getServerSnapshot),
1333
+ refresh: store.refresh
1334
+ };
1335
+ };
1237
1336
  export {
1238
1337
  useVoiceStream,
1338
+ useVoiceProviderStatus,
1239
1339
  useVoiceController
1240
1340
  };
@@ -0,0 +1,8 @@
1
+ import { type VoiceProviderStatusClientOptions } from '../client/providerStatus';
2
+ export declare const useVoiceProviderStatus: <TProvider extends string = string>(path?: string, options?: VoiceProviderStatusClientOptions) => {
3
+ refresh: () => Promise<import("..").VoiceProviderHealthSummary<TProvider>[]>;
4
+ error: string | null;
5
+ isLoading: boolean;
6
+ providers: import("..").VoiceProviderHealthSummary<TProvider>[];
7
+ updatedAt?: number;
8
+ };
@@ -0,0 +1,8 @@
1
+ import type { VoiceProviderStatusClientOptions } from '../client/providerStatus';
2
+ export declare const createVoiceProviderStatus: <TProvider extends string = string>(path?: string, options?: VoiceProviderStatusClientOptions) => {
3
+ close: () => void;
4
+ getServerSnapshot: () => import("../client").VoiceProviderStatusSnapshot<TProvider>;
5
+ getSnapshot: () => import("../client").VoiceProviderStatusSnapshot<TProvider>;
6
+ refresh: () => Promise<import("..").VoiceProviderHealthSummary<TProvider>[]>;
7
+ subscribe: (listener: () => void) => () => void;
8
+ };
@@ -1,2 +1,3 @@
1
1
  export { createVoiceStream } from './createVoiceStream';
2
+ export { createVoiceProviderStatus } from './createVoiceProviderStatus';
2
3
  export { createVoiceController } from '../client/controller';
@@ -548,6 +548,88 @@ var createVoiceStream = (path, options = {}) => {
548
548
 
549
549
  // src/svelte/createVoiceStream.ts
550
550
  var createVoiceStream2 = (path, options = {}) => createVoiceStream(path, options);
551
+ // src/client/providerStatus.ts
552
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
553
+ const fetchImpl = options.fetch ?? globalThis.fetch;
554
+ const response = await fetchImpl(path);
555
+ if (!response.ok) {
556
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
557
+ }
558
+ return await response.json();
559
+ };
560
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
561
+ const listeners = new Set;
562
+ let closed = false;
563
+ let timer;
564
+ let snapshot = {
565
+ error: null,
566
+ isLoading: false,
567
+ providers: []
568
+ };
569
+ const emit = () => {
570
+ for (const listener of listeners) {
571
+ listener();
572
+ }
573
+ };
574
+ const refresh = async () => {
575
+ if (closed) {
576
+ return snapshot.providers;
577
+ }
578
+ snapshot = {
579
+ ...snapshot,
580
+ error: null,
581
+ isLoading: true
582
+ };
583
+ emit();
584
+ try {
585
+ const providers = await fetchVoiceProviderStatus(path, options);
586
+ snapshot = {
587
+ error: null,
588
+ isLoading: false,
589
+ providers,
590
+ updatedAt: Date.now()
591
+ };
592
+ emit();
593
+ return providers;
594
+ } catch (error) {
595
+ snapshot = {
596
+ ...snapshot,
597
+ error: error instanceof Error ? error.message : String(error),
598
+ isLoading: false
599
+ };
600
+ emit();
601
+ throw error;
602
+ }
603
+ };
604
+ const close = () => {
605
+ closed = true;
606
+ if (timer) {
607
+ clearInterval(timer);
608
+ timer = undefined;
609
+ }
610
+ listeners.clear();
611
+ };
612
+ if (options.intervalMs && options.intervalMs > 0) {
613
+ timer = setInterval(() => {
614
+ refresh().catch(() => {});
615
+ }, options.intervalMs);
616
+ }
617
+ return {
618
+ close,
619
+ getServerSnapshot: () => snapshot,
620
+ getSnapshot: () => snapshot,
621
+ refresh,
622
+ subscribe: (listener) => {
623
+ listeners.add(listener);
624
+ return () => {
625
+ listeners.delete(listener);
626
+ };
627
+ }
628
+ };
629
+ };
630
+
631
+ // src/svelte/createVoiceProviderStatus.ts
632
+ var createVoiceProviderStatus = (path = "/api/provider-status", options = {}) => createVoiceProviderStatusStore(path, options);
551
633
  // src/client/htmx.ts
552
634
  var DEFAULT_EVENT_NAME = "voice-refresh";
553
635
  var DEFAULT_QUERY_PARAM = "sessionId";
@@ -1173,5 +1255,6 @@ var createVoiceController = (path, options = {}) => {
1173
1255
  };
1174
1256
  export {
1175
1257
  createVoiceStream2 as createVoiceStream,
1258
+ createVoiceProviderStatus,
1176
1259
  createVoiceController
1177
1260
  };
@@ -1,2 +1,3 @@
1
1
  export { useVoiceStream } from './useVoiceStream';
2
2
  export { useVoiceController } from './useVoiceController';
3
+ export { useVoiceProviderStatus } from './useVoiceProviderStatus';
package/dist/vue/index.js CHANGED
@@ -1270,7 +1270,120 @@ var useVoiceController = (path, options = {}) => {
1270
1270
  turns
1271
1271
  };
1272
1272
  };
1273
+ // src/vue/useVoiceProviderStatus.ts
1274
+ import { onUnmounted as onUnmounted3, ref as ref3, shallowRef as shallowRef3 } from "vue";
1275
+
1276
+ // src/client/providerStatus.ts
1277
+ var fetchVoiceProviderStatus = async (path = "/api/provider-status", options = {}) => {
1278
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1279
+ const response = await fetchImpl(path);
1280
+ if (!response.ok) {
1281
+ throw new Error(`Voice provider status failed: HTTP ${response.status}`);
1282
+ }
1283
+ return await response.json();
1284
+ };
1285
+ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {}) => {
1286
+ const listeners = new Set;
1287
+ let closed = false;
1288
+ let timer;
1289
+ let snapshot = {
1290
+ error: null,
1291
+ isLoading: false,
1292
+ providers: []
1293
+ };
1294
+ const emit = () => {
1295
+ for (const listener of listeners) {
1296
+ listener();
1297
+ }
1298
+ };
1299
+ const refresh = async () => {
1300
+ if (closed) {
1301
+ return snapshot.providers;
1302
+ }
1303
+ snapshot = {
1304
+ ...snapshot,
1305
+ error: null,
1306
+ isLoading: true
1307
+ };
1308
+ emit();
1309
+ try {
1310
+ const providers = await fetchVoiceProviderStatus(path, options);
1311
+ snapshot = {
1312
+ error: null,
1313
+ isLoading: false,
1314
+ providers,
1315
+ updatedAt: Date.now()
1316
+ };
1317
+ emit();
1318
+ return providers;
1319
+ } catch (error) {
1320
+ snapshot = {
1321
+ ...snapshot,
1322
+ error: error instanceof Error ? error.message : String(error),
1323
+ isLoading: false
1324
+ };
1325
+ emit();
1326
+ throw error;
1327
+ }
1328
+ };
1329
+ const close = () => {
1330
+ closed = true;
1331
+ if (timer) {
1332
+ clearInterval(timer);
1333
+ timer = undefined;
1334
+ }
1335
+ listeners.clear();
1336
+ };
1337
+ if (options.intervalMs && options.intervalMs > 0) {
1338
+ timer = setInterval(() => {
1339
+ refresh().catch(() => {});
1340
+ }, options.intervalMs);
1341
+ }
1342
+ return {
1343
+ close,
1344
+ getServerSnapshot: () => snapshot,
1345
+ getSnapshot: () => snapshot,
1346
+ refresh,
1347
+ subscribe: (listener) => {
1348
+ listeners.add(listener);
1349
+ return () => {
1350
+ listeners.delete(listener);
1351
+ };
1352
+ }
1353
+ };
1354
+ };
1355
+
1356
+ // src/vue/useVoiceProviderStatus.ts
1357
+ var useVoiceProviderStatus = (path = "/api/provider-status", options = {}) => {
1358
+ const store = createVoiceProviderStatusStore(path, options);
1359
+ const error = ref3(null);
1360
+ const isLoading = ref3(false);
1361
+ const providers = shallowRef3([]);
1362
+ const updatedAt = ref3(undefined);
1363
+ const sync = () => {
1364
+ const snapshot = store.getSnapshot();
1365
+ error.value = snapshot.error;
1366
+ isLoading.value = snapshot.isLoading;
1367
+ providers.value = [...snapshot.providers];
1368
+ updatedAt.value = snapshot.updatedAt;
1369
+ };
1370
+ const unsubscribe = store.subscribe(sync);
1371
+ sync();
1372
+ store.refresh().catch(() => {});
1373
+ onUnmounted3(() => {
1374
+ unsubscribe();
1375
+ store.close();
1376
+ });
1377
+ return {
1378
+ error,
1379
+ isLoading,
1380
+ providers,
1381
+ refresh: store.refresh,
1382
+ updatedAt
1383
+ };
1384
+ };
1273
1385
  export {
1274
1386
  useVoiceStream,
1387
+ useVoiceProviderStatus,
1275
1388
  useVoiceController
1276
1389
  };
@@ -0,0 +1,9 @@
1
+ import { type VoiceProviderStatusClientOptions } from '../client/providerStatus';
2
+ import type { VoiceProviderHealthSummary } from '../providerHealth';
3
+ export declare const useVoiceProviderStatus: <TProvider extends string = string>(path?: string, options?: VoiceProviderStatusClientOptions) => {
4
+ error: import("vue").Ref<string | null, string | null>;
5
+ isLoading: import("vue").Ref<boolean, boolean>;
6
+ providers: import("vue").ShallowRef<VoiceProviderHealthSummary<TProvider>[], VoiceProviderHealthSummary<TProvider>[]>;
7
+ refresh: () => Promise<VoiceProviderHealthSummary<TProvider>[]>;
8
+ updatedAt: import("vue").Ref<number | undefined, number | undefined>;
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.14",
3
+ "version": "0.0.22-beta.16",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",