neoagent 3.2.1-beta.1 → 3.2.1-beta.3

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.
Files changed (144) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +511 -89
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +29 -20
  6. package/flutter_app/lib/main_models.dart +3 -0
  7. package/flutter_app/lib/main_operations.dart +334 -321
  8. package/flutter_app/lib/main_settings.dart +4 -3
  9. package/flutter_app/lib/main_shared.dart +14 -11
  10. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  11. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  12. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  13. package/lib/manager.js +106 -89
  14. package/lib/schema_migrations.js +67 -13
  15. package/package.json +20 -13
  16. package/runtime/paths.js +49 -5
  17. package/server/guest_agent.js +52 -30
  18. package/server/http/middleware.js +24 -0
  19. package/server/http/routes.js +4 -6
  20. package/server/public/.last_build_id +1 -1
  21. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  22. package/server/public/flutter_bootstrap.js +1 -1
  23. package/server/public/main.dart.js +30803 -30805
  24. package/server/routes/admin.js +1 -1
  25. package/server/routes/android.js +30 -34
  26. package/server/routes/browser.js +23 -15
  27. package/server/routes/desktop.js +18 -1
  28. package/server/routes/integrations.js +5 -1
  29. package/server/routes/memory.js +1 -0
  30. package/server/routes/settings.js +16 -5
  31. package/server/routes/social_reach.js +12 -3
  32. package/server/routes/social_video.js +4 -0
  33. package/server/services/ai/compaction.js +7 -2
  34. package/server/services/ai/history.js +44 -5
  35. package/server/services/ai/integrated_tools/http_request.js +8 -0
  36. package/server/services/ai/loop/agent_engine_core.js +358 -162
  37. package/server/services/ai/loop/callbacks.js +1 -0
  38. package/server/services/ai/loop/completion_judge.js +12 -0
  39. package/server/services/ai/loop/conversation_loop.js +348 -242
  40. package/server/services/ai/loop/messaging_delivery.js +129 -57
  41. package/server/services/ai/loop/model_call_guard.js +91 -0
  42. package/server/services/ai/loop/model_io.js +20 -45
  43. package/server/services/ai/loop/tool_dispatch.js +19 -8
  44. package/server/services/ai/loopPolicy.js +24 -19
  45. package/server/services/ai/model_discovery.js +227 -0
  46. package/server/services/ai/model_identity.js +71 -0
  47. package/server/services/ai/models.js +67 -162
  48. package/server/services/ai/providerRetry.js +17 -59
  49. package/server/services/ai/provider_selector.js +111 -0
  50. package/server/services/ai/providers/anthropic.js +2 -2
  51. package/server/services/ai/providers/claudeCode.js +21 -33
  52. package/server/services/ai/providers/githubCopilot.js +41 -20
  53. package/server/services/ai/providers/google.js +23 -5
  54. package/server/services/ai/providers/grok.js +4 -3
  55. package/server/services/ai/providers/grokOauth.js +19 -27
  56. package/server/services/ai/providers/nvidia.js +10 -5
  57. package/server/services/ai/providers/ollama.js +111 -84
  58. package/server/services/ai/providers/ollama_stream.js +142 -0
  59. package/server/services/ai/providers/openai.js +39 -5
  60. package/server/services/ai/providers/openaiCodex.js +11 -4
  61. package/server/services/ai/providers/openrouter.js +29 -7
  62. package/server/services/ai/providers/provider_error.js +36 -0
  63. package/server/services/ai/settings.js +26 -2
  64. package/server/services/ai/taskAnalysis.js +5 -1
  65. package/server/services/ai/terminal_reply.js +45 -0
  66. package/server/services/ai/toolEvidence.js +58 -29
  67. package/server/services/ai/tools.js +124 -111
  68. package/server/services/android/controller.js +770 -237
  69. package/server/services/android/process.js +140 -0
  70. package/server/services/android/sdk_download.js +143 -0
  71. package/server/services/android/uia.js +6 -5
  72. package/server/services/artifacts/store.js +24 -0
  73. package/server/services/browser/controller.js +736 -385
  74. package/server/services/browser/extension/gateway.js +40 -16
  75. package/server/services/browser/extension/protocol.js +12 -1
  76. package/server/services/browser/extension/provider.js +59 -47
  77. package/server/services/browser/extension/registry.js +155 -34
  78. package/server/services/cli/executor.js +62 -9
  79. package/server/services/desktop/gateway.js +41 -4
  80. package/server/services/desktop/protocol.js +3 -0
  81. package/server/services/desktop/provider.js +39 -42
  82. package/server/services/desktop/registry.js +137 -52
  83. package/server/services/integrations/figma/provider.js +78 -12
  84. package/server/services/integrations/github/common.js +11 -6
  85. package/server/services/integrations/github/provider.js +52 -53
  86. package/server/services/integrations/google/provider.js +55 -19
  87. package/server/services/integrations/home_assistant/network.js +17 -20
  88. package/server/services/integrations/home_assistant/provider.js +7 -5
  89. package/server/services/integrations/home_assistant/tools.js +17 -5
  90. package/server/services/integrations/http.js +51 -0
  91. package/server/services/integrations/manager.js +158 -53
  92. package/server/services/integrations/microsoft/provider.js +80 -13
  93. package/server/services/integrations/neoarchive/provider.js +55 -29
  94. package/server/services/integrations/neorecall/client.js +17 -10
  95. package/server/services/integrations/neorecall/provider.js +20 -11
  96. package/server/services/integrations/notion/provider.js +16 -13
  97. package/server/services/integrations/oauth_provider.js +115 -51
  98. package/server/services/integrations/slack/provider.js +98 -9
  99. package/server/services/integrations/spotify/provider.js +67 -71
  100. package/server/services/integrations/trello/provider.js +21 -7
  101. package/server/services/integrations/weather/provider.js +18 -12
  102. package/server/services/integrations/whatsapp/provider.js +76 -16
  103. package/server/services/manager.js +87 -1
  104. package/server/services/memory/embedding_index.js +20 -8
  105. package/server/services/memory/embeddings.js +151 -90
  106. package/server/services/memory/ingestion.js +50 -9
  107. package/server/services/memory/ingestion_documents.js +13 -3
  108. package/server/services/memory/manager.js +52 -19
  109. package/server/services/messaging/automation.js +84 -9
  110. package/server/services/messaging/http_platforms.js +33 -13
  111. package/server/services/messaging/inbound_queue.js +78 -24
  112. package/server/services/messaging/inbound_store.js +224 -0
  113. package/server/services/messaging/manager.js +326 -51
  114. package/server/services/messaging/typing_keepalive.js +5 -2
  115. package/server/services/network/http.js +210 -0
  116. package/server/services/network/safe_request.js +307 -0
  117. package/server/services/runtime/backends/local-vm.js +214 -66
  118. package/server/services/runtime/manager.js +17 -12
  119. package/server/services/social_reach/channels/github.js +10 -4
  120. package/server/services/social_reach/channels/reddit.js +4 -4
  121. package/server/services/social_reach/channels/rss.js +2 -2
  122. package/server/services/social_reach/channels/social_video.js +12 -7
  123. package/server/services/social_reach/channels/v2ex.js +21 -8
  124. package/server/services/social_reach/channels/x.js +2 -2
  125. package/server/services/social_reach/channels/xueqiu.js +5 -5
  126. package/server/services/social_reach/service.js +9 -6
  127. package/server/services/social_reach/utils.js +65 -14
  128. package/server/services/social_video/service.js +160 -50
  129. package/server/services/tasks/integration_runtime.js +18 -8
  130. package/server/services/tasks/runtime.js +39 -4
  131. package/server/services/voice/agentBridge.js +17 -4
  132. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  133. package/server/services/voice/liveSession.js +31 -0
  134. package/server/services/voice/openaiSpeech.js +33 -8
  135. package/server/services/voice/providers.js +233 -151
  136. package/server/services/voice/runtimeManager.js +118 -20
  137. package/server/services/voice/turnRunner.js +6 -0
  138. package/server/services/wearable/firmware_manifest.js +51 -13
  139. package/server/services/wearable/service.js +1 -0
  140. package/server/utils/abort.js +96 -0
  141. package/server/utils/cloud-security.js +110 -3
  142. package/server/utils/files.js +31 -0
  143. package/server/utils/image_payload.js +95 -0
  144. package/server/utils/retry.js +107 -0
