@absolutejs/voice 0.0.22-beta.616 → 0.0.22-beta.617
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.
|
@@ -46,6 +46,10 @@ export type VoiceProviderRouterEvent<TProvider extends string = string> = {
|
|
|
46
46
|
at: number;
|
|
47
47
|
attempt: number;
|
|
48
48
|
elapsedMs: number;
|
|
49
|
+
/** Set when this event marks a provider returning an empty completion (no text
|
|
50
|
+
* and no tool call) that triggered a same-provider retry — so consumers can
|
|
51
|
+
* track how often the model glitches empty, and whether the retry recovered. */
|
|
52
|
+
emptyCompletion?: boolean;
|
|
49
53
|
error?: string;
|
|
50
54
|
fallbackProvider?: TProvider;
|
|
51
55
|
latencyBudgetMs?: number;
|
|
@@ -139,6 +143,14 @@ export type VoiceProviderRouterOptions<TContext = unknown, TSession extends Voic
|
|
|
139
143
|
allowProviders?: readonly TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
|
|
140
144
|
fallback?: TProvider[] | ((input: VoiceAgentModelInput<TContext, TSession>) => readonly TProvider[] | Promise<readonly TProvider[]>);
|
|
141
145
|
fallbackMode?: VoiceProviderRouterFallbackMode;
|
|
146
|
+
/** Retry the SAME provider this many times when it returns a non-error but
|
|
147
|
+
* EMPTY completion (no assistant text AND no tool call) before accepting it.
|
|
148
|
+
* An empty completion is a transient model glitch (observed on OpenAI
|
|
149
|
+
* /responses returning a 200 with zero output items mid-conversation), NOT a
|
|
150
|
+
* provider error — so it bypasses fallbackMode and never suppresses provider
|
|
151
|
+
* health. Without this a one-off empty surfaces to the app as a dead turn
|
|
152
|
+
* (the caller hears a re-engage / "go on, I'm listening" loop). Default 1. */
|
|
153
|
+
emptyCompletionRetries?: number;
|
|
142
154
|
isProviderError?: (error: unknown, provider: TProvider) => boolean;
|
|
143
155
|
isRateLimitError?: (error: unknown, provider: TProvider) => boolean;
|
|
144
156
|
isTimeoutError?: (error: unknown, provider: TProvider) => boolean;
|
package/dist/index.js
CHANGED
|
@@ -45454,6 +45454,7 @@ var createJSONVoiceAssistantModel = (options) => ({
|
|
|
45454
45454
|
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
45455
45455
|
}
|
|
45456
45456
|
});
|
|
45457
|
+
var isEmptyModelOutput = (output) => !output.assistantText?.trim() && !output.toolCalls?.length;
|
|
45457
45458
|
var createVoiceProviderRouter = (options) => {
|
|
45458
45459
|
const providerIds = Object.keys(options.providers);
|
|
45459
45460
|
const firstProvider = providerIds[0];
|
|
@@ -45461,6 +45462,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45461
45462
|
const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
|
|
45462
45463
|
const strategy = policy?.strategy ?? "prefer-selected";
|
|
45463
45464
|
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
|
|
45465
|
+
const emptyCompletionRetries = Math.max(0, options.emptyCompletionRetries ?? 1);
|
|
45464
45466
|
const providerProfiles = {
|
|
45465
45467
|
...orchestrationSurface?.providerProfiles ?? {},
|
|
45466
45468
|
...options.providerProfiles ?? {}
|
|
@@ -45635,6 +45637,24 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45635
45637
|
}
|
|
45636
45638
|
}
|
|
45637
45639
|
};
|
|
45640
|
+
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
|
|
45641
|
+
let output = await runProvider(provider, model, input);
|
|
45642
|
+
let emptyAttempt = 0;
|
|
45643
|
+
while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
|
|
45644
|
+
emptyAttempt += 1;
|
|
45645
|
+
await emit({
|
|
45646
|
+
at: Date.now(),
|
|
45647
|
+
attempt: index + 1,
|
|
45648
|
+
elapsedMs: Date.now() - startedAt,
|
|
45649
|
+
emptyCompletion: true,
|
|
45650
|
+
provider,
|
|
45651
|
+
selectedProvider,
|
|
45652
|
+
status: "success"
|
|
45653
|
+
}, input);
|
|
45654
|
+
output = await runProvider(provider, model, input);
|
|
45655
|
+
}
|
|
45656
|
+
return output;
|
|
45657
|
+
};
|
|
45638
45658
|
return {
|
|
45639
45659
|
generate: async (input) => {
|
|
45640
45660
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -45649,7 +45669,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45649
45669
|
}
|
|
45650
45670
|
const startedAt = Date.now();
|
|
45651
45671
|
try {
|
|
45652
|
-
const output = await
|
|
45672
|
+
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
|
|
45653
45673
|
const providerHealth = recordProviderSuccess(provider);
|
|
45654
45674
|
await emit({
|
|
45655
45675
|
at: Date.now(),
|
|
@@ -45934,10 +45954,21 @@ var finalizeToolCalls = (calls) => [...calls.values()].filter((call) => call.nam
|
|
|
45934
45954
|
var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) => {
|
|
45935
45955
|
let assistantText = "";
|
|
45936
45956
|
let usage;
|
|
45957
|
+
let status;
|
|
45958
|
+
let incompleteReason;
|
|
45959
|
+
let refusal = "";
|
|
45960
|
+
let failureMessage;
|
|
45961
|
+
let outputItems = 0;
|
|
45937
45962
|
const calls = new Map;
|
|
45938
45963
|
await readServerSentEvents(response, (event) => {
|
|
45939
45964
|
const type = typeof event.type === "string" ? event.type : "";
|
|
45940
45965
|
const item = event.item;
|
|
45966
|
+
if (type === "response.output_item.added") {
|
|
45967
|
+
outputItems += 1;
|
|
45968
|
+
}
|
|
45969
|
+
if ((type === "response.refusal.delta" || type === "response.refusal.done") && typeof event.delta === "string") {
|
|
45970
|
+
refusal += event.delta;
|
|
45971
|
+
}
|
|
45941
45972
|
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
45942
45973
|
assistantText += event.delta;
|
|
45943
45974
|
onTextDelta?.(event.delta);
|
|
@@ -45955,14 +45986,34 @@ var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) =
|
|
|
45955
45986
|
const entry = calls.get(String(item.id ?? item.call_id ?? ""));
|
|
45956
45987
|
if (entry)
|
|
45957
45988
|
entry.args = item.arguments;
|
|
45958
|
-
} else if (type === "response.completed") {
|
|
45989
|
+
} else if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
45959
45990
|
const completed = event.response;
|
|
45960
45991
|
if (completed?.usage && typeof completed.usage === "object") {
|
|
45961
45992
|
usage = completed.usage;
|
|
45962
45993
|
}
|
|
45994
|
+
if (typeof completed?.status === "string") {
|
|
45995
|
+
status = completed.status;
|
|
45996
|
+
}
|
|
45997
|
+
const incomplete = completed?.incomplete_details;
|
|
45998
|
+
if (typeof incomplete?.reason === "string") {
|
|
45999
|
+
incompleteReason = incomplete.reason;
|
|
46000
|
+
}
|
|
46001
|
+
const failure = completed?.error;
|
|
46002
|
+
if (typeof failure?.message === "string") {
|
|
46003
|
+
failureMessage = failure.message;
|
|
46004
|
+
}
|
|
45963
46005
|
}
|
|
45964
46006
|
}, abortOptions);
|
|
45965
|
-
return {
|
|
46007
|
+
return {
|
|
46008
|
+
assistantText,
|
|
46009
|
+
failureMessage,
|
|
46010
|
+
incompleteReason,
|
|
46011
|
+
outputItems,
|
|
46012
|
+
refusal: refusal || undefined,
|
|
46013
|
+
status,
|
|
46014
|
+
toolCalls: finalizeToolCalls(calls),
|
|
46015
|
+
usage
|
|
46016
|
+
};
|
|
45966
46017
|
};
|
|
45967
46018
|
var createOpenAIVoiceAssistantModel = (options) => {
|
|
45968
46019
|
const fetchImpl = hardenFetch(options.fetch);
|
|
@@ -46023,6 +46074,11 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46023
46074
|
let assistantText;
|
|
46024
46075
|
let toolCalls;
|
|
46025
46076
|
let usage;
|
|
46077
|
+
let status;
|
|
46078
|
+
let incompleteReason;
|
|
46079
|
+
let refusal;
|
|
46080
|
+
let failureMessage;
|
|
46081
|
+
let outputItems = 0;
|
|
46026
46082
|
let firstDeltaSeen = false;
|
|
46027
46083
|
const onTextDelta = input.onTextDelta ? (delta) => {
|
|
46028
46084
|
if (!firstDeltaSeen) {
|
|
@@ -46032,7 +46088,16 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46032
46088
|
input.onTextDelta?.(delta);
|
|
46033
46089
|
} : undefined;
|
|
46034
46090
|
try {
|
|
46035
|
-
({
|
|
46091
|
+
({
|
|
46092
|
+
assistantText,
|
|
46093
|
+
toolCalls,
|
|
46094
|
+
usage,
|
|
46095
|
+
status,
|
|
46096
|
+
incompleteReason,
|
|
46097
|
+
refusal,
|
|
46098
|
+
failureMessage,
|
|
46099
|
+
outputItems
|
|
46100
|
+
} = await consumeOpenAIResponsesStream(response, onTextDelta, {
|
|
46036
46101
|
signal: ac.signal,
|
|
46037
46102
|
inactivityMs: 1e4
|
|
46038
46103
|
}));
|
|
@@ -46040,6 +46105,15 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46040
46105
|
textChars: assistantText?.length ?? 0,
|
|
46041
46106
|
toolCalls: toolCalls.length
|
|
46042
46107
|
});
|
|
46108
|
+
if (!assistantText?.trim() && !toolCalls.length) {
|
|
46109
|
+
stamp("openai.empty-completion", {
|
|
46110
|
+
failureMessage: failureMessage ?? "none",
|
|
46111
|
+
incompleteReason: incompleteReason ?? "none",
|
|
46112
|
+
outputItems,
|
|
46113
|
+
refusal: refusal ? refusal.slice(0, 200) : "none",
|
|
46114
|
+
status: status ?? "unknown"
|
|
46115
|
+
});
|
|
46116
|
+
}
|
|
46043
46117
|
} finally {
|
|
46044
46118
|
clearTimeout(timer);
|
|
46045
46119
|
}
|
package/dist/testing/index.js
CHANGED
|
@@ -4477,6 +4477,7 @@ var createJSONVoiceAssistantModel = (options) => ({
|
|
|
4477
4477
|
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
4478
4478
|
}
|
|
4479
4479
|
});
|
|
4480
|
+
var isEmptyModelOutput = (output) => !output.assistantText?.trim() && !output.toolCalls?.length;
|
|
4480
4481
|
var createVoiceProviderRouter = (options) => {
|
|
4481
4482
|
const providerIds = Object.keys(options.providers);
|
|
4482
4483
|
const firstProvider = providerIds[0];
|
|
@@ -4484,6 +4485,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
4484
4485
|
const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
|
|
4485
4486
|
const strategy = policy?.strategy ?? "prefer-selected";
|
|
4486
4487
|
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
|
|
4488
|
+
const emptyCompletionRetries = Math.max(0, options.emptyCompletionRetries ?? 1);
|
|
4487
4489
|
const providerProfiles = {
|
|
4488
4490
|
...orchestrationSurface?.providerProfiles ?? {},
|
|
4489
4491
|
...options.providerProfiles ?? {}
|
|
@@ -4658,6 +4660,24 @@ var createVoiceProviderRouter = (options) => {
|
|
|
4658
4660
|
}
|
|
4659
4661
|
}
|
|
4660
4662
|
};
|
|
4663
|
+
const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
|
|
4664
|
+
let output = await runProvider(provider, model, input);
|
|
4665
|
+
let emptyAttempt = 0;
|
|
4666
|
+
while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
|
|
4667
|
+
emptyAttempt += 1;
|
|
4668
|
+
await emit({
|
|
4669
|
+
at: Date.now(),
|
|
4670
|
+
attempt: index + 1,
|
|
4671
|
+
elapsedMs: Date.now() - startedAt,
|
|
4672
|
+
emptyCompletion: true,
|
|
4673
|
+
provider,
|
|
4674
|
+
selectedProvider,
|
|
4675
|
+
status: "success"
|
|
4676
|
+
}, input);
|
|
4677
|
+
output = await runProvider(provider, model, input);
|
|
4678
|
+
}
|
|
4679
|
+
return output;
|
|
4680
|
+
};
|
|
4661
4681
|
return {
|
|
4662
4682
|
generate: async (input) => {
|
|
4663
4683
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -4672,7 +4692,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
4672
4692
|
}
|
|
4673
4693
|
const startedAt = Date.now();
|
|
4674
4694
|
try {
|
|
4675
|
-
const output = await
|
|
4695
|
+
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
|
|
4676
4696
|
const providerHealth = recordProviderSuccess(provider);
|
|
4677
4697
|
await emit({
|
|
4678
4698
|
at: Date.now(),
|
|
@@ -4957,10 +4977,21 @@ var finalizeToolCalls = (calls) => [...calls.values()].filter((call) => call.nam
|
|
|
4957
4977
|
var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) => {
|
|
4958
4978
|
let assistantText = "";
|
|
4959
4979
|
let usage;
|
|
4980
|
+
let status;
|
|
4981
|
+
let incompleteReason;
|
|
4982
|
+
let refusal = "";
|
|
4983
|
+
let failureMessage;
|
|
4984
|
+
let outputItems = 0;
|
|
4960
4985
|
const calls = new Map;
|
|
4961
4986
|
await readServerSentEvents(response, (event) => {
|
|
4962
4987
|
const type = typeof event.type === "string" ? event.type : "";
|
|
4963
4988
|
const item = event.item;
|
|
4989
|
+
if (type === "response.output_item.added") {
|
|
4990
|
+
outputItems += 1;
|
|
4991
|
+
}
|
|
4992
|
+
if ((type === "response.refusal.delta" || type === "response.refusal.done") && typeof event.delta === "string") {
|
|
4993
|
+
refusal += event.delta;
|
|
4994
|
+
}
|
|
4964
4995
|
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
4965
4996
|
assistantText += event.delta;
|
|
4966
4997
|
onTextDelta?.(event.delta);
|
|
@@ -4978,14 +5009,34 @@ var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) =
|
|
|
4978
5009
|
const entry = calls.get(String(item.id ?? item.call_id ?? ""));
|
|
4979
5010
|
if (entry)
|
|
4980
5011
|
entry.args = item.arguments;
|
|
4981
|
-
} else if (type === "response.completed") {
|
|
5012
|
+
} else if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
4982
5013
|
const completed = event.response;
|
|
4983
5014
|
if (completed?.usage && typeof completed.usage === "object") {
|
|
4984
5015
|
usage = completed.usage;
|
|
4985
5016
|
}
|
|
5017
|
+
if (typeof completed?.status === "string") {
|
|
5018
|
+
status = completed.status;
|
|
5019
|
+
}
|
|
5020
|
+
const incomplete = completed?.incomplete_details;
|
|
5021
|
+
if (typeof incomplete?.reason === "string") {
|
|
5022
|
+
incompleteReason = incomplete.reason;
|
|
5023
|
+
}
|
|
5024
|
+
const failure = completed?.error;
|
|
5025
|
+
if (typeof failure?.message === "string") {
|
|
5026
|
+
failureMessage = failure.message;
|
|
5027
|
+
}
|
|
4986
5028
|
}
|
|
4987
5029
|
}, abortOptions);
|
|
4988
|
-
return {
|
|
5030
|
+
return {
|
|
5031
|
+
assistantText,
|
|
5032
|
+
failureMessage,
|
|
5033
|
+
incompleteReason,
|
|
5034
|
+
outputItems,
|
|
5035
|
+
refusal: refusal || undefined,
|
|
5036
|
+
status,
|
|
5037
|
+
toolCalls: finalizeToolCalls(calls),
|
|
5038
|
+
usage
|
|
5039
|
+
};
|
|
4989
5040
|
};
|
|
4990
5041
|
var createOpenAIVoiceAssistantModel = (options) => {
|
|
4991
5042
|
const fetchImpl = hardenFetch(options.fetch);
|
|
@@ -5046,6 +5097,11 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
5046
5097
|
let assistantText;
|
|
5047
5098
|
let toolCalls;
|
|
5048
5099
|
let usage;
|
|
5100
|
+
let status;
|
|
5101
|
+
let incompleteReason;
|
|
5102
|
+
let refusal;
|
|
5103
|
+
let failureMessage;
|
|
5104
|
+
let outputItems = 0;
|
|
5049
5105
|
let firstDeltaSeen = false;
|
|
5050
5106
|
const onTextDelta = input.onTextDelta ? (delta) => {
|
|
5051
5107
|
if (!firstDeltaSeen) {
|
|
@@ -5055,7 +5111,16 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
5055
5111
|
input.onTextDelta?.(delta);
|
|
5056
5112
|
} : undefined;
|
|
5057
5113
|
try {
|
|
5058
|
-
({
|
|
5114
|
+
({
|
|
5115
|
+
assistantText,
|
|
5116
|
+
toolCalls,
|
|
5117
|
+
usage,
|
|
5118
|
+
status,
|
|
5119
|
+
incompleteReason,
|
|
5120
|
+
refusal,
|
|
5121
|
+
failureMessage,
|
|
5122
|
+
outputItems
|
|
5123
|
+
} = await consumeOpenAIResponsesStream(response, onTextDelta, {
|
|
5059
5124
|
signal: ac.signal,
|
|
5060
5125
|
inactivityMs: 1e4
|
|
5061
5126
|
}));
|
|
@@ -5063,6 +5128,15 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
5063
5128
|
textChars: assistantText?.length ?? 0,
|
|
5064
5129
|
toolCalls: toolCalls.length
|
|
5065
5130
|
});
|
|
5131
|
+
if (!assistantText?.trim() && !toolCalls.length) {
|
|
5132
|
+
stamp("openai.empty-completion", {
|
|
5133
|
+
failureMessage: failureMessage ?? "none",
|
|
5134
|
+
incompleteReason: incompleteReason ?? "none",
|
|
5135
|
+
outputItems,
|
|
5136
|
+
refusal: refusal ? refusal.slice(0, 200) : "none",
|
|
5137
|
+
status: status ?? "unknown"
|
|
5138
|
+
});
|
|
5139
|
+
}
|
|
5066
5140
|
} finally {
|
|
5067
5141
|
clearTimeout(timer);
|
|
5068
5142
|
}
|
|
@@ -79,10 +79,10 @@ export declare const VoiceProviderSimulationControls: import("vue").DefineCompon
|
|
|
79
79
|
}>> & Readonly<{}>, {
|
|
80
80
|
kind: string;
|
|
81
81
|
title: string;
|
|
82
|
+
failureMessage: string;
|
|
82
83
|
fallbackRequiredProvider: string;
|
|
83
84
|
fallbackRequiredMessage: string;
|
|
84
85
|
failureProviders: readonly string[] | undefined;
|
|
85
|
-
failureMessage: string;
|
|
86
86
|
pathPrefix: string;
|
|
87
87
|
class: string;
|
|
88
88
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/voice",
|
|
3
|
-
"version": "0.0.22-beta.
|
|
3
|
+
"version": "0.0.22-beta.617",
|
|
4
4
|
"description": "Voice primitives and Elysia plugin for AbsoluteJS",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/absolutejs/voice.git"
|
|
8
8
|
},
|
|
9
|
+
"homepage": "https://github.com/absolutejs/voice",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/absolutejs/voice/issues"
|
|
12
|
+
},
|
|
9
13
|
"files": [
|
|
10
14
|
"dist",
|
|
11
15
|
"README.md"
|