@absolutejs/voice 0.0.22-beta.670 → 0.0.22-beta.672
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/core/modelAdapters.d.ts +5 -0
- package/dist/index.js +32 -11
- package/dist/testing/index.js +28 -9
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ export type OpenAIVoiceAssistantModelOptions = {
|
|
|
13
13
|
model?: string;
|
|
14
14
|
onUsage?: (usage: Record<string, unknown>) => Promise<void> | void;
|
|
15
15
|
temperature?: number;
|
|
16
|
+
/** Maximum silence between SSE body chunks. Default 10 seconds. */
|
|
17
|
+
streamInactivityMs?: number;
|
|
16
18
|
/**
|
|
17
19
|
* Hard cap on a single /responses stream. Aborts via AbortController if
|
|
18
20
|
* the stream doesn't complete in time. Default 60s — generous for typical
|
|
@@ -62,6 +64,9 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
|
62
64
|
suppressedUntil?: number;
|
|
63
65
|
status: "error" | "fallback" | "success";
|
|
64
66
|
timedOut?: boolean;
|
|
67
|
+
timeoutKind?: "latency-budget" | "stream-inactivity";
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
totalElapsedMs?: number;
|
|
65
70
|
};
|
|
66
71
|
export type VoiceProviderRouterFallbackMode = "never" | "provider-error" | "rate-limit";
|
|
67
72
|
export type VoiceProviderRouterStrategy = "balanced" | "ordered" | "prefer-cheapest" | "prefer-fastest" | "prefer-selected" | "quality-first";
|
package/dist/index.js
CHANGED
|
@@ -4434,9 +4434,13 @@ var createVoiceSession = (options) => {
|
|
|
4434
4434
|
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
4435
4435
|
return;
|
|
4436
4436
|
}
|
|
4437
|
+
const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
|
|
4438
|
+
if (lastTranscriptAt !== undefined && stuckCloseAfterMs !== undefined && Date.now() - lastTranscriptAt < stuckCloseAfterMs) {
|
|
4439
|
+
kickStuckCloseWatchdog();
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4437
4442
|
stuckCloseFired = true;
|
|
4438
4443
|
const reason = `no caller progress for ${stuckCloseAfterMs}ms`;
|
|
4439
|
-
const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
|
|
4440
4444
|
const sttDeafConfirmed = sttDeafConfirmedAt > 0 && (lastTranscriptAt === undefined || sttDeafConfirmedAt > lastTranscriptAt);
|
|
4441
4445
|
const diagnostic = {
|
|
4442
4446
|
action: "stuck-call-close",
|
|
@@ -41730,13 +41734,14 @@ var buildVoiceIOProviderRouterTraceEvent = (options) => ({
|
|
|
41730
41734
|
metadata: options.metadata,
|
|
41731
41735
|
payload: {
|
|
41732
41736
|
...options.event,
|
|
41737
|
+
recoverable: options.event.status === "error" && options.event.fallbackProvider !== undefined,
|
|
41733
41738
|
...options.payload ?? {},
|
|
41734
41739
|
providerStatus: options.event.status
|
|
41735
41740
|
},
|
|
41736
41741
|
scenarioId: options.scenarioId,
|
|
41737
41742
|
sessionId: options.sessionId,
|
|
41738
41743
|
turnId: options.turnId,
|
|
41739
|
-
type: options.type ?? "session.error"
|
|
41744
|
+
type: options.type ?? (options.event.status === "error" ? "session.error" : "provider.decision")
|
|
41740
41745
|
});
|
|
41741
41746
|
var buildVoiceProviderRouterTraceEvent = (options) => ({
|
|
41742
41747
|
at: options.at ?? options.event.at,
|
|
@@ -41744,13 +41749,14 @@ var buildVoiceProviderRouterTraceEvent = (options) => ({
|
|
|
41744
41749
|
metadata: options.metadata,
|
|
41745
41750
|
payload: {
|
|
41746
41751
|
...options.event,
|
|
41752
|
+
recoverable: options.event.status === "error" && options.event.fallbackProvider !== undefined,
|
|
41747
41753
|
...options.payload ?? {},
|
|
41748
41754
|
providerStatus: options.event.status
|
|
41749
41755
|
},
|
|
41750
41756
|
scenarioId: options.scenarioId,
|
|
41751
41757
|
sessionId: options.sessionId,
|
|
41752
41758
|
turnId: options.turnId,
|
|
41753
|
-
type: options.type ?? "session.error"
|
|
41759
|
+
type: options.type ?? (options.event.status === "error" ? "session.error" : "provider.decision")
|
|
41754
41760
|
});
|
|
41755
41761
|
// src/core/workflowContract.ts
|
|
41756
41762
|
var getObject2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
@@ -46363,6 +46369,15 @@ class VoiceProviderTimeoutError extends Error {
|
|
|
46363
46369
|
this.timeoutMs = timeoutMs;
|
|
46364
46370
|
}
|
|
46365
46371
|
}
|
|
46372
|
+
|
|
46373
|
+
class VoiceProviderStreamInactivityError extends Error {
|
|
46374
|
+
inactivityMs;
|
|
46375
|
+
constructor(inactivityMs) {
|
|
46376
|
+
super(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`);
|
|
46377
|
+
this.name = "VoiceProviderStreamInactivityError";
|
|
46378
|
+
this.inactivityMs = inactivityMs;
|
|
46379
|
+
}
|
|
46380
|
+
}
|
|
46366
46381
|
var getMessageToolCalls = (message) => {
|
|
46367
46382
|
const toolCalls = message.metadata?.toolCalls;
|
|
46368
46383
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -46609,7 +46624,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46609
46624
|
}
|
|
46610
46625
|
}
|
|
46611
46626
|
};
|
|
46612
|
-
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
|
|
46627
|
+
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt, routingStartedAt) => {
|
|
46613
46628
|
let output = await runProvider(provider, model, input);
|
|
46614
46629
|
let emptyAttempt = 0;
|
|
46615
46630
|
while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
|
|
@@ -46621,7 +46636,8 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46621
46636
|
emptyCompletion: true,
|
|
46622
46637
|
provider,
|
|
46623
46638
|
selectedProvider,
|
|
46624
|
-
status: "success"
|
|
46639
|
+
status: "success",
|
|
46640
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
46625
46641
|
}, input);
|
|
46626
46642
|
output = await runProvider(provider, model, input);
|
|
46627
46643
|
}
|
|
@@ -46633,6 +46649,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46633
46649
|
if (!selectedProvider || order.length === 0) {
|
|
46634
46650
|
throw new Error("Voice provider router has no available providers.");
|
|
46635
46651
|
}
|
|
46652
|
+
const routingStartedAt = Date.now();
|
|
46636
46653
|
let lastError;
|
|
46637
46654
|
for (const [index, provider] of order.entries()) {
|
|
46638
46655
|
const model = options.providers[provider];
|
|
@@ -46641,7 +46658,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46641
46658
|
}
|
|
46642
46659
|
const startedAt = Date.now();
|
|
46643
46660
|
try {
|
|
46644
|
-
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
|
|
46661
|
+
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt, routingStartedAt);
|
|
46645
46662
|
const providerHealth = recordProviderSuccess(provider);
|
|
46646
46663
|
await emit({
|
|
46647
46664
|
at: Date.now(),
|
|
@@ -46653,14 +46670,15 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46653
46670
|
providerHealth,
|
|
46654
46671
|
recovered: provider !== selectedProvider,
|
|
46655
46672
|
selectedProvider,
|
|
46656
|
-
status: provider === selectedProvider ? "success" : "fallback"
|
|
46673
|
+
status: provider === selectedProvider ? "success" : "fallback",
|
|
46674
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
46657
46675
|
}, input);
|
|
46658
46676
|
return output;
|
|
46659
46677
|
} catch (error) {
|
|
46660
46678
|
lastError = error;
|
|
46661
46679
|
const hasNextProvider = index < order.length - 1;
|
|
46662
46680
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
46663
|
-
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
46681
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
|
|
46664
46682
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
46665
46683
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
46666
46684
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
@@ -46679,7 +46697,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
46679
46697
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
46680
46698
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
46681
46699
|
status: "error",
|
|
46682
|
-
timedOut
|
|
46700
|
+
timedOut,
|
|
46701
|
+
timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
|
|
46702
|
+
timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
|
|
46703
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
46683
46704
|
}, input);
|
|
46684
46705
|
if (!hasNextProvider || !shouldFallback) {
|
|
46685
46706
|
throw error;
|
|
@@ -46882,7 +46903,7 @@ var readServerSentEvents = async (response, onEvent, abortOptions) => {
|
|
|
46882
46903
|
return new Promise((resolve2, reject) => {
|
|
46883
46904
|
inactivityTimer = setTimeout(() => {
|
|
46884
46905
|
reader.cancel().catch(() => {});
|
|
46885
|
-
reject(new
|
|
46906
|
+
reject(new VoiceProviderStreamInactivityError(inactivityMs));
|
|
46886
46907
|
}, inactivityMs);
|
|
46887
46908
|
if (signal) {
|
|
46888
46909
|
onAbort = () => {
|
|
@@ -47071,7 +47092,7 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
47071
47092
|
outputItems
|
|
47072
47093
|
} = await consumeOpenAIResponsesStream(response, onTextDelta, {
|
|
47073
47094
|
signal: ac.signal,
|
|
47074
|
-
inactivityMs: 1e4
|
|
47095
|
+
inactivityMs: options.streamInactivityMs ?? 1e4
|
|
47075
47096
|
}));
|
|
47076
47097
|
stamp("openai.stream-done", {
|
|
47077
47098
|
textChars: assistantText?.length ?? 0,
|
package/dist/testing/index.js
CHANGED
|
@@ -5248,6 +5248,15 @@ class VoiceProviderTimeoutError extends Error {
|
|
|
5248
5248
|
this.timeoutMs = timeoutMs;
|
|
5249
5249
|
}
|
|
5250
5250
|
}
|
|
5251
|
+
|
|
5252
|
+
class VoiceProviderStreamInactivityError extends Error {
|
|
5253
|
+
inactivityMs;
|
|
5254
|
+
constructor(inactivityMs) {
|
|
5255
|
+
super(`SSE read inactivity timeout (${inactivityMs}ms with no chunk)`);
|
|
5256
|
+
this.name = "VoiceProviderStreamInactivityError";
|
|
5257
|
+
this.inactivityMs = inactivityMs;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5251
5260
|
var getMessageToolCalls = (message) => {
|
|
5252
5261
|
const toolCalls = message.metadata?.toolCalls;
|
|
5253
5262
|
return Array.isArray(toolCalls) ? toolCalls.filter((toolCall) => toolCall && typeof toolCall === "object" && typeof toolCall.name === "string") : [];
|
|
@@ -5494,7 +5503,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5494
5503
|
}
|
|
5495
5504
|
}
|
|
5496
5505
|
};
|
|
5497
|
-
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
|
|
5506
|
+
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt, routingStartedAt) => {
|
|
5498
5507
|
let output = await runProvider(provider, model, input);
|
|
5499
5508
|
let emptyAttempt = 0;
|
|
5500
5509
|
while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
|
|
@@ -5506,7 +5515,8 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5506
5515
|
emptyCompletion: true,
|
|
5507
5516
|
provider,
|
|
5508
5517
|
selectedProvider,
|
|
5509
|
-
status: "success"
|
|
5518
|
+
status: "success",
|
|
5519
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
5510
5520
|
}, input);
|
|
5511
5521
|
output = await runProvider(provider, model, input);
|
|
5512
5522
|
}
|
|
@@ -5518,6 +5528,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5518
5528
|
if (!selectedProvider || order.length === 0) {
|
|
5519
5529
|
throw new Error("Voice provider router has no available providers.");
|
|
5520
5530
|
}
|
|
5531
|
+
const routingStartedAt = Date.now();
|
|
5521
5532
|
let lastError;
|
|
5522
5533
|
for (const [index, provider] of order.entries()) {
|
|
5523
5534
|
const model = options.providers[provider];
|
|
@@ -5526,7 +5537,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5526
5537
|
}
|
|
5527
5538
|
const startedAt = Date.now();
|
|
5528
5539
|
try {
|
|
5529
|
-
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
|
|
5540
|
+
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt, routingStartedAt);
|
|
5530
5541
|
const providerHealth = recordProviderSuccess(provider);
|
|
5531
5542
|
await emit({
|
|
5532
5543
|
at: Date.now(),
|
|
@@ -5538,14 +5549,15 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5538
5549
|
providerHealth,
|
|
5539
5550
|
recovered: provider !== selectedProvider,
|
|
5540
5551
|
selectedProvider,
|
|
5541
|
-
status: provider === selectedProvider ? "success" : "fallback"
|
|
5552
|
+
status: provider === selectedProvider ? "success" : "fallback",
|
|
5553
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
5542
5554
|
}, input);
|
|
5543
5555
|
return output;
|
|
5544
5556
|
} catch (error) {
|
|
5545
5557
|
lastError = error;
|
|
5546
5558
|
const hasNextProvider = index < order.length - 1;
|
|
5547
5559
|
const isProviderError = options.isProviderError?.(error, provider) ?? true;
|
|
5548
|
-
const timedOut = options.isTimeoutError?.(error, provider) ?? error instanceof VoiceProviderTimeoutError;
|
|
5560
|
+
const timedOut = options.isTimeoutError?.(error, provider) ?? (error instanceof VoiceProviderTimeoutError || error instanceof VoiceProviderStreamInactivityError);
|
|
5549
5561
|
const rateLimited = options.isRateLimitError?.(error, provider) ?? defaultIsRateLimitError(error);
|
|
5550
5562
|
const shouldFallback = fallbackMode === "provider-error" ? isProviderError : fallbackMode === "rate-limit" ? isProviderError && rateLimited : false;
|
|
5551
5563
|
const providerHealth = recordProviderError(provider, isProviderError, rateLimited);
|
|
@@ -5564,7 +5576,10 @@ var createVoiceProviderRouter = (options) => {
|
|
|
5564
5576
|
suppressionRemainingMs: getSuppressionRemainingMs(provider),
|
|
5565
5577
|
suppressedUntil: providerHealth?.suppressedUntil,
|
|
5566
5578
|
status: "error",
|
|
5567
|
-
timedOut
|
|
5579
|
+
timedOut,
|
|
5580
|
+
timeoutKind: error instanceof VoiceProviderStreamInactivityError ? "stream-inactivity" : error instanceof VoiceProviderTimeoutError ? "latency-budget" : undefined,
|
|
5581
|
+
timeoutMs: error instanceof VoiceProviderStreamInactivityError ? error.inactivityMs : error instanceof VoiceProviderTimeoutError ? error.timeoutMs : undefined,
|
|
5582
|
+
totalElapsedMs: Date.now() - routingStartedAt
|
|
5568
5583
|
}, input);
|
|
5569
5584
|
if (!hasNextProvider || !shouldFallback) {
|
|
5570
5585
|
throw error;
|
|
@@ -5767,7 +5782,7 @@ var readServerSentEvents = async (response, onEvent, abortOptions) => {
|
|
|
5767
5782
|
return new Promise((resolve2, reject) => {
|
|
5768
5783
|
inactivityTimer = setTimeout(() => {
|
|
5769
5784
|
reader.cancel().catch(() => {});
|
|
5770
|
-
reject(new
|
|
5785
|
+
reject(new VoiceProviderStreamInactivityError(inactivityMs));
|
|
5771
5786
|
}, inactivityMs);
|
|
5772
5787
|
if (signal) {
|
|
5773
5788
|
onAbort = () => {
|
|
@@ -5956,7 +5971,7 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
5956
5971
|
outputItems
|
|
5957
5972
|
} = await consumeOpenAIResponsesStream(response, onTextDelta, {
|
|
5958
5973
|
signal: ac.signal,
|
|
5959
|
-
inactivityMs: 1e4
|
|
5974
|
+
inactivityMs: options.streamInactivityMs ?? 1e4
|
|
5960
5975
|
}));
|
|
5961
5976
|
stamp("openai.stream-done", {
|
|
5962
5977
|
textChars: assistantText?.length ?? 0,
|
|
@@ -7415,9 +7430,13 @@ var createVoiceSession = (options) => {
|
|
|
7415
7430
|
if (snapshot.status === "completed" || snapshot.status === "failed" || snapshot.call?.endedAt) {
|
|
7416
7431
|
return;
|
|
7417
7432
|
}
|
|
7433
|
+
const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
|
|
7434
|
+
if (lastTranscriptAt !== undefined && stuckCloseAfterMs !== undefined && Date.now() - lastTranscriptAt < stuckCloseAfterMs) {
|
|
7435
|
+
kickStuckCloseWatchdog();
|
|
7436
|
+
return;
|
|
7437
|
+
}
|
|
7418
7438
|
stuckCloseFired = true;
|
|
7419
7439
|
const reason = `no caller progress for ${stuckCloseAfterMs}ms`;
|
|
7420
|
-
const { lastSpeechAt, lastTranscriptAt } = snapshot.currentTurn;
|
|
7421
7440
|
const sttDeafConfirmed = sttDeafConfirmedAt > 0 && (lastTranscriptAt === undefined || sttDeafConfirmedAt > lastTranscriptAt);
|
|
7422
7441
|
const diagnostic = {
|
|
7423
7442
|
action: "stuck-call-close",
|