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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -26,7 +26,7 @@ export { resolveTurnDetectionConfig, TURN_PROFILE_DEFAULTS } from './turnProfile
26
26
  export { createVoiceCallReviewFromLiveTelephonyReport, createVoiceCallReviewRecorder, renderVoiceCallReviewHTML, renderVoiceCallReviewMarkdown } from './testing/review';
27
27
  export type { VoiceAssistant, VoiceAssistantArtifactPlan, VoiceAssistantExperiment, VoiceAssistantExperimentOptions, VoiceAssistantGuardrailInput, VoiceAssistantGuardrails, VoiceAssistantMemoryLifecycle, VoiceAssistantMemoryLifecycleInput, VoiceAssistantOptions, VoiceAssistantOutputGuardrailInput, VoiceAssistantPreset, VoiceAssistantRunsSummary, VoiceAssistantRunSummary, VoiceAssistantVariant } from './assistant';
28
28
  export type { VoiceAssistantMemoryBinding, VoiceAssistantMemoryHandle, VoiceAssistantMemoryOptions, VoiceAssistantMemoryRecord, VoiceAssistantMemoryStore } from './assistantMemory';
29
- export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
29
+ export type { AnthropicVoiceAssistantModelOptions, GeminiVoiceAssistantModelOptions, OpenAIVoiceAssistantModelOptions, VoiceProviderRouterEvent, VoiceProviderRouterFallbackMode, VoiceProviderRouterHealthOptions, VoiceProviderRouterOptions, VoiceProviderRouterPolicy, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile, VoiceJSONAssistantModelHandler, VoiceJSONAssistantModelOptions } from './modelAdapters';
30
30
  export type { VoiceAgent, VoiceAgentMessage, VoiceAgentMessageRole, VoiceAgentModel, VoiceAgentModelInput, VoiceAgentModelOutput, VoiceAgentOptions, VoiceAgentRunResult, VoiceAgentSquadOptions, VoiceAgentTool, VoiceAgentToolCall, VoiceAgentToolResult } from './agent';
31
31
  export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, VoiceOpsRuntimeSinkWorkerConfig, VoiceOpsRuntimeTaskWorkerConfig, VoiceOpsRuntimeTickResult, VoiceOpsRuntimeWebhookWorkerConfig } from './opsRuntime';
32
32
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
package/dist/index.js CHANGED
@@ -7246,6 +7246,74 @@ var createVoiceProviderRouter = (options) => {
7246
7246
  } : options.policy;
7247
7247
  const strategy = policy?.strategy ?? "prefer-selected";
7248
7248
  const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? "provider-error";
