@absolutejs/voice 0.0.22-beta.33 → 0.0.22-beta.34

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.js CHANGED
@@ -8911,21 +8911,86 @@ var withTimeout = async (input) => {
8911
8911
  var createResolver = (options) => {
8912
8912
  const providerIds = Object.keys(options.adapters);
8913
8913
  const firstProvider = providerIds[0];
8914
+ const healthOptions = typeof options.providerHealth === "object" ? options.providerHealth : options.providerHealth ? {} : undefined;
8915
+ const healthState = new Map;
8916
+ const now = () => healthOptions?.now?.() ?? Date.now();
8917
+ const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
8918
+ const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
8919
+ const getHealth = (provider) => {
8920
+ const existing = healthState.get(provider);
8921
+ if (existing) {
8922
+ return existing;
8923
+ }
8924
+ const next = {
8925
+ consecutiveFailures: 0,
8926
+ provider,
8927
+ status: "healthy"
8928
+ };
8929
+ healthState.set(provider, next);
8930
+ return next;
8931
+ };
8932
+ const cloneHealth = (provider) => {
8933
+ if (!healthOptions) {
8934
+ return;
8935
+ }
8936
+ return {
8937
+ ...getHealth(provider)
8938
+ };
8939
+ };
8940
+ const getSuppressionRemainingMs = (provider) => {
8941
+ if (!healthOptions) {
8942
+ return;
8943
+ }
8944
+ const suppressedUntil = getHealth(provider).suppressedUntil;
8945
+ return typeof suppressedUntil === "number" ? Math.max(0, suppressedUntil - now()) : undefined;
8946
+ };
8947
+ const isSuppressed = (provider) => {
8948
+ if (!healthOptions) {
8949
+ return false;
8950
+ }
8951
+ const suppressedUntil = getHealth(provider).suppressedUntil;
8952
+ return typeof suppressedUntil === "number" && suppressedUntil > now();
8953
+ };
8954
+ const recordSuccess = (provider) => {
8955
+ if (!healthOptions) {
8956
+ return;
8957
+ }
8958
+ const health = getHealth(provider);
8959
+ health.consecutiveFailures = 0;
8960
+ health.status = "healthy";
8961
+ health.suppressedUntil = undefined;
8962
+ return cloneHealth(provider);
8963
+ };
8964
+ const recordError = (provider, isProviderError) => {
8965
+ if (!healthOptions || !isProviderError) {
8966
+ return cloneHealth(provider);
8967
+ }
8968
+ const health = getHealth(provider);
8969
+ health.consecutiveFailures += 1;
8970
+ health.lastFailureAt = now();
8971
+ if (health.consecutiveFailures >= failureThreshold) {
8972
+ health.status = "suppressed";
8973
+ health.suppressedUntil = now() + cooldownMs;
8974
+ }
8975
+ return cloneHealth(provider);
8976
+ };
8914
8977
  const resolveOrder = async (input) => {
8915
8978
  const selectedProvider = await options.selectProvider?.(input) ?? firstProvider;
8916
8979
  const fallbackOrder = typeof options.fallback === "function" ? await options.fallback(input) : options.fallback;
8917
8980
  const candidates = [selectedProvider, ...fallbackOrder ?? providerIds];
8918
8981
  const seen = new Set;
8919
- const order = candidates.filter((provider) => {
8982
+ const rankedOrder = candidates.filter((provider) => {
8920
8983
  if (!provider || seen.has(provider) || !options.adapters[provider]) {
8921
8984
  return false;
8922
8985
  }
8923
8986
  seen.add(provider);
8924
8987
  return true;
8925
8988
  });
8989
+ const healthyOrder = healthOptions ? rankedOrder.filter((provider) => !isSuppressed(provider)) : rankedOrder;
8990
+ const order = healthyOrder.length ? healthyOrder : rankedOrder;
8926
8991
  return {
8927
8992
  order,
8928
- selectedProvider
8993
+ selectedProvider: selectedProvider && !isSuppressed(selectedProvider) ? selectedProvider : order[0]
8929
8994
  };
8930
8995
  };
8931
8996
  const emit = async (event, input) => {
@@ -8933,7 +8998,10 @@ var createResolver = (options) => {
8933
8998
  };
8934
8999
  return {
8935
9000
  emit,
9001
+ getSuppressionRemainingMs,
8936
9002
  providerIds,
9003
+ recordError,
9004
+ recordSuccess,
8937
9005
  resolveOrder
8938
9006
  };
8939
9007
  };
@@ -8961,6 +9029,7 @@ var createVoiceSTTProviderRouter = (options) => {
8961
9029
  run: () => adapter.open(input),
8962
9030
  timeoutMs: getTimeoutMs(options, provider)
8963
9031
  });
9032
+ const providerHealth = resolver.recordSuccess(provider);
8964
9033
  await resolver.emit({
8965
9034
  at: Date.now(),
8966
9035
  attempt: index + 1,
@@ -8970,6 +9039,7 @@ var createVoiceSTTProviderRouter = (options) => {
8970
9039
  latencyBudgetMs: getTimeoutMs(options, provider),
8971
9040
  operation: "open",
8972
9041
  provider,
9042
+ providerHealth,
8973
9043
  selectedProvider,
8974
9044
  status: provider === selectedProvider ? "success" : "fallback"
8975
9045
  }, input);
@@ -8978,6 +9048,7 @@ var createVoiceSTTProviderRouter = (options) => {
8978
9048
  lastError = error;
8979
9049
  const hasNextProvider = index < order.length - 1;
8980
9050
  const shouldFallback = options.isProviderError?.(error, provider) ?? true;
9051
+ const providerHealth = resolver.recordError(provider, shouldFallback);
8981
9052
  await resolver.emit({
8982
9053
  at: Date.now(),
8983
9054
  attempt: index + 1,
@@ -8988,8 +9059,11 @@ var createVoiceSTTProviderRouter = (options) => {
8988
9059
  latencyBudgetMs: getTimeoutMs(options, provider),
8989
9060
  operation: "open",
8990
9061
  provider,
9062
+ providerHealth,
8991
9063
  selectedProvider,
8992
9064
  status: "error",
9065
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(provider),
9066
+ suppressedUntil: providerHealth?.suppressedUntil,
8993
9067
  timedOut: error instanceof VoiceIOProviderTimeoutError
8994
9068
  }, input);
8995
9069
  if (!hasNextProvider || !shouldFallback) {
@@ -9035,6 +9109,7 @@ var createVoiceTTSProviderRouter = (options) => {
9035
9109
  attach(session);
9036
9110
  activeSession = session;
9037
9111
  activeProvider = provider;
9112
+ const providerHealth = resolver.recordSuccess(provider);
9038
9113
  await resolver.emit({
9039
9114
  at: Date.now(),
9040
9115
  attempt,
@@ -9044,6 +9119,7 @@ var createVoiceTTSProviderRouter = (options) => {
9044
9119
  latencyBudgetMs: getTimeoutMs(options, provider),
9045
9120
  operation: "open",
9046
9121
  provider,
9122
+ providerHealth,
9047
9123
  selectedProvider,
9048
9124
  status: provider === selectedProvider ? "success" : "fallback"
9049
9125
  }, input);
@@ -9051,6 +9127,7 @@ var createVoiceTTSProviderRouter = (options) => {
9051
9127
  };
9052
9128
  const failProvider = async (inputEvent) => {
9053
9129
  const shouldFallback = options.isProviderError?.(inputEvent.error, inputEvent.provider) ?? true;
9130
+ const providerHealth = resolver.recordError(inputEvent.provider, shouldFallback);
9054
9131
  await resolver.emit({
9055
9132
  at: Date.now(),
9056
9133
  attempt: inputEvent.attempt,
@@ -9061,8 +9138,11 @@ var createVoiceTTSProviderRouter = (options) => {
9061
9138
  latencyBudgetMs: getTimeoutMs(options, inputEvent.provider),
9062
9139
  operation: inputEvent.operation,
9063
9140
  provider: inputEvent.provider,
9141
+ providerHealth,
9064
9142
  selectedProvider,
9065
9143
  status: "error",
9144
+ suppressionRemainingMs: resolver.getSuppressionRemainingMs(inputEvent.provider),
9145
+ suppressedUntil: providerHealth?.suppressedUntil,
9066
9146
  timedOut: inputEvent.error instanceof VoiceIOProviderTimeoutError
9067
9147
  }, input);
9068
9148
  return shouldFallback;
@@ -1,5 +1,5 @@
1
1
  import type { STTAdapter, STTAdapterOpenOptions, TTSAdapter, TTSAdapterOpenOptions } from './types';
2
- import type { VoiceProviderRouterProviderProfile } from './modelAdapters';
2
+ import type { VoiceProviderRouterHealthOptions, VoiceProviderRouterProviderHealth, VoiceProviderRouterProviderProfile } from './modelAdapters';
3
3
  type MaybePromise<T> = T | Promise<T>;
4
4
  type VoiceIOProviderKind = 'stt' | 'tts';
5
5
  type VoiceIOProviderStatus = 'error' | 'fallback' | 'success';
@@ -13,8 +13,11 @@ export type VoiceIOProviderRouterEvent<TProvider extends string = string> = {
13
13
  latencyBudgetMs?: number;
14
14
  operation: 'open' | 'send';
15
15
  provider: TProvider;
16
+ providerHealth?: VoiceProviderRouterProviderHealth<TProvider>;
16
17
  selectedProvider: TProvider;
17
18
  status: VoiceIOProviderStatus;
19
+ suppressionRemainingMs?: number;
20
+ suppressedUntil?: number;
18
21
  timedOut?: boolean;
19
22
  };
20
23
  export type VoiceIOProviderRouterOptions<TProvider extends string, TAdapter, TOpenOptions> = {
@@ -22,6 +25,7 @@ export type VoiceIOProviderRouterOptions<TProvider extends string, TAdapter, TOp
22
25
  fallback?: readonly TProvider[] | ((input: TOpenOptions) => MaybePromise<readonly TProvider[]>);
23
26
  isProviderError?: (error: unknown, provider: TProvider) => boolean;
24
27
  onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>, input: TOpenOptions) => Promise<void> | void;
28
+ providerHealth?: boolean | VoiceProviderRouterHealthOptions;
25
29
  providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
26
30
  selectProvider?: (input: TOpenOptions) => MaybePromise<TProvider | undefined>;
27
31
  timeoutMs?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.33",
3
+ "version": "0.0.22-beta.34",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",