@absolutejs/voice 0.0.22-beta.616 → 0.0.22-beta.618

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
@@ -39579,6 +39579,7 @@ var voice = (config) => {
39579
39579
  const monitorBindings = new Map;
39580
39580
  const runtime = {
39581
39581
  activeSessions: new Map,
39582
+ pendingSessions: new Map,
39582
39583
  logger: resolveLogger(config.logger),
39583
39584
  profileSwitchGuardAutoSwitchCounts: new Map,
39584
39585
  profileSwitchGuardedSessions: new Set,
@@ -39738,6 +39739,31 @@ var voice = (config) => {
39738
39739
  turnDetection: sessionOptions.turnDetection
39739
39740
  });
39740
39741
  };
39742
+ const createAndConnectSession = async (ws, sessionId, scenarioId) => {
39743
+ const session = await createManagedSession(ws, sessionId, scenarioId);
39744
+ const typedSession = session;
39745
+ runtime.activeSessions.set(sessionId, typedSession);
39746
+ registerMonitorSession(sessionId, typedSession);
39747
+ await session.connect(buildSocketAdapter(ws, sessionId));
39748
+ return typedSession;
39749
+ };
39750
+ const ensureManagedSession = async (ws, sessionId, scenarioId) => {
39751
+ const active = runtime.activeSessions.get(sessionId);
39752
+ if (active) {
39753
+ return active;
39754
+ }
39755
+ const inFlight = runtime.pendingSessions.get(sessionId);
39756
+ if (inFlight) {
39757
+ return inFlight;
39758
+ }
39759
+ const creation = createAndConnectSession(ws, sessionId, scenarioId);
39760
+ runtime.pendingSessions.set(sessionId, creation);
39761
+ try {
39762
+ return await creation;
39763
+ } finally {
39764
+ runtime.pendingSessions.delete(sessionId);
39765
+ }
39766
+ };
39741
39767
  const mountSurface = (app, value, factory) => {
39742
39768
  if (value === undefined || value === false) {
39743
39769
  return app;
@@ -39950,13 +39976,7 @@ var voice = (config) => {
39950
39976
  if (!audio) {
39951
39977
  return;
39952
39978
  }
39953
- const session = current ?? await createManagedSession(ws, sessionState.sessionId, sessionState.scenarioId ?? undefined);
39954
- if (!current) {
39955
- const typedSession = session;
39956
- runtime.activeSessions.set(sessionState.sessionId, typedSession);
39957
- registerMonitorSession(sessionState.sessionId, typedSession);
39958
- await session.connect(buildSocketAdapter(ws, sessionState.sessionId));
39959
- }
39979
+ const session = current ?? await ensureManagedSession(ws, sessionState.sessionId, sessionState.scenarioId ?? undefined);
39960
39980
  await session.receiveAudio(audio);
39961
39981
  },
39962
39982
  open: async (ws) => {
@@ -39967,11 +39987,7 @@ var voice = (config) => {
39967
39987
  runtime.activeSessions.delete(sessionState.sessionId);
39968
39988
  deregisterMonitorSession(sessionState.sessionId, "superseded");
39969
39989
  }
39970
- const session = await createManagedSession(ws, sessionState.sessionId, sessionState.scenarioId ?? undefined);
39971
- const typedSession = session;
39972
- runtime.activeSessions.set(sessionState.sessionId, typedSession);
39973
- registerMonitorSession(sessionState.sessionId, typedSession);
39974
- await session.connect(buildSocketAdapter(ws, sessionState.sessionId));
39990
+ await ensureManagedSession(ws, sessionState.sessionId, sessionState.scenarioId ?? undefined);
39975
39991
  }
39976
39992
  }).use(htmxRoutes()).use(surfaceRoutes());
39977
39993
  };
@@ -45454,6 +45470,7 @@ var createJSONVoiceAssistantModel = (options) => ({
45454
45470
  return options.mapOutput?.(output) ?? normalizeRouteOutput(output);
45455
45471
  }
45456
45472
  });