7249
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
7250
+ const healthState = new Map;
7251
+ const now = () => healthOptions?.now?.() ?? Date.now();
7252
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
7253
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
7254
+ const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
7255
+ const getHealth = (provider) => {
7256
+ const existing = healthState.get(provider);
7257
+ if (existing) {
7258
+ return existing;
7259
+ }
7260
+ const next = {
7261
+ consecutiveFailures: 0,
7262
+ provider,
7263
+ status: "healthy"
7264
+ };
7265
+ healthState.set(provider, next);
7266
+ return next;
7267
+ };
7268
+ const cloneHealth = (provider) => {
7269
+ if (!healthOptions) {
7270
+ return;
7271
+ }
7272
+ return {
7273
+ ...getHealth(provider)
7274
+ };
7275
+ };
7276
+ const getSuppressionRemainingMs = (provider) => {
7277
+ if (!healthOptions) {
7278
+ return;
7279
+ }
7280
+ const suppressedUntil = getHealth(provider).suppressedUntil;
7281
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
7282
+ };
7283
+ const isSuppressed = (provider) => {
7284
+ if (!healthOptions) {
7285
+ return false;
7286
+ }
7287
+ const health = getHealth(provider);
7288
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
7289
+ };
7290
+ const recordProviderSuccess = (provider) => {
7291
+ if (!healthOptions) {
7292
+ return;
7293
+ }
7294
+ const health = getHealth(provider);
7295
+ health.consecutiveFailures = 0;
7296
+ health.status = "healthy";
7297
+ health.suppressedUntil = undefined;
7298
+ return cloneHealth(provider);
7299
+ };
7300
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
7301
+ if (!healthOptions || !isProviderError) {
7302
+ return cloneHealth(provider);
7303
+ }
7304
+ const currentTime = now();
7305
+ const health = getHealth(provider);
7306
+ health.consecutiveFailures += 1;
7307
+ health.lastFailureAt = currentTime;
7308
+ if (rateLimited) {
7309
+ health.lastRateLimitedAt = currentTime;
7310
+ }
7311
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
7312
+ health.status = "suppressed";
7313
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
7314
+ }
7315
+ return cloneHealth(provider);
7316
+ };
7249
7317
  const resolveAllowedProviders = async (input) => {
7250
7318
  const allowProviders = policy?.allowProviders ?? options.allowProviders;
7251
7319
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
@@ -7270,10 +7338,16 @@ var createVoiceProviderRouter = (options) => {
7270
7338
  const rankedProviders = sortProviders([
7271
7339
  ...fallbackOrder ?? providerIds
7272
7340
  ]).filter((provider) => allowedProviders.has(provider));
7273
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) ? selectedProvider : rankedProviders[0] ?? firstProvider;
7341
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
7342
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
7343
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
7274
7344
  const seen = new Set;
7275
7345
  const order = [];
7276
- const candidates = strategy === "ordered" ? rankedProviders : [preferred, ...rankedProviders, ...providerIds];
7346
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
7347
+ preferred,
7348
+ ...candidateRankedProviders,
7349
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
7350
+ ];
7277
7351
  for (const provider of candidates) {
7278
7352
  if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
7279
7353
  continue;
@@ -7304,11 +7378,13 @@ var createVoiceProviderRouter = (options) => {
7304
7378
  const startedAt = Date.now();
7305
7379
  try {
7306
7380
  const output = await model.generate(input);
7381
+ const providerHealth = recordProviderSuccess(provider);
7307
7382
  await emit({
7308
7383
  at: Date.now(),
7309
7384
  elapsedMs: Date.now() - startedAt,
7310
7385
  fallbackProvider: provider === selectedProvider ? undefined : provider,
7311
7386
  provider,
7387
+ providerHealth,
7312
7388
  recovered: provider !== selectedProvider,
7313
7389
  selectedProvider,
7314
7390
  status: provider === selectedProvider ? "success" : "fallback"
@@ -7320,6 +7396,7 @@ var createVoiceProviderRouter = (options) => {
7320
7396
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
7321
7397
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
7322
7398
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
7399
+ const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
7323
7400
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
7324
7401
  await emit({
7325
7402
  at: Date.now(),
@@ -7327,8 +7404,11 @@ var createVoiceProviderRouter = (options) => {
7327
7404
  error: errorMessage(error),
7328
7405
  fallbackProvider: shouldFallback ? nextProvider : undefined,
7329
7406
  provider,
7407
+ providerHealth,
7330
7408
  rateLimited,
7331
7409
  selectedProvider,
7410
+ suppressionRemainingMs: getSuppressionRemainingMs(provider),
7411
+ suppressedUntil: providerHealth?.suppressedUntil,
7332
7412
  status: "error"
7333
7413
  }, input);
7334
7414
  if (!hasNextProvider || !shouldFallback) {
@@ -40,9 +40,12 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
40
40
  error?: string;
41
41
  fallbackProvider?: TProvider;
42
42
  provider: TProvider;
43
+ providerHealth?: VoiceProviderRouterProviderHealth<TProvider>;
43
44
  rateLimited?: boolean;
44
45
  recovered?: boolean;
45
46
  selectedProvider: TProvider;
47
+ suppressionRemainingMs?: number;
48
+ suppressedUntil?: number;
46
49
  status: 'error' | 'fallback' | 'success';
47
50
  };
48
51
  export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
@@ -56,6 +59,20 @@ export type VoiceProviderRouterProviderProfile = {
56
59
  latencyMs?: number;
57
60
  priority?: number;
58
61
  };
62
+ export type VoiceProviderRouterHealthOptions = {
63
+ cooldownMs?: number;
64
+ failureThreshold?: number;
65
+ now?: () => number;
66
+ rateLimitCooldownMs?: number;
67
+ };
68
+ export type VoiceProviderRouterProviderHealth<TProvider extends string = string> = {
69
+ consecutiveFailures: number;
70
+ lastFailureAt?: number;
71
+ lastRateLimitedAt?: number;
72
+ provider: TProvider;
73
+ status: 'healthy' | 'suppressed';
74
+ suppressedUntil?: number;
75
+ };
59
76
  export type VoiceProviderRouterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TProvider extends string = string> = {
60
77
  allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
61
78
  fallback?: TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
@@ -64,6 +81,7 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
64
81
  isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
65
82
  onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
66
83
  policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
84
+ providerHealth?: boolean | VoiceProviderRouterHealthOptions;
67
85
  providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
68
86
  providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
69
87
  selectProvider?: (input: VoiceAgentModelInput<TContext, TSession>) => TProvider | undefined | Promise<TProvider | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.12",
3
+ "version": "0.0.22-beta.14",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",