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

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
@@ -5303,17 +5303,41 @@ var createVoiceSession = (options) => {
5303
5303
  });
5304
5304
  }, fillerDelayMs);
5305
5305
  }
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
- });
5306
+ let committedOutput;
5307
+ const onTurnStartedAt = Date.now();
5308
+ try {
5309
+ committedOutput = await options.route.onTurn({
5310
+ api,
5311
+ context: options.context,
5312
+ liveOps: liveOpsControl ? {
5313
+ control: liveOpsControl,
5314
+ injectedInstruction
5315
+ } : undefined,
5316
+ onTextDelta: ttsStreamer?.push,
5317
+ session,
5318
+ turn
5319
+ });
5320
+ } catch (error) {
5321
+ const message = toError(error).message;
5322
+ logger.warn("voice route.onTurn failed", {
5323
+ elapsedMs: Date.now() - onTurnStartedAt,
5324
+ error: message,
5325
+ sessionId: options.id,
5326
+ turnId: turn.id
5327
+ });
5328
+ console.error(`[voice] onTurn failed for session ${options.id} turn ${turn.id} after ${Date.now() - onTurnStartedAt}ms:`, message);
5329
+ await appendTrace({
5330
+ payload: {
5331
+ elapsedMs: Date.now() - onTurnStartedAt,
5332
+ error: message,
5333
+ stage: "route.onTurn"
5334
+ },
5335
+ session,
5336
+ turnId: turn.id,
5337
+ type: "session.error"
5338
+ });
5339
+ committedOutput = undefined;
5340
+ }
5317
5341
  const output = {
5318
5342
  assistantText: committedOutput?.assistantText,
5319
5343
  citations: committedOutput?.citations,
@@ -45106,41 +45130,61 @@ var createOpenAIVoiceAssistantModel = (options) => {
45106
45130
  const fetchImpl = options.fetch ?? globalThis.fetch;
45107
45131
  const baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
45108
45132
  const model = options.model ?? "gpt-4.1-mini";
45133
+ const timeoutMs = options.timeoutMs ?? 60000;
45109
45134
  return {
45110
45135
  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(`
45136
+ const ac = new AbortController;
45137
+ const timer = setTimeout(() => {
45138
+ ac.abort(new Error(`OpenAI /responses timed out after ${timeoutMs}ms (no completion event received)`));
45139
+ }, timeoutMs);
45140
+ let response;
45141
+ try {
45142
+ response = await fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, {
45143
+ body: JSON.stringify({
45144
+ input: messagesToOpenAIInput(input.messages),
45145
+ instructions: [input.system, VOICE_SYSTEM_INSTRUCTIONS].filter(Boolean).join(`
45115
45146
 
45116
45147
  `),
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
- });
45148
+ max_output_tokens: options.maxOutputTokens,
45149
+ model,
45150
+ stream: true,
45151
+ temperature: options.temperature,
45152
+ tool_choice: input.tools.length ? "auto" : "none",
45153
+ tools: input.tools.map((tool) => ({
45154
+ description: tool.description,
45155
+ name: tool.name,
45156
+ parameters: tool.parameters ?? {
45157
+ additionalProperties: true,
45158
+ type: "object"
45159
+ },
45160
+ strict: false,
45161
+ type: "function"
45162
+ }))
45163
+ }),
45164
+ headers: {
45165
+ accept: "text/event-stream",
45166
+ authorization: `Bearer ${options.apiKey}`,
45167
+ "content-type": "application/json"
45168
+ },
45169
+ method: "POST",
45170
+ signal: ac.signal
45171
+ });
45172
+ } catch (error) {
45173
+ clearTimeout(timer);
45174
+ throw error;
45175
+ }
45140
45176
  if (!response.ok) {
45177
+ clearTimeout(timer);
45141
45178
  throw createHTTPError("OpenAI", response);
45142
45179
  }
45143
- const { assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta);
45180
+ let assistantText;
45181
+ let toolCalls;
45182
+ let usage;
45183
+ try {
45184
+ ({ assistantText, toolCalls, usage } = await consumeOpenAIResponsesStream(response, input.onTextDelta));
45185
+ } finally {
45186
+ clearTimeout(timer);
45187
+ }
45144
45188
  if (usage) {
45145
45189
  await options.onUsage?.(usage);
45146
45190
  }
@@ -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
  }
@@ -7120,17 +7140,41 @@ var createVoiceSession = (options) => {
7120
7140
  });
7121
7141
  }, fillerDelayMs);
7122
7142
  }
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
- });
7143
+ let committedOutput;
7144
+ const onTurnStartedAt = Date.now();
7145
+ try {
7146
+ committedOutput = await options.route.onTurn({
7147
+ api,
7148
+ context: options.context,
7149
+ liveOps: liveOpsControl ? {
7150
+ control: liveOpsControl,
7151
+ injectedInstruction
7152
+ } : undefined,
7153
+ onTextDelta: ttsStreamer?.push,
7154
+ session,
7155
+ turn
7156
+ });
7157
+ } catch (error) {
7158
+ const message = toError(error).message;
7159
+ logger.warn("voice route.onTurn failed", {
7160
+ elapsedMs: Date.now() - onTurnStartedAt,
7161
+ error: message,
7162
+ sessionId: options.id,
7163
+ turnId: turn.id
7164
+ });
7165
+ console.error(`[voice] onTurn failed for session ${options.id} turn ${turn.id} after ${Date.now() - onTurnStartedAt}ms:`, message);
7166
+ await appendTrace({
7167
+ payload: {
7168
+ elapsedMs: Date.now() - onTurnStartedAt,
7169
+ error: message,
7170
+ stage: "route.onTurn"
7171
+ },
7172
+ session,
7173
+ turnId: turn.id,
7174
+ type: "session.error"
7175
+ });
7176
+ committedOutput = undefined;
7177
+ }
7134
7178
  const output = {
7135
7179
  assistantText: committedOutput?.assistantText,
7136
7180
  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.555",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",