45473
+ var isEmptyModelOutput = (output) => !output.assistantText?.trim() && !output.toolCalls?.length;
45457
45474
  var createVoiceProviderRouter = (options) => {
45458
45475
  const providerIds = Object.keys(options.providers);
45459
45476
  const firstProvider = providerIds[0];
@@ -45461,6 +45478,7 @@ var createVoiceProviderRouter = (options) => {
45461
45478
  const policy = resolveVoiceProviderRoutingPolicy(options.policy) ?? resolveVoiceProviderRoutingPolicy(orchestrationSurface?.policy);
45462
45479
  const strategy = policy?.strategy ?? "prefer-selected";
45463
45480
  const fallbackMode = policy?.fallbackMode ?? options.fallbackMode ?? orchestrationSurface?.fallbackMode ?? "provider-error";
45481
+ const emptyCompletionRetries = Math.max(0, options.emptyCompletionRetries ?? 1);
45464
45482
  const providerProfiles = {
45465
45483
  ...orchestrationSurface?.providerProfiles ?? {},
45466
45484
  ...options.providerProfiles ?? {}
@@ -45635,6 +45653,24 @@ var createVoiceProviderRouter = (options) => {
45635
45653
  }
45636
45654
  }
45637
45655
  };
45656
+ const runProviderWithEmptyRetry = async (provider, model, input, index, selectedProvider, startedAt) => {
45657
+ let output = await runProvider(provider, model, input);
45658
+ let emptyAttempt = 0;
45659
+ while (emptyAttempt < emptyCompletionRetries && isEmptyModelOutput(output)) {
45660
+ emptyAttempt += 1;
45661
+ await emit({
45662
+ at: Date.now(),
45663
+ attempt: index + 1,
45664
+ elapsedMs: Date.now() - startedAt,
45665
+ emptyCompletion: true,
45666
+ provider,
45667
+ selectedProvider,
45668
+ status: "success"
45669
+ }, input);
45670
+ output = await runProvider(provider, model, input);
45671
+ }
45672
+ return output;
45673
+ };
45638
45674
  return {
45639
45675
  generate: async (input) => {
45640
45676
  const { order, selectedProvider } = await resolveOrder(input);
@@ -45649,7 +45685,7 @@ var createVoiceProviderRouter = (options) => {
45649
45685
  }
45650
45686
  const startedAt = Date.now();
45651
45687
  try {
45652
- const output = await runProvider(provider, model, input);
45688
+ const output = await runProviderWithEmptyRetry(provider, model, input, index, selectedProvider, startedAt);
45653
45689
  const providerHealth = recordProviderSuccess(provider);
45654
45690
  await emit({
45655
45691
  at: Date.now(),
@@ -45934,10 +45970,21 @@ var finalizeToolCalls = (calls) => [...calls.values()].filter((call) => call.nam
45934
45970
  var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) => {
45935
45971
  let assistantText = "";
45936
45972
  let usage;
45973
+ let status;
45974
+ let incompleteReason;
45975
+ let refusal = "";
45976
+ let failureMessage;
45977
+ let outputItems = 0;
45937
45978
  const calls = new Map;
45938
45979
  await readServerSentEvents(response, (event) => {
45939
45980
  const type = typeof event.type === "string" ? event.type : "";
45940
45981
  const item = event.item;
45982
+ if (type === "response.output_item.added") {
45983
+ outputItems += 1;
45984
+ }
45985
+ if ((type === "response.refusal.delta" || type === "response.refusal.done") && typeof event.delta === "string") {
45986
+ refusal += event.delta;
45987
+ }
45941
45988
  if (type === "response.output_text.delta" && typeof event.delta === "string") {
45942
45989
  assistantText += event.delta;
45943
45990
  onTextDelta?.(event.delta);
@@ -45955,14 +46002,34 @@ var consumeOpenAIResponsesStream = async (response, onTextDelta, abortOptions) =
45955
46002
  const entry = calls.get(String(item.id ?? item.call_id ?? ""));
45956
46003
  if (entry)
45957
46004
  entry.args = item.arguments;
45958
- } else if (type === "response.completed") {
46005
+ } else if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
45959
46006
  const completed = event.response;
45960
46007
  if (completed?.usage && typeof completed.usage === "object") {
45961
46008
  usage = completed.usage;
45962
46009
  }
46010
+ if (typeof completed?.status === "string") {
46011
+ status = completed.status;
46012
+ }
46013
+ const incomplete = completed?.incomplete_details;
46014
+ if (typeof incomplete?.reason === "string") {
46015
+ incompleteReason = incomplete.reason;
46016
+ }
46017
+ const failure = completed?.error;
46018
+ if (typeof failure?.message === "string") {
46019
+ failureMessage = failure.message;
46020
+ }
45963
46021
  }
45964
46022
  }, abortOptions);
45965
- return { assistantText, toolCalls: finalizeToolCalls(calls), usage };
46023
+ return {
46024
+ assistantText,
46025
+ failureMessage,
46026
+ incompleteReason,
46027
+ outputItems,
46028
+ refusal: refusal || undefined,
46029
+ status,
46030
+ toolCalls: finalizeToolCalls(calls),
46031
+ usage
46032
+ };
45966
46033
  };
45967
46034
  var createOpenAIVoiceAssistantModel = (options) => {
45968
46035
  const fetchImpl = hardenFetch(options.fetch);
@@ -46023,6 +46090,11 @@ var createOpenAIVoiceAssistantModel = (options) => {
46023
46090
  let assistantText;
46024
46091
  let toolCalls;
46025
46092
  let usage;
46093
+ let status;
46094
+ let incompleteReason;
46095
+ let refusal;
46096
+ let failureMessage;
46097
+ let outputItems = 0;
46026
46098
  let firstDeltaSeen = false;
46027
46099
  const onTextDelta = input.onTextDelta ? (delta) => {
46028
46100
  if (!firstDeltaSeen) {
@@ -46032,7 +46104,16 @@ var createOpenAIVoiceAssistantModel = (options) => {
46032
46104
  input.onTextDelta?.(delta);
46033
46105
  } : undefined;
46034
46106
  try {
46035
- ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, onTextDelta, {
46107
+ ({
46108
+ assistantText,
46109
+ toolCalls,
46110
+ usage,
46111
+ status,
46112
+ incompleteReason,
46113
+ refusal,
46114
+ failureMessage,
46115
+ outputItems
46116
+ } = await consumeOpenAIResponsesStream(response, onTextDelta, {
46036
46117
  signal: ac.signal,
46037
46118
  inactivityMs: 1e4
46038
46119
  }));
@@ -46040,6 +46121,15 @@ var createOpenAIVoiceAssistantModel = (options) => {
46040
46121
  textChars: assistantText?.length ?? 0,
46041
46122
  toolCalls: toolCalls.length
46042
46123
  });
46124
+ if (!assistantText?.trim() && !toolCalls.length) {
46125
+ stamp("openai.empty-completion", {
46126
+ failureMessage: failureMessage ?? "none",
46127
+ incompleteReason: incompleteReason ?? "none",
46128
+ outputItems,
46129
+ refusal: refusal ? refusal.slice(0, 200) : "none",
46130
+ status: status ?? "unknown"
46131
+ });
46132
+ }
46043
46133
  } finally {
46044
46134
  clearTimeout(timer);
46045
46135
  }
@@ -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.618",
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"