@@ -15,6 +15,7 @@ const scheduleAdapter = require('./adapters/schedule');
15
15
  const { normalizeJsonObject } = require('./utils');
16
16
  const { normalizeOutgoingMessageForPlatform } = require('../messaging/formatting_guides');
17
17
  const { isTransientError } = require('../ai/providerRetry');
18
+ const { isAbortError, throwIfAborted } = require('../../utils/abort');
18
19
 
19
20
  const MAX_AUTONOMOUS_RETRIES = 1;
20
21
  const MAX_RECURRING_TASK_START_DELAY_MS = 90 * 1000;
@@ -112,6 +113,7 @@ class TaskRuntime {
112
113
  this.runningTaskExecutions = new Set();
113
114
  this.activeExecutionPromises = new Set();
114
115
  this.activePolls = new Map();
116
+ this.abortController = new AbortController();
115
117
  this.integrationEventCleanups = [];
116
118
  this.triggerRegistry = new TriggerRegistry(taskAdapters);
117
119
  this.started = false;
@@ -149,6 +151,9 @@ class TaskRuntime {
149
151
  throw new Error('Task runtime cannot start while shutdown is in progress.');
150
152
  }
151
153
 
154
+ if (this.abortController.signal.aborted) {
155
+ this.abortController = new AbortController();
156
+ }
152
157
  this.started = true;
153
158
  this.stopping = false;
154
159
  this.state = 'starting';
@@ -208,6 +213,7 @@ class TaskRuntime {
208
213
  this.started = false;
209
214
  this.stopping = true;
210
215
  this.state = 'stopping';
216
+ this.abortController.abort('Task runtime is stopping.');
211
217
  this._stopScheduling();
212
218
 
213
219
  this.stopPromise = (async () => {
@@ -301,6 +307,9 @@ class TaskRuntime {
301
307
  runTaskNow(taskId, userId) {
302
308
  const task = this.taskRepository.getTaskById(taskId, userId);
303
309
  if (!task) throw new Error('Task not found');
310
+ if (this.stopping || this.abortController.signal.aborted) {
311
+ return { running: false, skipped: true, reason: 'runtime_stopping' };
312
+ }
304
313
  void this._executeTask(taskId, userId, {
305
314
  scheduledAt: new Date().toISOString(),
306
315
  manual: true,
@@ -349,6 +358,7 @@ class TaskRuntime {
349
358
  const due = this.taskRepository.listDueOneTimeTasks();
350
359
 
351
360
  for (const task of due) {
361
+ if (this.abortController.signal.aborted) break;
352
362
  this.scheduleJobs.delete(task.id);
353
363
  try {
354
364
  const result = await this._executeTask(task.id, task.user_id, {
@@ -373,9 +383,13 @@ class TaskRuntime {
373
383
  return this._runPoll('integration', async () => {
374
384
  const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
375
385
  for (const task of tasks) {
386
+ if (this.abortController.signal.aborted) break;
376
387
  try {
377
- await pollIntegrationTask(this, task);
388
+ await pollIntegrationTask(this, task, {
389
+ signal: this.abortController.signal,
390
+ });
378
391
  } catch (error) {
392
+ if (isAbortError(error, this.abortController.signal) && this.stopping) break;
379
393
  console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
380
394
  }
381
395
  }
@@ -390,13 +404,16 @@ class TaskRuntime {
390
404
  if (active) {
391
405
  return active;
392
406
  }
393
- if (this.stopping) {
407
+ if (this.stopping || this.abortController.signal.aborted) {
394
408
  return Promise.resolve({ skipped: true, reason: 'runtime_stopping' });
395
409
  }
396
410
 
397
411
  const promise = Promise.resolve()
398
- .then(callback)
412
+ .then(() => callback(this.abortController.signal))
399
413
  .catch((error) => {
414
+ if (isAbortError(error, this.abortController.signal) && this.stopping) {
415
+ return { skipped: true, reason: 'runtime_stopping' };
416
+ }
400
417
  this.lastError = error.message;
401
418
  onError(error);
402
419
  return { error: error.message };
@@ -453,7 +470,7 @@ class TaskRuntime {
453
470
  }
454
471
 
455
472
  async _executeTask(taskId, userId, executionMeta = {}) {
456
- if (this.stopping) {
473
+ if (this.stopping || this.abortController.signal.aborted) {
457
474
  return { skipped: true, reason: 'runtime_stopping' };
458
475
  }
459
476
  const executionKey = `${userId}:${taskId}`;
@@ -488,6 +505,8 @@ class TaskRuntime {
488
505
  }
489
506
 
490
507
  async _executeTaskSerial(taskId, userId, executionMeta = {}) {
508
+ const signal = this.abortController.signal;
509
+ if (signal.aborted) return { skipped: true, reason: 'runtime_stopping' };
491
510
  const task = this.taskRepository.getTaskById(taskId, userId);
492
511
  if (!task || !task.enabled) return { skipped: true, reason: 'missing_or_disabled' };
493
512
 
@@ -582,7 +601,11 @@ class TaskRuntime {
582
601
  taskId,
583
602
  manual: executionMeta.manual === true,
584
603
  scheduledAt: executionMeta.scheduledAt || null,
604
+ signal,
585
605
  });
606
+ if (this.stopping || signal.aborted) {
607
+ return { skipped: true, reason: 'runtime_stopping' };
608
+ }
586
609
  this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
587
610
  this._recordTaskLifecycle({
588
611
  userId,
@@ -647,12 +670,16 @@ class TaskRuntime {
647
670
  skipVerifier: false,
648
671
  stream: false,
649
672
  context: executionMeta.triggerPayload || {},
673
+ signal,
650
674
  };
651
675
  try {
652
676
  const result = typeof this.agentEngine.runWithModel === 'function'
653
677
  ? await this.agentEngine.runWithModel(userId, finalPrompt, runOptions, normalizedConfig.model || null)
654
678
  : await this.agentEngine.run(userId, finalPrompt, runOptions);
655
679
  completedRunId = result?.runId || null;
680
+ if (this.stopping || signal.aborted) {
681
+ return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
682
+ }
656
683
  const fallbackDelivery = await this._deliverTaskResultIfNeeded({
657
684
  userId,
658
685
  agentId,
@@ -692,6 +719,9 @@ class TaskRuntime {
692
719
  });
693
720
  return result;
694
721
  } catch (err) {
722
+ if (isAbortError(err, signal) && this.stopping) {
723
+ return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
724
+ }
695
725
  const transientExecutionError = isTransientError(err);
696
726
  if (completedRunId && !transientExecutionError) {
697
727
  this.taskRepository.markAgentRunFailed(completedRunId, userId, err.message);
@@ -733,6 +763,9 @@ class TaskRuntime {
733
763
  }
734
764
  }
735
765
  } catch (err) {
766
+ if (isAbortError(err, signal) && this.stopping) {
767
+ return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
768
+ }
736
769
  console.error(`[Tasks] Task ${taskId} error:`, err.message);
737
770
  if (err?.code !== 'TASK_DELIVERY_FAILED') {
738
771
  const failureMessage = this._buildTaskFailureMessage(taskName, err);
@@ -1045,6 +1078,7 @@ class TaskRuntime {
1045
1078
  deliveryState,
1046
1079
  allowPlainResultFallback = true,
1047
1080
  }) {
1081
+ throwIfAborted(this.abortController.signal, 'Task runtime is stopping.');
1048
1082
  if (deliveryState?.messagingSent || deliveryState?.noResponse || taskConfig.callTo) return null;
1049
1083
  const targets = this._buildNotifyTargets(userId, agentId, taskConfig);
1050
1084
  if (!targets.length) return null;
@@ -1118,6 +1152,7 @@ class TaskRuntime {
1118
1152
  mediaPath: target.mediaPath || null,
1119
1153
  runId: result?.runId || null,
1120
1154
  persistConversation: true,
1155
+ signal: this.abortController.signal,
1121
1156
  });
1122
1157
  deliveryState.messagingSent = true;
1123
1158
  deliveryState.proactiveMessageStaged = false;
@@ -3,6 +3,7 @@
3
3
  const { randomUUID } = require('crypto');
4
4
 
5
5
  const { runVoiceTranscriptTurn } = require('./turnRunner');
6
+ const { createAbortError, throwIfAborted } = require('../../utils/abort');
6
7
 
7
8
  class VoiceAgentBridge {
8
9
  constructor({ agentEngine, memoryManager, runtimeManager }) {
@@ -20,6 +21,8 @@ class VoiceAgentBridge {
20
21
  await session.publishTranscriptFinal(transcriptText);
21
22
  await session.setState('thinking');
22
23
  const runId = randomUUID();
24
+ let ownedRunId = runId;
25
+ const turnSignal = session.signal;
23
26
  session.currentRunId = runId;
24
27
 
25
28
  try {
@@ -40,8 +43,11 @@ class VoiceAgentBridge {
40
43
  allowInterimUpdates: true,
41
44
  voiceSessionId: session.id,
42
45
  runId,
46
+ signal: turnSignal,
43
47
  });
44
- session.currentRunId = result.runId || runId;
48
+ throwIfAborted(turnSignal, 'Voice turn was interrupted.');
49
+ ownedRunId = result.runId || runId;
50
+ session.currentRunId = ownedRunId;
45
51
  const replyText = String(result.replyText || '').trim();
46
52
  if (replyText) {
47
53
  if (deferredFollowUp) {
@@ -50,6 +56,7 @@ class VoiceAgentBridge {
50
56
  deferredFollowUp,
51
57
  replyText,
52
58
  result.runId || runId,
59
+ { signal: turnSignal },
53
60
  );
54
61
  if (!followUp?.sent) {
55
62
  await this.runtimeManager.deliverAssistantMessage(session, replyText, {
@@ -62,12 +69,18 @@ class VoiceAgentBridge {
62
69
  });
63
70
  }
64
71
  }
72
+ throwIfAborted(turnSignal, 'Voice turn was interrupted.');
65
73
  await session.setState('idle');
66
- session.currentRunId = null;
74
+ if (session.currentRunId === ownedRunId) {
75
+ session.currentRunId = null;
76
+ }
67
77
  return result;
68
78
  } catch (error) {
69
- session.currentRunId = null;
70
- await session.setState('idle');
79
+ if (session.currentRunId === ownedRunId) session.currentRunId = null;
80
+ if (session.signal === turnSignal && !session.closed) {
81
+ await session.setState('idle');
82
+ }
83
+ if (turnSignal.aborted) throw createAbortError(turnSignal);
71
84
  throw error;
72
85
  }
73
86
  }
@@ -3,6 +3,7 @@
3
3
  const { getProviderRuntimeConfig } = require('../ai/models');
4
4
  const { resolveSttModel, transcribeVoiceInput } = require('./providers');
5
5
  const { writeTempAudioFile, removeTempFile } = require('./liveAudio');
6
+ const { createAbortError } = require('../../utils/abort');
6
7
 
7
8
  const DEFAULT_PARTIAL_DEBOUNCE_MS = 700;
8
9
  const DEFAULT_MIN_PARTIAL_BYTES = 8000;
@@ -68,6 +69,7 @@ class BufferedLiveRelayAdapter {
68
69
  userId: session.userId,
69
70
  agentId: session.agentId,
70
71
  timeoutMs: 20000,
72
+ signal: session.signal,
71
73
  });
72
74
  } finally {
73
75
  // Release buffered audio immediately after commit so completed turns do
@@ -99,6 +101,7 @@ class BufferedLiveRelayAdapter {
99
101
  userId: session.userId,
100
102
  agentId: session.agentId,
101
103
  timeoutMs: 6000,
104
+ signal: session.signal,
102
105
  });
103
106
  if (transcript) {
104
107
  await session.publishTranscriptPartial(transcript);
@@ -136,9 +139,11 @@ class BufferedLiveRelayAdapter {
136
139
  apiKey: attempt.apiKey,
137
140
  baseUrl: attempt.baseUrl,
138
141
  timeoutMs: options.timeoutMs,
142
+ signal: options.signal,
139
143
  });
140
144
  return String(transcript || '').trim();
141
145
  } catch (error) {
146
+ if (options.signal?.aborted) throw createAbortError(options.signal);
142
147
  lastError = error;
143
148
  }
144
149
  }
@@ -1,5 +1,12 @@
1
1
  'use strict';
2
2
 
3
+ function voiceAbortError(message, code) {
4
+ const error = new Error(message);
5
+ error.name = 'AbortError';
6
+ error.code = code;
7
+ return error;
8
+ }
9
+
3
10
  class VoiceLiveSession {
4
11
  constructor({
5
12
  id,
@@ -32,6 +39,11 @@ class VoiceLiveSession {
32
39
  this.lastAssistantText = '';
33
40
  this.assistantMessageCount = 0;
34
41
  this.closed = false;
42
+ this.turnAbortController = new AbortController();
43
+ }
44
+
45
+ get signal() {
46
+ return this.turnAbortController.signal;
35
47
  }
36
48
 
37
49
  resetInput(mimeType = 'audio/pcm;rate=16000;channels=1') {
@@ -46,6 +58,13 @@ class VoiceLiveSession {
46
58
  }
47
59
 
48
60
  resetTurnState() {
61
+ if (!this.turnAbortController.signal.aborted) {
62
+ this.turnAbortController.abort(voiceAbortError(
63
+ 'Voice turn was reset.',
64
+ 'VOICE_TURN_RESET',
65
+ ));
66
+ }
67
+ this.turnAbortController = new AbortController();
49
68
  this.lastPartialTranscript = '';
50
69
  this.lastFinalTranscript = '';
51
70
  this.lastAssistantText = '';
@@ -204,6 +223,12 @@ class VoiceLiveSession {
204
223
 
205
224
  async interruptOutput() {
206
225
  this.interrupted = true;
226
+ if (!this.turnAbortController.signal.aborted) {
227
+ this.turnAbortController.abort(voiceAbortError(
228
+ 'Voice output was interrupted.',
229
+ 'VOICE_INTERRUPTED',
230
+ ));
231
+ }
207
232
  if (typeof this.sink?.interruptOutput === 'function') {
208
233
  await this.sink.interruptOutput(this);
209
234
  }
@@ -217,6 +242,12 @@ class VoiceLiveSession {
217
242
 
218
243
  async close(reason = 'closed') {
219
244
  this.closed = true;
245
+ if (!this.turnAbortController.signal.aborted) {
246
+ this.turnAbortController.abort(voiceAbortError(
247
+ `Voice session closed: ${reason}.`,
248
+ 'VOICE_SESSION_CLOSED',
249
+ ));
250
+ }
220
251
  if (typeof this.sink?.close === 'function') {
221
252
  await this.sink.close(this, reason);
222
253
  }
@@ -1,9 +1,22 @@
1
1
  'use strict';
2
2
 
3
+ const { readResponseBuffer } = require('../network/http');
4
+ const { runWithAbortTimeout } = require('../../utils/abort');
5
+
6
+ const DEFAULT_SPEECH_TIMEOUT_MS = 30000;
7
+ const DEFAULT_MAX_SPEECH_BYTES = 32 * 1024 * 1024;
8
+
3
9
  async function synthesizeSpeechBuffer(
4
10
  client,
5
11
  text,
6
- { model = 'gpt-4o-mini-tts', voice = 'alloy', responseFormat = 'mp3' } = {},
12
+ {
13
+ model = 'gpt-4o-mini-tts',
14
+ voice = 'alloy',
15
+ responseFormat = 'mp3',
16
+ signal = null,
17
+ timeoutMs = DEFAULT_SPEECH_TIMEOUT_MS,
18
+ maxResponseBytes = DEFAULT_MAX_SPEECH_BYTES,
19
+ } = {},
7
20
  ) {
8
21
  if (!client) {
9
22
  throw new Error('OpenAI client is not configured for speech synthesis.');
@@ -14,14 +27,26 @@ async function synthesizeSpeechBuffer(
14
27
  throw new Error('Speech input is empty; cannot synthesize audio.');
15
28
  }
16
29
 
17
- const response = await client.audio.speech.create({
18
- model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
19
- voice: String(voice || 'alloy').trim() || 'alloy',
20
- input: content,
21
- response_format: String(responseFormat || 'mp3').trim() || 'mp3',
22
- });
30
+ return runWithAbortTimeout(async (operationSignal) => {
31
+ const response = await client.audio.speech.create({
32
+ model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
33
+ voice: String(voice || 'alloy').trim() || 'alloy',
34
+ input: content,
35
+ response_format: String(responseFormat || 'mp3').trim() || 'mp3',
36
+ }, { signal: operationSignal });
23
37
 
24
- return Buffer.from(await response.arrayBuffer());
38
+ return readResponseBuffer(response, {
39
+ maxResponseBytes,
40
+ serviceName: 'OpenAI speech synthesis',
41
+ signal: operationSignal,
42
+ tooLargeCode: 'VOICE_PROVIDER_RESPONSE_TOO_LARGE',
43
+ });
44
+ }, {
45
+ signal,
46
+ timeoutMs,
47
+ timeoutCode: 'VOICE_PROVIDER_TIMEOUT',
48
+ label: 'OpenAI speech synthesis',
49
+ });
25
50
  }
26
51
 
27
52
  module.exports = {