@absolutejs/voice 0.0.22-beta.11 → 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, VoiceProviderRouterOptions, 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
@@ -7241,18 +7241,98 @@ var createJSONVoiceAssistantModel = (options) => ({
7241
7241
  var createVoiceProviderRouter = (options) => {
7242
7242
  const providerIds = Object.keys(options.providers);
7243
7243
  const firstProvider = providerIds[0];
7244
+ const policy = typeof options.policy === "string" ? {
7245
+ strategy: options.policy
7246
+ } : options.policy;
7247
+ const strategy = policy?.strategy ?? "prefer-selected";
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
+ };
7300
+ const resolveAllowedProviders = async (input) => {
7301
+ const allowProviders = policy?.allowProviders ?? options.allowProviders;
7302
+ const allowed = typeof allowProviders === "function" ? await allowProviders(input) : allowProviders;
7303
+ return new Set(allowed ?? providerIds);
7304
+ };
7305
+ const sortProviders = (providers) => {
7306
+ if (strategy !== "prefer-cheapest" && strategy !== "prefer-fastest") {
7307
+ return providers;
7308
+ }
7309
+ return [...providers].sort((left, right) => {
7310
+ const leftProfile = options.providerProfiles?.[left];
7311
+ const rightProfile = options.providerProfiles?.[right];
7312
+ const leftValue = strategy === "prefer-cheapest" ? leftProfile?.cost ?? Number.MAX_SAFE_INTEGER : leftProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
7313
+ const rightValue = strategy === "prefer-cheapest" ? rightProfile?.cost ?? Number.MAX_SAFE_INTEGER : rightProfile?.latencyMs ?? Number.MAX_SAFE_INTEGER;
7314
+ return leftValue - rightValue || (leftProfile?.priority ?? Number.MAX_SAFE_INTEGER) - (rightProfile?.priority ?? Number.MAX_SAFE_INTEGER);
7315
+ });
7316
+ };
7244
7317
  const resolveOrder = async (input) => {
7245
7318
  const selectedProvider = await options.selectProvider?.(input);
7319
+ const allowedProviders = await resolveAllowedProviders(input);
7246
7320
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
7247
- const preferred = selectedProvider ?? fallbackOrder?.[0] ?? firstProvider;
7321
+ const rankedProviders = sortProviders([
7322
+ ...fallbackOrder ?? providerIds
7323
+ ]).filter((provider) => allowedProviders.has(provider));
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;
7248
7327
  const seen = new Set;
7249
7328
  const order = [];
7250
- for (const provider of [
7329
+ const candidates = strategy === "ordered" ? candidateRankedProviders : [
7251
7330
  preferred,
7252
- ...fallbackOrder ?? [],
7253
- ...providerIds
7254
- ]) {
7255
- if (!provider || seen.has(provider) || !options.providers[provider]) {
7331
+ ...candidateRankedProviders,
7332
+ ...providerIds.filter((provider) => !healthOptions || !isSuppressed(provider))
7333
+ ];
7334
+ for (const provider of candidates) {
7335
+ if (!provider || seen.has(provider) || !allowedProviders.has(provider) || !options.providers[provider]) {
7256
7336
  continue;
7257
7337
  }
7258
7338
  seen.add(provider);
@@ -7281,6 +7361,7 @@ var createVoiceProviderRouter = (options) => {
7281
7361
  const startedAt = Date.now();
7282
7362
  try {
7283
7363
  const output = await model.generate(input);
7364
+ recordProviderSuccess(provider);
7284
7365
  await emit({
7285
7366
  at: Date.now(),
7286
7367
  elapsedMs: Date.now() - startedAt,
@@ -7295,18 +7376,21 @@ var createVoiceProviderRouter = (options) => {
7295
7376
  lastError = error;
7296
7377
  const hasNextProvider = index < order.length - 1;
7297
7378
  const isProviderError = options.isProviderError?.(error, provider) ?? true;
7379
+ const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
7380
+ const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
7381
+ recordProviderError(provider, isProviderError, rateLimited);
7298
7382
  const nextProvider = hasNextProvider ? order[index + 1] : undefined;
7299
7383
  await emit({
7300
7384
  at: Date.now(),
7301
7385
  elapsedMs: Date.now() - startedAt,
7302
7386
  error: errorMessage(error),
7303
- fallbackProvider: isProviderError ? nextProvider : undefined,
7387
+ fallbackProvider: shouldFallback ? nextProvider : undefined,
7304
7388
  provider,
7305
- rateLimited: options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error),
7389
+ rateLimited,
7306
7390
  selectedProvider,
7307
7391
  status: "error"
7308
7392
  }, input);
7309
- if (!hasNextProvider || !isProviderError) {
7393
+ if (!hasNextProvider || !shouldFallback) {
7310
7394
  throw error;
7311
7395
  }
7312
7396
  }
@@ -45,11 +45,41 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
45
45
  selectedProvider: TProvider;
46
46
  status: 'error' | 'fallback' | 'success';
47
47
  };
48
+ export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
49
+ export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | {
50
+ allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
51
+ fallbackMode?: VoiceProviderRouterFallbackMode;
52
+ strategy?: 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected';
53
+ };
54
+ export type VoiceProviderRouterProviderProfile = {
55
+ cost?: number;
56
+ latencyMs?: number;
57
+ priority?: number;
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
+ };
48
73
  export type VoiceProviderRouterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TProvider extends string = string> = {
74
+ allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
49
75
  fallback?: TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
76
+ fallbackMode?: VoiceProviderRouterFallbackMode;
50
77
  isProviderError?: (error: unknown, provider: TProvider) => boolean;
51
78
  isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
52
79
  onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
80
+ policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
81
+ providerHealth?: boolean | VoiceProviderRouterHealthOptions;
82
+ providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
53
83
  providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
54
84
  selectProvider?: (input: VoiceAgentModelInput<TContext, TSession>) => TProvider | undefined | Promise<TProvider | undefined>;
55
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.11",
3
+ "version": "0.0.22-beta.13",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",