@absolutejs/voice 0.0.22-beta.554 → 0.0.22-beta.556

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.
@@ -13,6 +13,14 @@ export type OpenAIVoiceAssistantModelOptions = {
13
13
  model?: string;
14
14
  onUsage?: (usage: Record<string, unknown>) => Promise<void> | void;
15
15
  temperature?: number;
16
+ /**
17
+ * Hard cap on a single /responses stream. Aborts via AbortController if
18
+ * the stream doesn't complete in time. Default 60s — generous for typical
19
+ * gpt-4.1-mini conversational turns (1-3s) but catches infinite hangs
20
+ * (server-side stream stalls observed on rare complex inputs). On timeout
21
+ * the agent loop falls through to default-silent-turn-ack.
22
+ */
23
+ timeoutMs?: number;
16
24
  };
17
25
  export type AnthropicVoiceAssistantModelOptions = {
18
26
  apiKey: string;
package/dist/index.js CHANGED
@@ -5253,6 +5253,7 @@ var createVoiceSession = (options) => {
5253
5253
  };
5254
5254
  };
5255
5255
  const completeTurn = async (session, turn) => {
5256
+ console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
5256
5257
  const liveOpsControl = await options.liveOps?.getControl(options.id);
5257
5258
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
5258
5259
  await appendTrace({
@@ -5303,17 +5304,41 @@ var createVoiceSession = (options) => {
5303
5304
  });
5304
5305
  }, fillerDelayMs);
5305
5306
  }
