@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 runProvider(provider, model, input);
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 { assistantText, toolCalls: finalizeToolCalls(calls), usage };
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
- ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, onTextDelta, {
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
  }
@@ -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 runProvider(provider, model, input);
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 { assistantText, toolCalls: finalizeToolCalls(calls), usage };
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
- ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, onTextDelta, {
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.616",
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"