@absolutejs/voice 0.0.22-beta.31 → 0.0.22-beta.32
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 +50 -4
- package/dist/modelAdapters.d.ts +6 -0
- package/dist/providerHealth.d.ts +1 -0
- package/dist/testing/index.js +42 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6552,7 +6552,8 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6552
6552
|
rateLimited: false,
|
|
6553
6553
|
recommended: false,
|
|
6554
6554
|
runCount: 0,
|
|
6555
|
-
status: "idle"
|
|
6555
|
+
status: "idle",
|
|
6556
|
+
timeoutCount: 0
|
|
6556
6557
|
};
|
|
6557
6558
|
entries.set(provider, entry);
|
|
6558
6559
|
return entry;
|
|
@@ -6629,6 +6630,9 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6629
6630
|
}
|
|
6630
6631
|
const entry = applyProviderHealth();
|
|
6631
6632
|
entry.errorCount += 1;
|
|
6633
|
+
if (event.payload.timedOut === true) {
|
|
6634
|
+
entry.timeoutCount += 1;
|
|
6635
|
+
}
|
|
6632
6636
|
entry.lastError = getString(event.payload.error);
|
|
6633
6637
|
entry.lastErrorAt = event.at;
|
|
6634
6638
|
entry.rateLimited ||= event.payload.rateLimited === true;
|
|
@@ -6653,7 +6657,8 @@ var summarizeVoiceProviderHealth = async (input) => {
|
|
|
6653
6657
|
runCount: entry.runCount,
|
|
6654
6658
|
status,
|
|
6655
6659
|
suppressionRemainingMs: activeSuppression ? suppressionRemainingMs : undefined,
|
|
6656
|
-
suppressedUntil: entry.suppressedUntil
|
|
6660
|
+
suppressedUntil: entry.suppressedUntil,
|
|
6661
|
+
timeoutCount: entry.timeoutCount
|
|
6657
6662
|
};
|
|
6658
6663
|
});
|
|
6659
6664
|
const recommended = summaries.filter((entry) => entry.status === "healthy").sort((left, right) => (left.averageElapsedMs ?? Number.MAX_SAFE_INTEGER) - (right.averageElapsedMs ?? Number.MAX_SAFE_INTEGER))[0];
|
|
@@ -6677,6 +6682,7 @@ var renderVoiceProviderHealthHTML = (providers) => providers.length === 0 ? '<p
|
|
|
6677
6682
|
`<div><dt>Runs</dt><dd>${String(provider.runCount)}</dd></div>`,
|
|
6678
6683
|
`<div><dt>Avg latency</dt><dd>${String(provider.averageElapsedMs ?? 0)}ms</dd></div>`,
|
|
6679
6684
|
`<div><dt>Errors</dt><dd>${String(provider.errorCount)}</dd></div>`,
|
|
6685
|
+
`<div><dt>Timeouts</dt><dd>${String(provider.timeoutCount)}</dd></div>`,
|
|
6680
6686
|
`<div><dt>Fallbacks</dt><dd>${String(provider.fallbackCount)}</dd></div>`,
|
|
6681
6687
|
"</dl>",
|
|
6682
6688
|
suppressionSeconds ? `<p>Temporarily suppressed for ${String(suppressionSeconds)}s.</p>` : "",
|
|
@@ -8132,6 +8138,17 @@ var parseJSONValue = (value) => {
|
|
|
8132
8138
|
return value;
|
|
8133
8139
|
}
|
|
8134
8140
|
};
|
|
8141
|
+
|
|
8142
|
+
class VoiceProviderTimeoutError extends Error {
|
|
8143
|
+
provider;
|
|
8144
|
+
timeoutMs;
|
|
8145
|
+
constructor(provider, timeoutMs) {
|
|
8146
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
8147
|
+
this.name = "VoiceProviderTimeoutError";
|
|
8148
|
+
this.provider = provider;
|
|
8149
|
+
this.timeoutMs = timeoutMs;
|
|
8150
|
+
}
|
|
8151
|
+
}
|
|
8135
8152
|
var getMessageToolCalls = (message) => {
|
|
8136
8153
|
const toolCalls = message.metadata?.toolCalls;
|
|
8137
8154
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -8209,6 +8226,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8209
8226
|
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
8210
8227
|
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
8211
8228
|
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
8229
|
+
const getProviderTimeoutMs = (provider) => {
|
|
8230
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
8231
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
8232
|
+
};
|
|
8212
8233
|
const getHealth = (provider) => {
|
|
8213
8234
|
const existing = healthState.get(provider);
|
|
8214
8235
|
if (existing) {
|
|
@@ -8320,6 +8341,25 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8320
8341
|
const emit = async (event, input) => {
|
|
8321
8342
|
await options.onProviderEvent?.(event, input);
|
|
8322
8343
|
};
|
|
8344
|
+
const runProvider = async (provider, model, input) => {
|
|
8345
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
8346
|
+
if (!timeoutMs) {
|
|
8347
|
+
return model.generate(input);
|
|
8348
|
+
}
|
|
8349
|
+
let timeout;
|
|
8350
|
+
try {
|
|
8351
|
+
return await Promise.race([
|
|
8352
|
+
model.generate(input),
|
|
8353
|
+
new Promise((_, reject) => {
|
|
8354
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
8355
|
+
})
|
|
8356
|
+
]);
|
|
8357
|
+
} finally {
|
|
8358
|
+
if (timeout) {
|
|
8359
|
+
clearTimeout(timeout);
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
};
|
|
8323
8363
|
return {
|
|
8324
8364
|
generate: async (input) => {
|
|
8325
8365
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -8334,12 +8374,14 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8334
8374
|
}
|
|
8335
8375
|
const startedAt = Date.now();
|
|
8336
8376
|
try {
|
|
8337
|
-
const output = await model
|
|
8377
|
+
const output = await runProvider(provider, model, input);
|
|
8338
8378
|
const providerHealth = recordProviderSuccess(provider);
|
|
8339
8379
|
await emit({
|
|
8340
8380
|
at: Date.now(),
|
|
8381
|
+
attempt: index + 1,
|
|
8341
8382
|
elapsedMs: Date.now() - startedAt,
|
|
8342
8383
|
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
8384
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8343
8385
|
provider,
|
|
8344
8386
|
providerHealth,
|
|
8345
8387
|
recovered: provider !== selectedProvider,
|
|
@@ -8351,22 +8393,26 @@ var createVoiceProviderRouter = (options) => {
|
|
|
8351
8393
|
lastError = error;
|
|
8352
8394
|
const hasNextProvider = index < order.length - 1;
|
|
8353
8395
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
8396
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
8354
8397
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
8355
8398
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
8356
8399
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
8357
8400
|
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
8358
8401
|
await emit({
|
|
8359
8402
|
at: Date.now(),
|
|
8403
|
+
attempt: index + 1,
|
|
8360
8404
|
elapsedMs: Date.now() - startedAt,
|
|
8361
8405
|
error: errorMessage(error),
|
|
8362
8406
|
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
8407
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
8363
8408
|
provider,
|
|
8364
8409
|
providerHealth,
|
|
8365
8410
|
rateLimited,
|
|
8366
8411
|
selectedProvider,
|
|
8367
8412
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
8368
8413
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
8369
|
-
status: "error"
|
|
8414
|
+
status: "error",
|
|
8415
|
+
timedOut
|
|
8370
8416
|
}, input);
|
|
8371
8417
|
if (!hasNextProvider || !shouldFallback) {
|
|
8372
8418
|
throw error;
|
package/dist/modelAdapters.d.ts
CHANGED
|
@@ -36,9 +36,11 @@ export type GeminiVoiceAssistantModelOptions = {
|
|
|
36
36
|
};
|
|
37
37
|
export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
38
38
|
at: number;
|
|
39
|
+
attempt: number;
|
|
39
40
|
elapsedMs: number;
|
|
40
41
|
error?: string;
|
|
41
42
|
fallbackProvider?: TProvider;
|
|
43
|
+
latencyBudgetMs?: number;
|
|
42
44
|
provider: TProvider;
|
|
43
45
|
providerHealth?: VoiceProviderRouterProviderHealth<TProvider>;
|
|
44
46
|
rateLimited?: boolean;
|
|
@@ -47,6 +49,7 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
|
47
49
|
suppressionRemainingMs?: number;
|
|
48
50
|
suppressedUntil?: number;
|
|
49
51
|
status: 'error' | 'fallback' | 'success';
|
|
52
|
+
timedOut?: boolean;
|
|
50
53
|
};
|
|
51
54
|
export type VoiceProviderRouterFallbackMode = 'never' | 'provider-error' | 'rate-limit';
|
|
52
55
|
export type VoiceProviderRouterPolicy<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TProvider extends string = string> = 'ordered' | 'prefer-cheapest' | 'prefer-fastest' | 'prefer-selected' | {
|
|
@@ -58,6 +61,7 @@ export type VoiceProviderRouterProviderProfile = {
|
|
|
58
61
|
cost?: number;
|
|
59
62
|
latencyMs?: number;
|
|
60
63
|
priority?: number;
|
|
64
|
+
timeoutMs?: number;
|
|
61
65
|
};
|
|
62
66
|
export type VoiceProviderRouterHealthOptions = {
|
|
63
67
|
cooldownMs?: number;
|
|
@@ -79,10 +83,12 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
|
|
|
79
83
|
fallbackMode?: VoiceProviderRouterFallbackMode;
|
|
80
84
|
isProviderError?: (error: unknown, provider: TProvider) => boolean;
|
|
81
85
|
isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
|
|
86
|
+
isTimeoutError?: (error: unknown, provider: TProvider) => boolean;
|
|
82
87
|
onProviderEvent?: (event: VoiceProviderRouterEvent<TProvider>, input: VoiceAgentModelInput<TContext, TSession>) => Promise<void> | void;
|
|
83
88
|
policy?: VoiceProviderRouterPolicy<TContext, TSession, TProvider>;
|
|
84
89
|
providerHealth?: boolean | VoiceProviderRouterHealthOptions;
|
|
85
90
|
providerProfiles?: Partial<Record<TProvider, VoiceProviderRouterProviderProfile>>;
|
|
91
|
+
timeoutMs?: number;
|
|
86
92
|
providers: Partial<Record<TProvider, VoiceAgentModel<TContext, TSession, TResult>>>;
|
|
87
93
|
selectProvider?: (input: VoiceAgentModelInput<TContext, TSession>) => TProvider | undefined | Promise<TProvider | undefined>;
|
|
88
94
|
};
|
package/dist/providerHealth.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type VoiceProviderHealthSummary<TProvider extends string = string> = {
|
|
|
15
15
|
status: VoiceProviderHealthStatus;
|
|
16
16
|
suppressionRemainingMs?: number;
|
|
17
17
|
suppressedUntil?: number;
|
|
18
|
+
timeoutCount: number;
|
|
18
19
|
};
|
|
19
20
|
export type VoiceProviderHealthSummaryOptions<TProvider extends string = string> = {
|
|
20
21
|
events?: StoredVoiceTraceEvent[];
|
package/dist/testing/index.js
CHANGED
|
@@ -3601,6 +3601,17 @@ var parseJSONValue = (value) => {
|
|
|
3601
3601
|
return value;
|
|
3602
3602
|
}
|
|
3603
3603
|
};
|
|
3604
|
+
|
|
3605
|
+
class VoiceProviderTimeoutError extends Error {
|
|
3606
|
+
provider;
|
|
3607
|
+
timeoutMs;
|
|
3608
|
+
constructor(provider, timeoutMs) {
|
|
3609
|
+
super(`Voice provider ${provider} exceeded ${timeoutMs}ms latency budget.`);
|
|
3610
|
+
this.name = "VoiceProviderTimeoutError";
|
|
3611
|
+
this.provider = provider;
|
|
3612
|
+
this.timeoutMs = timeoutMs;
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3604
3615
|
var getMessageToolCalls = (message) => {
|
|
3605
3616
|
const toolCalls = message.metadata?.toolCalls;
|
|
3606
3617
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -3678,6 +3689,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3678
3689
|
const failureThreshold = Math.max(1, healthOptions?.failureThreshold ?? 1);
|
|
3679
3690
|
const cooldownMs = Math.max(0, healthOptions?.cooldownMs ?? 30000);
|
|
3680
3691
|
const rateLimitCooldownMs = Math.max(0, healthOptions?.rateLimitCooldownMs ?? 60000);
|
|
3692
|
+
const getProviderTimeoutMs = (provider) => {
|
|
3693
|
+
const timeoutMs = options.providerProfiles?.[provider]?.timeoutMs ?? options.timeoutMs;
|
|
3694
|
+
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : undefined;
|
|
3695
|
+
};
|
|
3681
3696
|
const getHealth = (provider) => {
|
|
3682
3697
|
const existing = healthState.get(provider);
|
|
3683
3698
|
if (existing) {
|
|
@@ -3789,6 +3804,25 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3789
3804
|
const emit = async (event, input) => {
|
|
3790
3805
|
await options.onProviderEvent?.(event, input);
|
|
3791
3806
|
};
|
|
3807
|
+
const runProvider = async (provider, model, input) => {
|
|
3808
|
+
const timeoutMs = getProviderTimeoutMs(provider);
|
|
3809
|
+
if (!timeoutMs) {
|
|
3810
|
+
return model.generate(input);
|
|
3811
|
+
}
|
|
3812
|
+
let timeout;
|
|
3813
|
+
try {
|
|
3814
|
+
return await Promise.race([
|
|
3815
|
+
model.generate(input),
|
|
3816
|
+
new Promise((_, reject) => {
|
|
3817
|
+
timeout = setTimeout(() => reject(new VoiceProviderTimeoutError(provider, timeoutMs)), timeoutMs);
|
|
3818
|
+
})
|
|
3819
|
+
]);
|
|
3820
|
+
} finally {
|
|
3821
|
+
if (timeout) {
|
|
3822
|
+
clearTimeout(timeout);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
};
|
|
3792
3826
|
return {
|
|
3793
3827
|
generate: async (input) => {
|
|
3794
3828
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -3803,12 +3837,14 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3803
3837
|
}
|
|
3804
3838
|
const startedAt = Date.now();
|
|
3805
3839
|
try {
|
|
3806
|
-
const output = await model
|
|
3840
|
+
const output = await runProvider(provider, model, input);
|
|
3807
3841
|
const providerHealth = recordProviderSuccess(provider);
|
|
3808
3842
|
await emit({
|
|
3809
3843
|
at: Date.now(),
|
|
3844
|
+
attempt: index + 1,
|
|
3810
3845
|
elapsedMs: Date.now() - startedAt,
|
|
3811
3846
|
fallbackProvider: provider === selectedProvider ? undefined : provider,
|
|
3847
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3812
3848
|
provider,
|
|
3813
3849
|
providerHealth,
|
|
3814
3850
|
recovered: provider !== selectedProvider,
|
|
@@ -3820,22 +3856,26 @@ var createVoiceProviderRouter = (options) => {
|
|
|
3820
3856
|
lastError = error;
|
|
3821
3857
|
const hasNextProvider = index < order.length - 1;
|
|
3822
3858
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
3859
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
3823
3860
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
3824
3861
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
3825
3862
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
3826
3863
|
const nextProvider = hasNextProvider ? order[index + 1] : undefined;
|
|
3827
3864
|
await emit({
|
|
3828
3865
|
at: Date.now(),
|
|
3866
|
+
attempt: index + 1,
|
|
3829
3867
|
elapsedMs: Date.now() - startedAt,
|
|
3830
3868
|
error: errorMessage(error),
|
|
3831
3869
|
fallbackProvider: shouldFallback ? nextProvider : undefined,
|
|
3870
|
+
latencyBudgetMs: getProviderTimeoutMs(provider),
|
|
3832
3871
|
provider,
|
|
3833
3872
|
providerHealth,
|
|
3834
3873
|
rateLimited,
|
|
3835
3874
|
selectedProvider,
|
|
3836
3875
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
3837
3876
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
3838
|
-
status: "error"
|
|
3877
|
+
status: "error",
|
|
3878
|
+
timedOut
|
|
3839
3879
|
}, input);
|
|
3840
3880
|
if (!hasNextProvider || !shouldFallback) {
|
|
3841
3881
|
throw error;
|