@absolutejs/voice 0.0.22-beta.615 → 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.
|
@@ -39,30 +39,40 @@ export type VoiceCostTelephonyRecord = {
|
|
|
39
39
|
minutes: number;
|
|
40
40
|
provider?: string;
|
|
41
41
|
};
|
|
42
|
+
export type VoiceCostProviderSlice = {
|
|
43
|
+
provider: string;
|
|
44
|
+
usd: number;
|
|
45
|
+
cachedInputTokens?: number;
|
|
46
|
+
inputTokens?: number;
|
|
47
|
+
outputTokens?: number;
|
|
48
|
+
characters?: number;
|
|
49
|
+
audioMs?: number;
|
|
50
|
+
minutes?: number;
|
|
51
|
+
};
|
|
42
52
|
export type VoiceCostBreakdown = {
|
|
43
53
|
llm: {
|
|
44
54
|
cachedInputTokens: number;
|
|
45
55
|
inputTokens: number;
|
|
46
56
|
outputTokens: number;
|
|
47
|
-
|
|
57
|
+
byProvider: VoiceCostProviderSlice[];
|
|
48
58
|
usd: number;
|
|
49
59
|
};
|
|
50
60
|
sessionId?: string;
|
|
51
61
|
stt: {
|
|
52
62
|
audioMs: number;
|
|
53
|
-
|
|
63
|
+
byProvider: VoiceCostProviderSlice[];
|
|
54
64
|
usd: number;
|
|
55
65
|
};
|
|
56
66
|
telephony: {
|
|
57
67
|
minutes: number;
|
|
58
|
-
|
|
68
|
+
byProvider: VoiceCostProviderSlice[];
|
|
59
69
|
usd: number;
|
|
60
70
|
};
|
|
61
71
|
totalUsd: number;
|
|
62
72
|
tts: {
|
|
63
73
|
audioMs: number;
|
|
64
74
|
characters: number;
|
|
65
|
-
|
|
75
|
+
byProvider: VoiceCostProviderSlice[];
|
|
66
76
|
usd: number;
|
|
67
77
|
};
|
|
68
78
|
};
|
|
@@ -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
|
@@ -41866,30 +41866,20 @@ var createVoiceCostAccountant = (options = {}) => {
|
|
|
41866
41866
|
let sttUsd = 0;
|
|
41867
41867
|
let telephonyMinutes = 0;
|
|
41868
41868
|
let telephonyUsd = 0;
|
|
41869
|
-
const
|
|
41870
|
-
const
|
|
41871
|
-
const
|
|
41872
|
-
const
|
|
41873
|
-
|
|
41874
|
-
|
|
41875
|
-
|
|
41876
|
-
|
|
41877
|
-
|
|
41878
|
-
if (!provider)
|
|
41879
|
-
return;
|
|
41880
|
-
byProvider.set(provider, (byProvider.get(provider) ?? 0) + usd);
|
|
41881
|
-
};
|
|
41882
|
-
const dominant = (byProvider, fallback) => {
|
|
41883
|
-
let best;
|
|
41884
|
-
let bestUsd = -1;
|
|
41885
|
-
for (const [provider, usd] of byProvider) {
|
|
41886
|
-
if (usd > bestUsd) {
|
|
41887
|
-
bestUsd = usd;
|
|
41888
|
-
best = provider;
|
|
41889
|
-
}
|
|
41869
|
+
const llmSlices = new Map;
|
|
41870
|
+
const ttsSlices = new Map;
|
|
41871
|
+
const sttSlices = new Map;
|
|
41872
|
+
const telephonySlices = new Map;
|
|
41873
|
+
const sliceFor = (slices, provider) => {
|
|
41874
|
+
let slice = slices.get(provider);
|
|
41875
|
+
if (!slice) {
|
|
41876
|
+
slice = { provider, usd: 0 };
|
|
41877
|
+
slices.set(provider, slice);
|
|
41890
41878
|
}
|
|
41891
|
-
return
|
|
41879
|
+
return slice;
|
|
41892
41880
|
};
|
|
41881
|
+
const round6 = (value) => Math.round(value * 1e6) / 1e6;
|
|
41882
|
+
const finalizeSlices = (slices) => [...slices.values()].map((slice) => ({ ...slice, usd: round6(slice.usd) }));
|
|
41893
41883
|
return {
|
|
41894
41884
|
recordLLM: (usage) => {
|
|
41895
41885
|
const input = usage.inputTokens ?? 0;
|
|
@@ -41898,86 +41888,98 @@ var createVoiceCostAccountant = (options = {}) => {
|
|
|
41898
41888
|
llmInput += input;
|
|
41899
41889
|
llmCachedInput += cached;
|
|
41900
41890
|
llmOutput += output;
|
|
41901
|
-
if (usage.provider)
|
|
41902
|
-
lastLlmProvider = usage.provider;
|
|
41903
41891
|
const rates = lookupRates(priceBook, usage.provider, usage.model)?.llm;
|
|
41904
|
-
|
|
41905
|
-
|
|
41892
|
+
let delta = 0;
|
|
41893
|
+
if (rates) {
|
|
41894
|
+
const cachedRate = rates.cachedInputPerMillionTokensUsd ?? rates.inputPerMillionTokensUsd;
|
|
41895
|
+
delta = Math.max(0, input - cached) * rates.inputPerMillionTokensUsd / 1e6 + cached * cachedRate / 1e6 + output * rates.outputPerMillionTokensUsd / 1e6;
|
|
41896
|
+
llmUsd += delta;
|
|
41897
|
+
}
|
|
41898
|
+
if (usage.provider) {
|
|
41899
|
+
const slice = sliceFor(llmSlices, usage.provider);
|
|
41900
|
+
slice.usd += delta;
|
|
41901
|
+
slice.inputTokens = (slice.inputTokens ?? 0) + input;
|
|
41902
|
+
slice.outputTokens = (slice.outputTokens ?? 0) + output;
|
|
41903
|
+
slice.cachedInputTokens = (slice.cachedInputTokens ?? 0) + cached;
|
|
41906
41904
|
}
|
|
41907
|
-
const cachedRate = rates.cachedInputPerMillionTokensUsd ?? rates.inputPerMillionTokensUsd;
|
|
41908
|
-
const delta = Math.max(0, input - cached) * rates.inputPerMillionTokensUsd / 1e6 + cached * cachedRate / 1e6 + output * rates.outputPerMillionTokensUsd / 1e6;
|
|
41909
|
-
llmUsd += delta;
|
|
41910
|
-
addProvider(llmByProvider, usage.provider, delta);
|
|
41911
41905
|
},
|
|
41912
41906
|
recordSTT: (input) => {
|
|
41913
|
-
|
|
41914
|
-
|
|
41915
|
-
lastSttProvider = input.provider;
|
|
41907
|
+
const audioMs = Math.max(0, input.audioMs);
|
|
41908
|
+
sttAudioMs += audioMs;
|
|
41916
41909
|
const rates = lookupRates(priceBook, input.provider, input.model)?.stt;
|
|
41917
|
-
|
|
41918
|
-
|
|
41910
|
+
let delta = 0;
|
|
41911
|
+
if (rates) {
|
|
41912
|
+
delta = audioMs / 1000 * rates.perSecondUsd;
|
|
41913
|
+
sttUsd += delta;
|
|
41914
|
+
}
|
|
41915
|
+
if (input.provider) {
|
|
41916
|
+
const slice = sliceFor(sttSlices, input.provider);
|
|
41917
|
+
slice.usd += delta;
|
|
41918
|
+
slice.audioMs = (slice.audioMs ?? 0) + audioMs;
|
|
41919
41919
|
}
|
|
41920
|
-
const delta = Math.max(0, input.audioMs) / 1000 * rates.perSecondUsd;
|
|
41921
|
-
sttUsd += delta;
|
|
41922
|
-
addProvider(sttByProvider, input.provider, delta);
|
|
41923
41920
|
},
|
|
41924
41921
|
recordTelephony: (input) => {
|
|
41925
|
-
|
|
41926
|
-
|
|
41927
|
-
lastTelephonyProvider = input.provider;
|
|
41922
|
+
const minutes = Math.max(0, input.minutes);
|
|
41923
|
+
telephonyMinutes += minutes;
|
|
41928
41924
|
const rates = lookupRates(priceBook, input.provider)?.telephony;
|
|
41929
|
-
|
|
41930
|
-
|
|
41925
|
+
let delta = 0;
|
|
41926
|
+
if (rates) {
|
|
41927
|
+
delta = minutes * rates.perMinuteUsd;
|
|
41928
|
+
telephonyUsd += delta;
|
|
41929
|
+
}
|
|
41930
|
+
if (input.provider) {
|
|
41931
|
+
const slice = sliceFor(telephonySlices, input.provider);
|
|
41932
|
+
slice.usd += delta;
|
|
41933
|
+
slice.minutes = (slice.minutes ?? 0) + minutes;
|
|
41931
41934
|
}
|
|
41932
|
-
const delta = Math.max(0, input.minutes) * rates.perMinuteUsd;
|
|
41933
|
-
telephonyUsd += delta;
|
|
41934
|
-
addProvider(telephonyByProvider, input.provider, delta);
|
|
41935
41935
|
},
|
|
41936
41936
|
recordTTS: (input) => {
|
|
41937
41937
|
const chars = input.characters ?? 0;
|
|
41938
41938
|
const audioMs = input.audioMs ?? 0;
|
|
41939
41939
|
ttsCharacters += chars;
|
|
41940
41940
|
ttsAudioMs += audioMs;
|
|
41941
|
-
if (input.provider)
|
|
41942
|
-
lastTtsProvider = input.provider;
|
|
41943
41941
|
const rates = lookupRates(priceBook, input.provider, input.voice)?.tts;
|
|
41944
|
-
if (!rates) {
|
|
41945
|
-
return;
|
|
41946
|
-
}
|
|
41947
41942
|
let delta = 0;
|
|
41948
|
-
if (rates
|
|
41949
|
-
|
|
41950
|
-
|
|
41951
|
-
|
|
41943
|
+
if (rates) {
|
|
41944
|
+
if (rates.perMillionCharactersUsd !== undefined && chars > 0) {
|
|
41945
|
+
delta = chars * rates.perMillionCharactersUsd / 1e6;
|
|
41946
|
+
} else if (rates.perSecondUsd !== undefined && audioMs > 0) {
|
|
41947
|
+
delta = audioMs / 1000 * rates.perSecondUsd;
|
|
41948
|
+
}
|
|
41949
|
+
ttsUsd += delta;
|
|
41950
|
+
}
|
|
41951
|
+
if (input.provider) {
|
|
41952
|
+
const slice = sliceFor(ttsSlices, input.provider);
|
|
41953
|
+
slice.usd += delta;
|
|
41954
|
+
slice.characters = (slice.characters ?? 0) + chars;
|
|
41955
|
+
slice.audioMs = (slice.audioMs ?? 0) + audioMs;
|
|
41952
41956
|
}
|
|
41953
|
-
ttsUsd += delta;
|
|
41954
|
-
addProvider(ttsByProvider, input.provider, delta);
|
|
41955
41957
|
},
|
|
41956
41958
|
snapshot: () => ({
|
|
41957
41959
|
llm: {
|
|
41960
|
+
byProvider: finalizeSlices(llmSlices),
|
|
41958
41961
|
cachedInputTokens: llmCachedInput,
|
|
41959
41962
|
inputTokens: llmInput,
|
|
41960
41963
|
outputTokens: llmOutput,
|
|
41961
|
-
|
|
41962
|
-
usd: Math.round(llmUsd * 1e6) / 1e6
|
|
41964
|
+
usd: round6(llmUsd)
|
|
41963
41965
|
},
|
|
41964
41966
|
sessionId: options.sessionId,
|
|
41965
41967
|
stt: {
|
|
41966
41968
|
audioMs: sttAudioMs,
|
|
41967
|
-
|
|
41968
|
-
usd:
|
|
41969
|
+
byProvider: finalizeSlices(sttSlices),
|
|
41970
|
+
usd: round6(sttUsd)
|
|
41969
41971
|
},
|
|
41970
41972
|
telephony: {
|
|
41973
|
+
byProvider: finalizeSlices(telephonySlices),
|
|
41971
41974
|
minutes: telephonyMinutes,
|
|
41972
|
-
|
|
41973
|
-
usd: Math.round(telephonyUsd * 1e6) / 1e6
|
|
41975
|
+
usd: round6(telephonyUsd)
|
|
41974
41976
|
},
|
|
41975
|
-
totalUsd:
|
|
41977
|
+
totalUsd: round6(llmUsd + ttsUsd + sttUsd + telephonyUsd),
|
|
41976
41978
|
tts: {
|
|
41977
41979
|
audioMs: ttsAudioMs,
|
|
41980
|
+
byProvider: finalizeSlices(ttsSlices),
|
|
41978
41981
|
characters: ttsCharacters,
|
|
41979
|
-
|
|
41980
|
-
usd: Math.round(ttsUsd * 1e6) / 1e6
|
|
41982
|
+
usd: round6(ttsUsd)
|
|
41981
41983
|
}
|
|
41982
41984
|
})
|
|
41983
41985
|
};
|
|
@@ -45452,6 +45454,7 @@ var createJSONVoiceAssistantModel = (options) => ({
|
|
|
45452
45454
|
return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
|
|
45453
45455
|
}
|
|
45454
45456
|
});
|
|
45457
|
+
var isEmptyModelOutput = (output) => !output.assistantText?.trim() && !output.toolCalls?.length;
|
|
45455
45458
|
var createVoiceProviderRouter = (options) => {
|
|
45456
45459
|
const providerIds = Object.keys(options.providers);
|
|
45457
45460
|
const firstProvider = providerIds[0];
|
|
@@ -45459,6 +45462,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45459
45462
|
const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
|
|
45460
45463
|
const strategy = policy?.strategy ?? "prefer-selected";
|
|
45461
45464
|
const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
|
|
45465
|
+
const emptyCompletionRetries = Math.max(0, options.emptyCompletionRetries ?? 1);
|
|
45462
45466
|
const providerProfiles = {
|
|
45463
45467
|
...orchestrationSurface?.providerProfiles ?? {},
|
|
45464
45468
|
...options.providerProfiles ?? {}
|
|
@@ -45633,6 +45637,24 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45633
45637
|
}
|
|
45634
45638
|
}
|
|
45635
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
|
+
};
|
|
45636
45658
|
return {
|
|
45637
45659
|
generate: async (input) => {
|
|
45638
45660
|
const { order, selectedProvider } = await resolveOrder(input);
|
|
@@ -45647,7 +45669,7 @@ var createVoiceProviderRouter = (options) => {
|
|
|
45647
45669
|
}
|
|
45648
45670
|
const startedAt = Date.now();
|
|
45649
45671
|
try {
|
|
45650
|
-
const output = await
|
|
45672
|
+
const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
|
|
45651
45673
|
const providerHealth = recordProviderSuccess(provider);
|
|
45652
45674
|
await emit({
|
|
45653
45675
|
at: Date.now(),
|
|
@@ -45932,10 +45954,21 @@ var finalizeToolCalls = (calls) => [...calls.values()].filter((call) => call.nam
|
|
|
45932
45954
|
var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) => {
|
|
45933
45955
|
let assistantText = "";
|
|
45934
45956
|
let usage;
|
|
45957
|
+
let status;
|
|
45958
|
+
let incompleteReason;
|
|
45959
|
+
let refusal = "";
|
|
45960
|
+
let failureMessage;
|
|
45961
|
+
let outputItems = 0;
|
|
45935
45962
|
const calls = new Map;
|
|
45936
45963
|
await readServerSentEvents(response, (event) => {
|
|
45937
45964
|
const type = typeof event.type === "string" ? event.type : "";
|
|
45938
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
|
+
}
|
|
45939
45972
|
if (type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
45940
45973
|
assistantText += event.delta;
|
|
45941
45974
|
onTextDelta?.(event.delta);
|
|
@@ -45953,14 +45986,34 @@ var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) =
|
|
|
45953
45986
|
const entry = calls.get(String(item.id ?? item.call_id ?? ""));
|
|
45954
45987
|
if (entry)
|
|
45955
45988
|
entry.args = item.arguments;
|
|
45956
|
-
} else if (type === "response.completed") {
|
|
45989
|
+
} else if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
|
|
45957
45990
|
const completed = event.response;
|
|
45958
45991
|
if (completed?.usage && typeof completed.usage === "object") {
|
|
45959
45992
|
usage = completed.usage;
|
|
45960
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
|
+
}
|
|
45961
46005
|
}
|
|
45962
46006
|
}, abortOptions);
|
|
45963
|
-
return {
|
|
46007
|
+
return {
|
|
46008
|
+
assistantText,
|
|
46009
|
+
failureMessage,
|
|
46010
|
+
incompleteReason,
|
|
46011
|
+
outputItems,
|
|
46012
|
+
refusal: refusal || undefined,
|
|
46013
|
+
status,
|
|
46014
|
+
toolCalls: finalizeToolCalls(calls),
|
|
46015
|
+
usage
|
|
46016
|
+
};
|
|
45964
46017
|
};
|
|
45965
46018
|
var createOpenAIVoiceAssistantModel = (options) => {
|
|
45966
46019
|
const fetchImpl = hardenFetch(options.fetch);
|
|
@@ -46021,6 +46074,11 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46021
46074
|
let assistantText;
|
|
46022
46075
|
let toolCalls;
|
|
46023
46076
|
let usage;
|
|
46077
|
+
let status;
|
|
46078
|
+
let incompleteReason;
|
|
46079
|
+
let refusal;
|
|
46080
|
+
let failureMessage;
|
|
46081
|
+
let outputItems = 0;
|
|
46024
46082
|
let firstDeltaSeen = false;
|
|
46025
46083
|
const onTextDelta = input.onTextDelta ? (delta) => {
|
|
46026
46084
|
if (!firstDeltaSeen) {
|
|
@@ -46030,7 +46088,16 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46030
46088
|
input.onTextDelta?.(delta);
|
|
46031
46089
|
} : undefined;
|
|
46032
46090
|
try {
|
|
46033
|
-
({
|
|
46091
|
+
({
|
|
46092
|
+
assistantText,
|
|
46093
|
+
toolCalls,
|
|
46094
|
+
usage,
|
|
46095
|
+
status,
|
|
46096
|
+
incompleteReason,
|
|
46097
|
+
refusal,
|
|
46098
|
+
failureMessage,
|
|
46099
|
+
outputItems
|
|
46100
|
+
} = await consumeOpenAIResponsesStream(response, onTextDelta, {
|
|
46034
46101
|
signal: ac.signal,
|
|
46035
46102
|
inactivityMs: 1e4
|
|
46036
46103
|
}));
|
|
@@ -46038,6 +46105,15 @@ var createOpenAIVoiceAssistantModel = (options) => {
|
|
|
46038
46105
|
textChars: assistantText?.length ?? 0,
|
|
46039
46106
|
toolCalls: toolCalls.length
|
|
46040
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
|
+
}
|
|
46041
46117
|
} finally {
|
|
46042
46118
|
clearTimeout(timer);
|
|
46043
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"
|