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

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,57 @@ 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 isSuppressed = (provider) => {
7269
+ if (!healthOptions) {
7270
+ return false;
7271
+ }
7272
+ const health = getHealth(provider);
7273
+ return typeof health.suppressedUntil === "number" && health.suppressedUntil > now();
7274
+ };
7275
+ const recordProviderSuccess = (provider) => {
7276
+ if (!healthOptions) {
7277
+ return;
7278
+ }
7279
+ const health = getHealth(provider);
7280
+ health.consecutiveFailures = 0;
7281
+ health.status = "healthy";
7282
+ health.suppressedUntil = undefined;
7283
+ };
7284
+ const recordProviderError = (provider, isProviderError, rateLimited) => {
7285
+ if (!healthOptions || !isProviderError) {
7286
+ return;
7287
+ }
7288
+ const currentTime = now();
7289
+ const health = getHealth(provider);
7290
+ health.consecutiveFailures += 1;
7291
+ health.lastFailureAt = currentTime;
7292
+ if (rateLimited) {
7293
+ health.lastRateLimitedAt = currentTime;
7294
+ }
7295
+ if (rateLimited || health.consecutiveFailures >= failureThreshold) {
7296
+ health.status = "suppressed";
7297
+ health.suppressedUntil = currentTime + (rateLimited ? rateLimitCooldownMs : cooldownMs);
7298
+ }
7299
+ };
7249
7300
  const resolveAllowedProviders = async (input) => {
7250
7301
  const allowProviders = policy?.allowProviders ?? options.allowProviders;
7251
7302
  const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
@@ -7270,10 +7321,16 @@ var createVoiceProviderRouter = (options) => {
7270
7321
  const rankedProviders = sortProviders([
7271
7322
  ...fallbackOrder ?? providerIds
7272
7323
  ]).filter((provider) => allowedProviders.has(provider));
7273
- const preferred = selectedProvider && allowedProviders.has(selectedProvider) ? selectedProvider : rankedProviders[0] ?? firstProvider;
7324
+ const healthyRankedProviders = healthOptions ? rankedProviders.filter((provider) => !isSuppressed(provider)) : rankedProviders;
7325
+ const candidateRankedProviders = healthyRankedProviders.length ? healthyRankedProviders : rankedProviders;
7326
+ const preferred = selectedProvider && allowedProviders.has(selectedProvider) && (!healthOptions || !isSuppressed(selectedProvider)) ? selectedProvider : candidateRankedProviders[0] ?? firstProvider;
7274
7327
  const seen = new Set;
7275
7328
  const order = [];
7276
- const candidates = strategy === "ordered" ? rankedProviders : [preferred, ...rankedProviders, ...providerIds];
7329
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
7330
+ preferred,
7331
+ ...candidateRankedProviders,
7332
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
7333
+ ];
7277
7334
  for (const provider of candidates) {
7278
7335
  if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
7279
7336
  continue;
@@ -7304,6 +7361,7 @@ var createVoiceProviderRouter = (options) => {
7304
7361
  const startedAt = Date.now();
7305
7362
  try {
7306
7363
  const output = await model.generate(input);
7364
+ recordProviderSuccess(provider);
7307
7365
  await emit({
7308
7366
  at: Date.now(),
7309
7367
  elapsedMs: Date.now() - startedAt,
@@ -7320,6 +7378,7 @@ var createVoiceProviderRouter = (options) => {
7320
7378
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
7321
7379
  const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
7322
7380
  const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
7381
+ recordProviderError(provider, isProviderError, rateLimited);
7323
7382
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
7324
7383
  await emit({
7325
7384
  at: Date.now(),
@@ -56,6 +56,20 @@ export type VoiceProviderRouterProviderProfile = {
56
56
  latencyMs?: number;
57
57
  priority?: number;
58
58
  };
59
+ export type VoiceProviderRouterHealthOptions = {
60
+ cooldownMs?: number;
61
+ failureThreshold?: number;
62
+ now?: () => number;
63
+ rateLimitCooldownMs?: number;
64
+ };
65
+ export type VoiceProviderRouterProviderHealth<TProvider extends string = string> = {
66
+ consecutiveFailures: number;
67
+ lastFailureAt?: number;
68
+ lastRateLimitedAt?: number;
69
+ provider: TProvider;
70
+ status: 'healthy' | 'suppressed';
71
+ suppressedUntil?: number;
72
+ };
59
73
  export type VoiceProviderRouterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TProvider extends string = string> = {
60
74
  allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
61
75
  fallback?: TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
@@ -64,6 +78,7 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
64
78
  isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
65
79
  onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
66
80
  policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
81
+ providerHealth?: boolean | VoiceProviderRouterHealthOptions;
67
82
  providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
68
83
  providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
69
84
  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.13",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",