5306
- const committedOutput = await options.route.onTurn({
5307
- api,
5308
- context: options.context,
5309
- liveOps: liveOpsControl ? {
5310
- control: liveOpsControl,
5311
- injectedInstruction
5312
- } : undefined,
5313
- onTextDelta: ttsStreamer?.push,
5314
- session,
5315
- turn
5316
- });
5307
+ let committedOutput;
5308
+ const onTurnStartedAt = Date.now();
5309
+ try {
5310
+ committedOutput = await options.route.onTurn({
5311
+ api,
5312
+ context: options.context,
5313
+ liveOps: liveOpsControl ? {
5314
+ control: liveOpsControl,
5315
+ injectedInstruction
5316
+ } : undefined,
5317
+ onTextDelta: ttsStreamer?.push,
5318
+ session,
5319
+ turn
5320
+ });
5321
+ } catch (error) {
5322
+ const message = toError(error).message;
5323
+ logger.warn("voice route.onTurn failed", {
5324
+ elapsedMs: Date.now() - onTurnStartedAt,
5325
+ error: message,
5326
+ sessionId: options.id,
5327
+ turnId: turn.id
5328
+ });
5329
+ console.error(`[voice] onTurn failed for session ${options.id} turn ${turn.id} after ${Date.now() - onTurnStartedAt}ms:`, message);
5330
+ await appendTrace({
5331
+ payload: {
5332
+ elapsedMs: Date.now() - onTurnStartedAt,
5333
+ error: message,
5334
+ stage: "route.onTurn"
5335
+ },
5336
+ session,
5337
+ turnId: turn.id,
5338
+ type: "session.error"
5339
+ });
5340
+ committedOutput = undefined;
5341
+ }
5317
5342
  const output = {
5318
5343
  assistantText: committedOutput?.assistantText,
5319
5344
  citations: committedOutput?.citations,
@@ -45106,41 +45131,61 @@ var createOpenAIVoiceAssistantModel = (options) => {
45106
45131
  const fetchImpl = options.fetch ?? globalThis.fetch;
45107
45132
  const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
45108
45133
  const model = options.model ?? "gpt-4.1-mini";
45134
+ const timeoutMs = options.timeoutMs ?? 60000;
45109
45135
  return {
45110
45136
  generate: async (input) => {
45111
- const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
45112
- body: JSON.stringify({
45113
- input: messagesToOpenAIInput(input.messages),
45114
- instructions: [input.system, VOICE_SYSTEM_INSTRUCTIONS].filter(Boolean).join(`
45137
+ const ac = new AbortController;
45138
+ const timer = setTimeout(() => {
45139
+ ac.abort(new Error(`OpenAI /responses timed out after ${timeoutMs}ms (no completion event received)`));
45140
+ }, timeoutMs);
45141
+ let response;
45142
+ try {
45143
+ response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
45144
+ body: JSON.stringify({
45145
+ input: messagesToOpenAIInput(input.messages),
45146
+ instructions: [input.system, VOICE_SYSTEM_INSTRUCTIONS].filter(Boolean).join(`
45115
45147
 
45116
45148
  `),
45117
- max_output_tokens: options.maxOutputTokens,
45118
- model,
45119
- stream: true,
45120
- temperature: options.temperature,
45121
- tool_choice: input.tools.length ? "auto" : "none",
45122
- tools: input.tools.map((tool) => ({
45123
- description: tool.description,
45124
- name: tool.name,
45125
- parameters: tool.parameters ?? {
45126
- additionalProperties: true,
45127
- type: "object"
45128
- },
45129
- strict: false,
45130
- type: "function"
45131
- }))
45132
- }),
45133
- headers: {
45134
- accept: "text/event-stream",
45135
- authorization: `Bearer ${options.apiKey}`,
45136
- "content-type": "application/json"
45137
- },
45138
- method: "POST"
45139
- });
45149
+ max_output_tokens: options.maxOutputTokens,
45150
+ model,
45151
+ stream: true,
45152
+ temperature: options.temperature,
45153
+ tool_choice: input.tools.length ? "auto" : "none",
45154
+ tools: input.tools.map((tool) => ({
45155
+ description: tool.description,
45156
+ name: tool.name,
45157
+ parameters: tool.parameters ?? {
45158
+ additionalProperties: true,
45159
+ type: "object"
45160
+ },
45161
+ strict: false,
45162
+ type: "function"
45163
+ }))
45164
+ }),
45165
+ headers: {
45166
+ accept: "text/event-stream",
45167
+ authorization: `Bearer ${options.apiKey}`,
45168
+ "content-type": "application/json"
45169
+ },
45170
+ method: "POST",
45171
+ signal: ac.signal
45172
+ });
45173
+ } catch (error) {
45174
+ clearTimeout(timer);
45175
+ throw error;
45176
+ }
45140
45177
  if (!response.ok) {
45178
+ clearTimeout(timer);
45141
45179
  throw createHTTPError("OpenAI", response);
45142
45180
  }
45143
- const { assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta);
45181
+ let assistantText;
45182
+ let toolCalls;
45183
+ let usage;
45184
+ try {
45185
+ ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta));
45186
+ } finally {
45187
+ clearTimeout(timer);
45188
+ }
45144
45189
  if (usage) {
45145
45190
  await options.onUsage?.(usage);
45146
45191
  }
@@ -4600,41 +4600,61 @@ var createOpenAIVoiceAssistantModel = (options) => {
4600
4600
  const fetchImpl = options.fetch ?? globalThis.fetch;
4601
4601
  const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
4602
4602
  const model = options.model ?? "gpt-4.1-mini";
4603
+ const timeoutMs = options.timeoutMs ?? 60000;
4603
4604
  return {
4604
4605
  generate: async (input) => {
4605
- const response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
4606
- body: JSON.stringify({
4607
- input: messagesToOpenAIInput(input.messages),
4608
- instructions: [input.system, VOICE_SYSTEM_INSTRUCTIONS].filter(Boolean).join(`
4606
+ const ac = new AbortController;
4607
+ const timer = setTimeout(() => {
4608
+ ac.abort(new Error(`OpenAI /responses timed out after ${timeoutMs}ms (no completion event received)`));
4609
+ }, timeoutMs);
4610
+ let response;
4611
+ try {
4612
+ response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
4613
+ body: JSON.stringify({
4614
+ input: messagesToOpenAIInput(input.messages),
4615
+ instructions: [input.system, VOICE_SYSTEM_INSTRUCTIONS].filter(Boolean).join(`
4609
4616
 
4610
4617
  `),
4611
- max_output_tokens: options.maxOutputTokens,
4612
- model,
4613
- stream: true,
4614
- temperature: options.temperature,
4615
- tool_choice: input.tools.length ? "auto" : "none",
4616
- tools: input.tools.map((tool) => ({
4617
- description: tool.description,
4618
- name: tool.name,
4619
- parameters: tool.parameters ?? {
4620
- additionalProperties: true,
4621
- type: "object"
4622
- },
4623
- strict: false,
4624
- type: "function"
4625
- }))
4626
- }),
4627
- headers: {
4628
- accept: "text/event-stream",
4629
- authorization: `Bearer ${options.apiKey}`,
4630
- "content-type": "application/json"
4631
- },
4632
- method: "POST"
4633
- });
4618
+ max_output_tokens: options.maxOutputTokens,
4619
+ model,
4620
+ stream: true,
4621
+ temperature: options.temperature,
4622
+ tool_choice: input.tools.length ? "auto" : "none",
4623
+ tools: input.tools.map((tool) => ({
4624
+ description: tool.description,
4625
+ name: tool.name,
4626
+ parameters: tool.parameters ?? {
4627
+ additionalProperties: true,
4628
+ type: "object"
4629
+ },
4630
+ strict: false,
4631
+ type: "function"
4632
+ }))
4633
+ }),
4634
+ headers: {
4635
+ accept: "text/event-stream",
4636
+ authorization: `Bearer ${options.apiKey}`,
4637
+ "content-type": "application/json"
4638
+ },
4639
+ method: "POST",
4640
+ signal: ac.signal
4641
+ });
4642
+ } catch (error) {
4643
+ clearTimeout(timer);
4644
+ throw error;
4645
+ }
4634
4646
  if (!response.ok) {
4647
+ clearTimeout(timer);
4635
4648
  throw createHTTPError("OpenAI", response);
4636
4649
  }
4637
- const { assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta);
4650
+ let assistantText;
4651
+ let toolCalls;
4652
+ let usage;
4653
+ try {
4654
+ ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta));
4655
+ } finally {
4656
+ clearTimeout(timer);
4657
+ }
4638
4658
  if (usage) {
4639
4659
  await options.onUsage?.(usage);
4640
4660
  }
