@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
- provider?: string;
57
+ byProvider: VoiceCostProviderSlice[];
48
58
  usd: number;
49
59
  };
50
60
  sessionId?: string;
51
61
  stt: {
52
62
  audioMs: number;
53
- provider?: string;
63
+ byProvider: VoiceCostProviderSlice[];
54
64
  usd: number;
55
65
  };
56
66
  telephony: {
57
67
  minutes: number;
58
- provider?: string;
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
- provider?: string;
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 llmByProvider = new Map;
41870
- const ttsByProvider = new Map;
41871
- const sttByProvider = new Map;
41872
- const telephonyByProvider = new Map;
41873
- let lastLlmProvider;
41874
- let lastTtsProvider;
41875
- let lastSttProvider;
41876
- let lastTelephonyProvider;
41877
- const addProvider = (byProvider, provider, usd) => {
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 best ?? fallback;
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
- if (!rates) {
41905
- return;
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
- sttAudioMs += Math.max(0, input.audioMs);
41914
- if (input.provider)
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
- if (!rates) {
41918
- return;
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
- telephonyMinutes += Math.max(0, input.minutes);
41926
- if (input.provider)
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
- if (!rates) {
41930
- return;
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.perMillionCharactersUsd !== undefined && chars > 0) {
41949
- delta = chars * rates.perMillionCharactersUsd / 1e6;
41950
- } else if (rates.perSecondUsd !== undefined && audioMs > 0) {
41951
- delta = audioMs / 1000 * rates.perSecondUsd;
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
- provider: dominant(llmByProvider, lastLlmProvider),
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
- provider: dominant(sttByProvider, lastSttProvider),
41968
- usd: Math.round(sttUsd * 1e6) / 1e6
41969
+ byProvider: finalizeSlices(sttSlices),
41970
+ usd: round6(sttUsd)
41969
41971
  },
41970
41972
  telephony: {
41973
+ byProvider: finalizeSlices(telephonySlices),
41971
41974
  minutes: telephonyMinutes,
41972
- provider: dominant(telephonyByProvider, lastTelephonyProvider),
41973
- usd: Math.round(telephonyUsd * 1e6) / 1e6
41975
+ usd: round6(telephonyUsd)
41974
41976
  },
41975
- totalUsd: Math.round((llmUsd + ttsUsd + sttUsd + telephonyUsd) * 1e6) / 1e6,
41977
+ totalUsd: round6(llmUsd + ttsUsd + sttUsd + telephonyUsd),
41976
41978
  tts: {
41977
41979
  audioMs: ttsAudioMs,
41980
+ byProvider: finalizeSlices(ttsSlices),
41978
41981
  characters: ttsCharacters,
41979
- provider: dominant(ttsByProvider, lastTtsProvider),
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 runProvider(provider, model, input);
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 { 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
+ };
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
- ({ 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, {
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
  }
@@ -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.615",
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"