@absolutejs/voice 0.0.22-beta.33 → 0.0.22-beta.35
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 +82 -2
- package/dist/providerAdapters.d.ts +5 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +118 -0
- package/dist/testing/ioProviderSimulator.d.ts +37 -0
- package/package.json +1 -1
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
|
|
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/dist/testing/index.d.ts
CHANGED
package/dist/testing/index.js
CHANGED
|
@@ -3510,6 +3510,123 @@ var loadVoiceTestFixtures = async (fixtureDirectory) => {
|
|
|
3510
3510
|
}
|
|
3511
3511
|
return fixtures;
|
|
3512
3512
|
};
|
|
3513
|
+
// src/testing/ioProviderSimulator.ts
|
|
3514
|
+
var defaultFailureMessage = (input) => `Simulated ${input.provider} ${input.kind.toUpperCase()} ${input.operation} failure.`;
|
|
3515
|
+
var resolveRecoveryElapsedMs = (value, provider) => {
|
|
3516
|
+
if (typeof value === "number") {
|
|
3517
|
+
return value;
|
|
3518
|
+
}
|
|
3519
|
+
return value?.[provider] ?? 25;
|
|
3520
|
+
};
|
|
3521
|
+
var createHealth = (input) => ({
|
|
3522
|
+
consecutiveFailures: input.status === "healthy" ? 0 : 1,
|
|
3523
|
+
lastFailureAt: input.status === "healthy" ? undefined : input.now,
|
|
3524
|
+
provider: input.provider,
|
|
3525
|
+
status: input.status,
|
|
3526
|
+
suppressedUntil: input.suppressedUntil
|
|
3527
|
+
});
|
|
3528
|
+
var resolveFallback = async (options, provider) => {
|
|
3529
|
+
const configured = typeof options.fallback === "function" ? await options.fallback(provider) : options.fallback;
|
|
3530
|
+
return (configured ?? options.providers).find((candidate) => candidate !== provider);
|
|
3531
|
+
};
|
|
3532
|
+
var createVoiceIOProviderFailureSimulator = (options) => {
|
|
3533
|
+
if (options.providers.length === 0) {
|
|
3534
|
+
throw new Error("At least one provider is required.");
|
|
3535
|
+
}
|
|
3536
|
+
const now = options.now ?? Date.now;
|
|
3537
|
+
const operation = options.operation ?? "open";
|
|
3538
|
+
const cooldownMs = Math.max(0, options.cooldownMs ?? 30000);
|
|
3539
|
+
const emit = async (event) => {
|
|
3540
|
+
await options.onProviderEvent?.(event);
|
|
3541
|
+
};
|
|
3542
|
+
const run = async (provider, mode) => {
|
|
3543
|
+
if (!options.providers.includes(provider)) {
|
|
3544
|
+
throw new Error(`${provider} is not configured for simulation.`);
|
|
3545
|
+
}
|
|
3546
|
+
const startedAt = now();
|
|
3547
|
+
const sessionId = options.sessionId?.({ mode, now: startedAt, provider }) ?? `${options.kind}-provider-sim-${startedAt}`;
|
|
3548
|
+
if (mode === "recovery") {
|
|
3549
|
+
await emit({
|
|
3550
|
+
at: startedAt,
|
|
3551
|
+
attempt: 0,
|
|
3552
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, provider),
|
|
3553
|
+
kind: options.kind,
|
|
3554
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3555
|
+
operation,
|
|
3556
|
+
provider,
|
|
3557
|
+
providerHealth: createHealth({
|
|
3558
|
+
now: startedAt,
|
|
3559
|
+
provider,
|
|
3560
|
+
status: "healthy"
|
|
3561
|
+
}),
|
|
3562
|
+
selectedProvider: provider,
|
|
3563
|
+
status: "success"
|
|
3564
|
+
});
|
|
3565
|
+
return {
|
|
3566
|
+
mode,
|
|
3567
|
+
provider,
|
|
3568
|
+
sessionId,
|
|
3569
|
+
status: "simulated"
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
const fallbackProvider = await resolveFallback(options, provider);
|
|
3573
|
+
const suppressedUntil = startedAt + cooldownMs;
|
|
3574
|
+
await emit({
|
|
3575
|
+
at: startedAt,
|
|
3576
|
+
attempt: 0,
|
|
3577
|
+
elapsedMs: options.failureElapsedMs ?? 10,
|
|
3578
|
+
error: (options.failureMessage ?? defaultFailureMessage)({
|
|
3579
|
+
kind: options.kind,
|
|
3580
|
+
operation,
|
|
3581
|
+
provider
|
|
3582
|
+
}),
|
|
3583
|
+
fallbackProvider,
|
|
3584
|
+
kind: options.kind,
|
|
3585
|
+
latencyBudgetMs: options.latencyBudgets?.[provider],
|
|
3586
|
+
operation,
|
|
3587
|
+
provider,
|
|
3588
|
+
providerHealth: createHealth({
|
|
3589
|
+
now: startedAt,
|
|
3590
|
+
provider,
|
|
3591
|
+
status: "suppressed",
|
|
3592
|
+
suppressedUntil
|
|
3593
|
+
}),
|
|
3594
|
+
selectedProvider: provider,
|
|
3595
|
+
status: "error",
|
|
3596
|
+
suppressedUntil
|
|
3597
|
+
});
|
|
3598
|
+
if (fallbackProvider) {
|
|
3599
|
+
await emit({
|
|
3600
|
+
at: startedAt + 1,
|
|
3601
|
+
attempt: 1,
|
|
3602
|
+
elapsedMs: resolveRecoveryElapsedMs(options.recoveryElapsedMs, fallbackProvider),
|
|
3603
|
+
fallbackProvider,
|
|
3604
|
+
kind: options.kind,
|
|
3605
|
+
latencyBudgetMs: options.latencyBudgets?.[fallbackProvider],
|
|
3606
|
+
operation,
|
|
3607
|
+
provider: fallbackProvider,
|
|
3608
|
+
providerHealth: createHealth({
|
|
3609
|
+
now: startedAt + 1,
|
|
3610
|
+
provider: fallbackProvider,
|
|
3611
|
+
status: "healthy"
|
|
3612
|
+
}),
|
|
3613
|
+
selectedProvider: provider,
|
|
3614
|
+
status: "fallback"
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
return {
|
|
3618
|
+
fallbackProvider,
|
|
3619
|
+
mode,
|
|
3620
|
+
provider,
|
|
3621
|
+
sessionId,
|
|
3622
|
+
status: "simulated",
|
|
3623
|
+
suppressedUntil
|
|
3624
|
+
};
|
|
3625
|
+
};
|
|
3626
|
+
return {
|
|
3627
|
+
run
|
|
3628
|
+
};
|
|
3629
|
+
};
|
|
3513
3630
|
// src/modelAdapters.ts
|
|
3514
3631
|
var OUTPUT_SCHEMA = {
|
|
3515
3632
|
additionalProperties: false,
|
|
@@ -8551,6 +8668,7 @@ export {
|
|
|
8551
8668
|
getDefaultTTSBenchmarkFixtures,
|
|
8552
8669
|
evaluateSTTBenchmarkAcceptance,
|
|
8553
8670
|
createVoiceProviderFailureSimulator,
|
|
8671
|
+
createVoiceIOProviderFailureSimulator,
|
|
8554
8672
|
createVoiceCallReviewRecorder,
|
|
8555
8673
|
createVoiceCallReviewFromLiveTelephonyReport,
|
|
8556
8674
|
createTelephonyVoiceTestFixtures,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { VoiceIOProviderRouterEvent } from '../providerAdapters';
|
|
2
|
+
export type VoiceIOProviderFailureSimulationMode = 'failure' | 'recovery';
|
|
3
|
+
export type VoiceIOProviderFailureSimulationKind = 'stt' | 'tts';
|
|
4
|
+
export type VoiceIOProviderFailureSimulationOperation = 'open' | 'send';
|
|
5
|
+
export type VoiceIOProviderFailureSimulationResult<TProvider extends string = string> = {
|
|
6
|
+
fallbackProvider?: TProvider;
|
|
7
|
+
mode: VoiceIOProviderFailureSimulationMode;
|
|
8
|
+
provider: TProvider;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
status: 'simulated';
|
|
11
|
+
suppressedUntil?: number;
|
|
12
|
+
};
|
|
13
|
+
export type VoiceIOProviderFailureSimulatorOptions<TProvider extends string = string> = {
|
|
14
|
+
cooldownMs?: number;
|
|
15
|
+
fallback?: readonly TProvider[] | ((provider: TProvider) => readonly TProvider[] | Promise<readonly TProvider[]>);
|
|
16
|
+
failureElapsedMs?: number;
|
|
17
|
+
failureMessage?: (input: {
|
|
18
|
+
kind: VoiceIOProviderFailureSimulationKind;
|
|
19
|
+
operation: VoiceIOProviderFailureSimulationOperation;
|
|
20
|
+
provider: TProvider;
|
|
21
|
+
}) => string;
|
|
22
|
+
kind: VoiceIOProviderFailureSimulationKind;
|
|
23
|
+
latencyBudgets?: Partial<Record<TProvider, number>>;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
onProviderEvent?: (event: VoiceIOProviderRouterEvent<TProvider>) => Promise<void> | void;
|
|
26
|
+
operation?: VoiceIOProviderFailureSimulationOperation;
|
|
27
|
+
providers: readonly TProvider[];
|
|
28
|
+
recoveryElapsedMs?: number | Partial<Record<TProvider, number>>;
|
|
29
|
+
sessionId?: (input: {
|
|
30
|
+
mode: VoiceIOProviderFailureSimulationMode;
|
|
31
|
+
now: number;
|
|
32
|
+
provider: TProvider;
|
|
33
|
+
}) => string;
|
|
34
|
+
};
|
|
35
|
+
export declare const createVoiceIOProviderFailureSimulator: <TProvider extends string>(options: VoiceIOProviderFailureSimulatorOptions<TProvider>) => {
|
|
36
|
+
run: (provider: TProvider, mode: VoiceIOProviderFailureSimulationMode) => Promise<VoiceIOProviderFailureSimulationResult<TProvider>>;
|
|
37
|
+
};
|