@@ -7070,6 +7090,7 @@ var createVoiceSession = (options) => {
7070
7090
  };
7071
7091
  };
7072
7092
  const completeTurn = async (session, turn) => {
7093
+ console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
7073
7094
  const liveOpsControl = await options.liveOps?.getControl(options.id);
7074
7095
  if (liveOpsControl?.assistantPaused || liveOpsControl?.operatorTakeover) {
7075
7096
  await appendTrace({
@@ -7120,17 +7141,41 @@ var createVoiceSession = (options) => {
7120
7141
  });
7121
7142
  }, fillerDelayMs);
7122
7143
  }
7123
- const committedOutput = await options.route.onTurn({
7124
- api,
7125
- context: options.context,
7126
- liveOps: liveOpsControl ? {
7127
- control: liveOpsControl,
7128
- injectedInstruction
7129
- } : undefined,
7130
- onTextDelta: ttsStreamer?.push,
7131
- session,
7132
- turn
7133
- });
7144
+ let committedOutput;
7145
+ const onTurnStartedAt = Date.now();
7146
+ try {
7147
+ committedOutput = await options.route.onTurn({
7148
+ api,
7149
+ context: options.context,
7150
+ liveOps: liveOpsControl ? {
7151
+ control: liveOpsControl,
7152
+ injectedInstruction
7153
+ } : undefined,
7154
+ onTextDelta: ttsStreamer?.push,
7155
+ session,
7156
+ turn
7157
+ });
7158
+ } catch (error) {
7159
+ const message = toError(error).message;
7160
+ logger.warn("voice route.onTurn failed", {
7161
+ elapsedMs: Date.now() - onTurnStartedAt,
7162
+ error: message,
7163
+ sessionId: options.id,
7164
+ turnId: turn.id
7165
+ });
7166
+ console.error(`[voice] onTurn failed for session ${options.id} turn ${turn.id} after ${Date.now() - onTurnStartedAt}ms:`, message);
7167
+ await appendTrace({
7168
+ payload: {
7169
+ elapsedMs: Date.now() - onTurnStartedAt,
7170
+ error: message,
7171
+ stage: "route.onTurn"
7172
+ },
7173
+ session,
7174
+ turnId: turn.id,
7175
+ type: "session.error"
7176
+ });
7177
+ committedOutput = undefined;
7178
+ }
7134
7179
  const output = {
7135
7180
  assistantText: committedOutput?.assistantText,
7136
7181
  citations: committedOutput?.citations,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.554",
3
+ "version": "0.0.22-beta.556",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",