neoagent 3.2.1-beta.0 → 3.2.1-beta.10

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 (175) hide show
  1. package/docs/agent-run-lifecycle.md +10 -0
  2. package/extensions/chrome-browser/background.mjs +318 -88
  3. package/extensions/chrome-browser/http.mjs +136 -0
  4. package/extensions/chrome-browser/protocol.mjs +654 -90
  5. package/flutter_app/lib/main_chat.dart +118 -739
  6. package/flutter_app/lib/main_controller.dart +111 -20
  7. package/flutter_app/lib/main_integrations.dart +607 -8
  8. package/flutter_app/lib/main_models.dart +3 -0
  9. package/flutter_app/lib/main_operations.dart +334 -321
  10. package/flutter_app/lib/main_security.dart +266 -112
  11. package/flutter_app/lib/main_settings.dart +4 -3
  12. package/flutter_app/lib/main_shared.dart +14 -11
  13. package/flutter_app/lib/src/backend_client.dart +78 -0
  14. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  15. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  16. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  17. package/landing/index.html +3 -1
  18. package/lib/manager.js +106 -89
  19. package/lib/schema_migrations.js +145 -13
  20. package/package.json +30 -15
  21. package/runtime/paths.js +49 -5
  22. package/server/db/database.js +4 -4
  23. package/server/guest-agent.cli.package.json +13 -0
  24. package/server/guest_agent.js +85 -40
  25. package/server/http/middleware.js +24 -0
  26. package/server/http/routes.js +11 -6
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  29. package/server/public/flutter_bootstrap.js +2 -2
  30. package/server/public/main.dart.js +73083 -72209
  31. package/server/routes/admin.js +1 -1
  32. package/server/routes/agents.js +35 -2
  33. package/server/routes/android.js +30 -34
  34. package/server/routes/browser.js +23 -15
  35. package/server/routes/desktop.js +18 -1
  36. package/server/routes/integrations.js +107 -1
  37. package/server/routes/memory.js +1 -0
  38. package/server/routes/settings.js +16 -5
  39. package/server/routes/social_reach.js +12 -3
  40. package/server/routes/social_video.js +4 -0
  41. package/server/services/agents/manager.js +1 -1
  42. package/server/services/ai/capabilityHealth.js +62 -96
  43. package/server/services/ai/compaction.js +7 -2
  44. package/server/services/ai/history.js +45 -6
  45. package/server/services/ai/integrated_tools/http_request.js +8 -0
  46. package/server/services/ai/loop/agent_engine_core.js +496 -166
  47. package/server/services/ai/loop/blank_recovery.js +5 -4
  48. package/server/services/ai/loop/callbacks.js +1 -0
  49. package/server/services/ai/loop/completion_judge.js +121 -5
  50. package/server/services/ai/loop/conversation_loop.js +620 -340
  51. package/server/services/ai/loop/lifecycle.js +108 -0
  52. package/server/services/ai/loop/messaging_delivery.js +129 -57
  53. package/server/services/ai/loop/model_call_guard.js +91 -0
  54. package/server/services/ai/loop/model_io.js +48 -56
  55. package/server/services/ai/loop/progress_classification.js +2 -0
  56. package/server/services/ai/loop/tool_dispatch.js +28 -8
  57. package/server/services/ai/loopPolicy.js +48 -21
  58. package/server/services/ai/messagingFallback.js +17 -17
  59. package/server/services/ai/model_discovery.js +227 -0
  60. package/server/services/ai/model_failure_cache.js +108 -0
  61. package/server/services/ai/model_identity.js +71 -0
  62. package/server/services/ai/models.js +68 -163
  63. package/server/services/ai/providerRetry.js +17 -59
  64. package/server/services/ai/provider_selector.js +166 -0
  65. package/server/services/ai/providers/anthropic.js +4 -4
  66. package/server/services/ai/providers/claudeCode.js +21 -33
  67. package/server/services/ai/providers/githubCopilot.js +41 -20
  68. package/server/services/ai/providers/google.js +135 -97
  69. package/server/services/ai/providers/grok.js +6 -5
  70. package/server/services/ai/providers/grokOauth.js +19 -27
  71. package/server/services/ai/providers/nvidia.js +12 -7
  72. package/server/services/ai/providers/ollama.js +114 -86
  73. package/server/services/ai/providers/ollama_stream.js +142 -0
  74. package/server/services/ai/providers/openai.js +41 -7
  75. package/server/services/ai/providers/openaiCodex.js +13 -4
  76. package/server/services/ai/providers/openrouter.js +31 -9
  77. package/server/services/ai/providers/provider_error.js +36 -0
  78. package/server/services/ai/settings.js +26 -2
  79. package/server/services/ai/systemPrompt.js +19 -12
  80. package/server/services/ai/taskAnalysis.js +58 -10
  81. package/server/services/ai/terminal_reply.js +18 -0
  82. package/server/services/ai/toolEvidence.js +350 -29
  83. package/server/services/ai/tools.js +190 -111
  84. package/server/services/android/controller.js +770 -237
  85. package/server/services/android/process.js +140 -0
  86. package/server/services/android/sdk_download.js +143 -0
  87. package/server/services/android/uia.js +6 -5
  88. package/server/services/artifacts/store.js +24 -0
  89. package/server/services/browser/controller.js +843 -385
  90. package/server/services/browser/extension/gateway.js +40 -16
  91. package/server/services/browser/extension/protocol.js +15 -1
  92. package/server/services/browser/extension/provider.js +71 -47
  93. package/server/services/browser/extension/registry.js +155 -34
  94. package/server/services/cli/executor.js +62 -9
  95. package/server/services/credentials/bitwarden_cli.js +322 -0
  96. package/server/services/credentials/broker.js +594 -0
  97. package/server/services/desktop/gateway.js +41 -4
  98. package/server/services/desktop/protocol.js +3 -0
  99. package/server/services/desktop/provider.js +39 -42
  100. package/server/services/desktop/registry.js +137 -52
  101. package/server/services/integrations/bitwarden/constants.js +14 -0
  102. package/server/services/integrations/bitwarden/provider.js +197 -0
  103. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  104. package/server/services/integrations/figma/provider.js +78 -12
  105. package/server/services/integrations/github/common.js +11 -6
  106. package/server/services/integrations/github/provider.js +52 -53
  107. package/server/services/integrations/google/provider.js +55 -19
  108. package/server/services/integrations/home_assistant/network.js +17 -20
  109. package/server/services/integrations/home_assistant/provider.js +7 -5
  110. package/server/services/integrations/home_assistant/tools.js +17 -5
  111. package/server/services/integrations/http.js +51 -0
  112. package/server/services/integrations/manager.js +159 -53
  113. package/server/services/integrations/microsoft/provider.js +80 -13
  114. package/server/services/integrations/neoarchive/provider.js +55 -29
  115. package/server/services/integrations/neorecall/client.js +17 -10
  116. package/server/services/integrations/neorecall/provider.js +20 -11
  117. package/server/services/integrations/notion/provider.js +16 -13
  118. package/server/services/integrations/oauth_provider.js +115 -51
  119. package/server/services/integrations/registry.js +2 -0
  120. package/server/services/integrations/slack/provider.js +98 -9
  121. package/server/services/integrations/spotify/provider.js +67 -71
  122. package/server/services/integrations/trello/provider.js +21 -7
  123. package/server/services/integrations/weather/provider.js +18 -12
  124. package/server/services/integrations/whatsapp/provider.js +76 -16
  125. package/server/services/manager.js +110 -1
  126. package/server/services/memory/embedding_index.js +20 -8
  127. package/server/services/memory/embeddings.js +151 -90
  128. package/server/services/memory/ingestion.js +50 -9
  129. package/server/services/memory/ingestion_documents.js +13 -3
  130. package/server/services/memory/manager.js +52 -19
  131. package/server/services/messaging/automation.js +85 -10
  132. package/server/services/messaging/formatting_guides.js +7 -4
  133. package/server/services/messaging/http_platforms.js +33 -13
  134. package/server/services/messaging/inbound_queue.js +78 -24
  135. package/server/services/messaging/inbound_store.js +224 -0
  136. package/server/services/messaging/manager.js +326 -51
  137. package/server/services/messaging/typing_keepalive.js +5 -2
  138. package/server/services/messaging/whatsapp.js +22 -14
  139. package/server/services/network/http.js +210 -0
  140. package/server/services/network/safe_request.js +307 -0
  141. package/server/services/runtime/backends/local-vm.js +227 -67
  142. package/server/services/runtime/docker-vm-manager.js +9 -0
  143. package/server/services/runtime/guest_bootstrap.js +30 -4
  144. package/server/services/runtime/guest_image.js +43 -12
  145. package/server/services/runtime/manager.js +77 -23
  146. package/server/services/runtime/validation.js +7 -6
  147. package/server/services/security/tool_categories.js +6 -0
  148. package/server/services/social_reach/channels/github.js +10 -4
  149. package/server/services/social_reach/channels/reddit.js +4 -4
  150. package/server/services/social_reach/channels/rss.js +2 -2
  151. package/server/services/social_reach/channels/social_video.js +12 -7
  152. package/server/services/social_reach/channels/v2ex.js +21 -8
  153. package/server/services/social_reach/channels/x.js +2 -2
  154. package/server/services/social_reach/channels/xueqiu.js +5 -5
  155. package/server/services/social_reach/service.js +9 -6
  156. package/server/services/social_reach/utils.js +65 -14
  157. package/server/services/social_video/service.js +160 -50
  158. package/server/services/tasks/integration_runtime.js +18 -8
  159. package/server/services/tasks/runtime.js +39 -4
  160. package/server/services/voice/agentBridge.js +17 -4
  161. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  162. package/server/services/voice/liveSession.js +31 -0
  163. package/server/services/voice/message.js +1 -1
  164. package/server/services/voice/openaiSpeech.js +33 -8
  165. package/server/services/voice/providers.js +233 -151
  166. package/server/services/voice/runtime.js +2 -2
  167. package/server/services/voice/runtimeManager.js +118 -20
  168. package/server/services/voice/turnRunner.js +6 -0
  169. package/server/services/wearable/firmware_manifest.js +51 -13
  170. package/server/services/wearable/service.js +1 -0
  171. package/server/utils/abort.js +96 -0
  172. package/server/utils/cloud-security.js +110 -3
  173. package/server/utils/files.js +31 -0
  174. package/server/utils/image_payload.js +95 -0
  175. package/server/utils/retry.js +107 -0
@@ -1,4 +1,24 @@
1
+ 'use strict';
2
+
1
3
  const { BaseProvider } = require('./base');
4
+ const { fetchResponseText, readResponseText } = require('../../network/http');
5
+ const { createAbortError, isAbortError, throwIfAborted } = require('../../../utils/abort');
6
+ const { readOllamaStream } = require('./ollama_stream');
7
+
8
+ const MAX_CHAT_RESPONSE_BYTES = 16 * 1024 * 1024;
9
+
10
+ function ollamaError(message, status = null) {
11
+ const error = new Error(
12
+ status == null
13
+ ? `Ollama request failed: ${message}`
14
+ : `Ollama request failed (HTTP ${status}): ${message}`,
15
+ );
16
+ if (status != null) error.status = status;
17
+ if (/does not support tools|tools.*not supported/i.test(String(message || ''))) {
18
+ error.code = 'OLLAMA_TOOLS_UNSUPPORTED';
19
+ }
20
+ return error;
21
+ }
2
22
 
3
23
  class OllamaProvider extends BaseProvider {
4
24
  constructor(config = {}) {
@@ -8,22 +28,26 @@ class OllamaProvider extends BaseProvider {
8
28
  this.models = [];
9
29
  }
10
30
 
11
- async listModels() {
31
+ async listModels(signal = null) {
12
32
  try {
13
- const controller = new AbortController();
14
- const timer = setTimeout(() => controller.abort(), 5000);
15
- const res = await fetch(`${this.baseUrl}/api/tags`, { signal: controller.signal });
16
- clearTimeout(timer);
17
- const data = await res.json();
33
+ const { response, text } = await fetchResponseText(`${this.baseUrl}/api/tags`, {
34
+ maxResponseBytes: 2 * 1024 * 1024,
35
+ serviceName: 'Ollama model catalog',
36
+ signal,
37
+ timeoutMs: 5000,
38
+ });
39
+ if (!response.ok) throw new Error(`Ollama /api/tags returned HTTP ${response.status}`);
40
+ const data = JSON.parse(text || '{}');
18
41
  this.models = (data.models || []).map(m => m.name);
19
42
  return this.models;
20
- } catch {
43
+ } catch (err) {
44
+ if (isAbortError(err, signal)) throw createAbortError(signal);
21
45
  return [];
22
46
  }
23
47
  }
24
48
 
25
- async ensureModel(model) {
26
- const models = await this.listModels();
49
+ async ensureModel(model, signal = null) {
50
+ const models = await this.listModels(signal);
27
51
  // Normalization: Ollama often adds :latest if no tag is specified
28
52
  const normalizedModel = model.includes(':') ? model : `${model}:latest`;
29
53
  const found = models.some(m => m === model || m === normalizedModel);
@@ -39,12 +63,22 @@ class OllamaProvider extends BaseProvider {
39
63
  message: `Downloading local Ollama model '${model}'. First-time pulls can take a while.`
40
64
  });
41
65
  try {
42
- const res = await fetch(`${this.baseUrl}/api/pull`, {
66
+ const { response, text } = await fetchResponseText(`${this.baseUrl}/api/pull`, {
43
67
  method: 'POST',
44
68
  headers: { 'Content-Type': 'application/json' },
45
- body: JSON.stringify({ name: model, stream: false })
69
+ body: JSON.stringify({ name: model, stream: false }),
70
+ maxResponseBytes: 2 * 1024 * 1024,
71
+ serviceName: 'Ollama model pull',
72
+ signal,
73
+ timeoutMs: 5 * 60 * 1000,
46
74
  });
47
- if (!res.ok) throw new Error(`Pull failed: ${res.statusText}`);
75
+ if (!response.ok) {
76
+ let detail = text;
77
+ try { detail = JSON.parse(text || '{}').error || text; } catch {}
78
+ throw new Error(
79
+ `Pull failed with HTTP ${response.status}: ${String(detail || response.statusText).slice(0, 500)}`,
80
+ );
81
+ }
48
82
  console.log(`[Ollama] Model '${model}' pulled successfully.`);
49
83
  this.onStatus?.({
50
84
  kind: 'model_download',
@@ -54,9 +88,10 @@ class OllamaProvider extends BaseProvider {
54
88
  message: `Local Ollama model '${model}' is ready.`
55
89
  });
56
90
  // Refresh local model list
57
- await this.listModels();
91
+ await this.listModels(signal);
58
92
  return true;
59
93
  } catch (e) {
94
+ if (isAbortError(e, signal)) throw createAbortError(signal);
60
95
  this.onStatus?.({
61
96
  kind: 'model_download',
62
97
  status: 'failed',
@@ -122,42 +157,63 @@ class OllamaProvider extends BaseProvider {
122
157
  // status for others; surface both as real errors instead of letting callers
123
158
  // see a silently empty response. Tags models that reject tools so the caller
124
159
  // can transparently retry without them.
125
- async postChat(body) {
160
+ async postChat(body, signal = null) {
126
161
  const res = await fetch(`${this.baseUrl}/api/chat`, {
127
162
  method: 'POST',
128
163
  headers: { 'Content-Type': 'application/json' },
129
- body: JSON.stringify(body)
164
+ body: JSON.stringify(body),
165
+ signal,
130
166
  });
131
167
  if (!res.ok) {
132
- const detail = await res.text().catch(() => '');
168
+ const detail = await readResponseText(res, {
169
+ maxResponseBytes: 64 * 1024,
170
+ serviceName: 'Ollama chat error',
171
+ }).catch(() => '');
133
172
  let message = detail;
134
173
  try { message = JSON.parse(detail)?.error || detail; } catch {}
135
- const err = new Error(`Ollama /api/chat failed (HTTP ${res.status}): ${message || res.statusText}`);
136
- if (/does not support tools|tools.*not supported/i.test(message)) {
137
- err.code = 'OLLAMA_TOOLS_UNSUPPORTED';
138
- }
139
- throw err;
174
+ throw ollamaError(message || res.statusText, res.status);
140
175
  }
141
176
  return res;
142
177
  }
143
178
 
179
+ async readChatResponse(body, signal = null) {
180
+ const res = await this.postChat(body, signal);
181
+ const text = await readResponseText(res, {
182
+ maxResponseBytes: MAX_CHAT_RESPONSE_BYTES,
183
+ serviceName: 'Ollama chat',
184
+ });
185
+ throwIfAborted(signal, 'Ollama chat aborted.');
186
+ let data;
187
+ try {
188
+ data = JSON.parse(text);
189
+ } catch (cause) {
190
+ throw new Error('Ollama /api/chat returned malformed JSON.', { cause });
191
+ }
192
+ if (data.error) throw ollamaError(data.error);
193
+ return data;
194
+ }
195
+
144
196
  async chat(messages, tools = [], options = {}) {
145
197
  const model = options.model || this.config.model || 'llama3.1';
146
- await this.ensureModel(model);
198
+ await this.ensureModel(model, options.signal);
147
199
 
148
- let res;
200
+ let data;
149
201
  try {
150
- res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, false));
202
+ data = await this.readChatResponse(
203
+ this.buildChatBody(messages, tools, { ...options, model }, false),
204
+ options.signal,
205
+ );
151
206
  } catch (err) {
152
207
  if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
153
208
  console.warn(`[Ollama] Model '${model}' does not support tools; retrying without them.`);
154
- res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, false));
209
+ data = await this.readChatResponse(
210
+ this.buildChatBody(messages, [], { ...options, model }, false),
211
+ options.signal,
212
+ );
155
213
  } else {
156
214
  throw err;
157
215
  }
158
216
  }
159
-
160
- const data = await res.json();
161
217
  const msg = data.message || {};
162
218
 
163
219
  return {
@@ -182,67 +238,39 @@ class OllamaProvider extends BaseProvider {
182
238
 
183
239
  async *stream(messages, tools = [], options = {}) {
184
240
  const model = options.model || this.config.model || 'llama3.1';
185
- await this.ensureModel(model);
186
-
187
- let res;
188
- try {
189
- res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, true));
190
- } catch (err) {
191
- if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
192
- console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
193
- res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, true));
194
- } else {
195
- throw err;
196
- }
197
- }
198
-
199
- const reader = res.body.getReader();
200
- const decoder = new TextDecoder();
201
- let content = '';
202
- let buffer = '';
203
- let accumulatedToolCalls = [];
241
+ await this.ensureModel(model, options.signal);
204
242
 
243
+ let requestTools = tools;
244
+ let retriedWithoutTools = false;
205
245
  while (true) {
206
- const { done, value } = await reader.read();
207
- if (done) break;
208
-
209
- buffer += decoder.decode(value, { stream: true });
210
- const lines = buffer.split('\n');
211
- buffer = lines.pop() || '';
212
-
213
- for (const line of lines) {
214
- if (!line.trim()) continue;
215
- try {
216
- const data = JSON.parse(line);
217
- if (data.message?.content) {
218
- content += data.message.content;
219
- yield { type: 'content', content: data.message.content };
220
- }
221
- if (data.message?.tool_calls && Array.isArray(data.message.tool_calls)) {
222
- const mapped = data.message.tool_calls.map((tc, i) => ({
223
- id: `call_ollama_${Date.now()}_${i}`,
224
- type: 'function',
225
- function: {
226
- name: tc.function.name,
227
- arguments: typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments || {})
228
- }
229
- }));
230
- accumulatedToolCalls = accumulatedToolCalls.concat(mapped);
231
- }
232
- if (data.done) {
233
- yield {
234
- type: 'done',
235
- content,
236
- toolCalls: accumulatedToolCalls,
237
- finishReason: accumulatedToolCalls.length > 0 ? 'tool_calls' : 'stop',
238
- usage: data.prompt_eval_count ? {
239
- promptTokens: data.prompt_eval_count || 0,
240
- completionTokens: data.eval_count || 0,
241
- totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
242
- } : null
243
- };
244
- }
245
- } catch {}
246
+ let emittedChunk = false;
247
+ try {
248
+ const res = await this.postChat(
249
+ this.buildChatBody(messages, requestTools, { ...options, model }, true),
250
+ options.signal,
251
+ );
252
+ for await (const chunk of readOllamaStream(res, {
253
+ errorFactory: ollamaError,
254
+ maxResponseBytes: MAX_CHAT_RESPONSE_BYTES,
255
+ signal: options.signal,
256
+ })) {
257
+ emittedChunk = true;
258
+ yield chunk;
259
+ }
260
+ return;
261
+ } catch (err) {
262
+ if (
263
+ err.code === 'OLLAMA_TOOLS_UNSUPPORTED'
264
+ && tools.length > 0
265
+ && !retriedWithoutTools
266
+ && !emittedChunk
267
+ ) {
268
+ console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
269
+ retriedWithoutTools = true;
270
+ requestTools = [];
271
+ continue;
272
+ }
273
+ throw err;
246
274
  }
247
275
  }
248
276
  }
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const { waitForAbortableResult } = require('../../network/http');
4
+ const { throwIfAborted } = require('../../../utils/abort');
5
+
6
+ function malformedStreamError(cause = null) {
7
+ const error = new Error('Ollama /api/chat returned malformed streaming JSON.', cause ? { cause } : undefined);
8
+ error.code = 'OLLAMA_STREAM_MALFORMED';
9
+ return error;
10
+ }
11
+
12
+ function streamEndedEarlyError() {
13
+ const error = new Error('Ollama stream ended before sending a completion marker.');
14
+ error.code = 'OLLAMA_STREAM_INCOMPLETE';
15
+ return error;
16
+ }
17
+
18
+ function parseLine(line) {
19
+ if (!line.trim()) return null;
20
+ try {
21
+ const value = JSON.parse(line);
22
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
23
+ throw malformedStreamError();
24
+ }
25
+ return value;
26
+ } catch (error) {
27
+ if (error?.code === 'OLLAMA_STREAM_MALFORMED') throw error;
28
+ throw malformedStreamError(error);
29
+ }
30
+ }
31
+
32
+ function usageFrom(data) {
33
+ if (data.prompt_eval_count == null && data.eval_count == null) return null;
34
+ const promptTokens = Number(data.prompt_eval_count) || 0;
35
+ const completionTokens = Number(data.eval_count) || 0;
36
+ return {
37
+ promptTokens,
38
+ completionTokens,
39
+ totalTokens: promptTokens + completionTokens,
40
+ };
41
+ }
42
+
43
+ function consumeData(data, state, errorFactory) {
44
+ if (data.error) throw errorFactory(data.error);
45
+ const chunks = [];
46
+ if (data.message?.content) {
47
+ const content = String(data.message.content);
48
+ state.content += content;
49
+ chunks.push({ type: 'content', content });
50
+ }
51
+ if (Array.isArray(data.message?.tool_calls)) {
52
+ for (const toolCall of data.message.tool_calls) {
53
+ const name = String(toolCall?.function?.name || '').trim();
54
+ if (!name) throw malformedStreamError();
55
+ state.toolCalls.push({
56
+ id: `call_ollama_${state.callSeed}_${state.nextCallIndex}`,
57
+ type: 'function',
58
+ function: {
59
+ name,
60
+ arguments: typeof toolCall.function.arguments === 'string'
61
+ ? toolCall.function.arguments
62
+ : JSON.stringify(toolCall.function.arguments || {}),
63
+ },
64
+ });
65
+ state.nextCallIndex += 1;
66
+ }
67
+ }
68
+ if (data.done === true) {
69
+ state.done = true;
70
+ chunks.push({
71
+ type: 'done',
72
+ content: state.content,
73
+ toolCalls: state.toolCalls,
74
+ finishReason: state.toolCalls.length > 0 ? 'tool_calls' : 'stop',
75
+ usage: usageFrom(data),
76
+ });
77
+ }
78
+ return chunks;
79
+ }
80
+
81
+ async function* readOllamaStream(response, options = {}) {
82
+ const reader = response?.body?.getReader?.();
83
+ if (!reader) throw new Error('Ollama /api/chat returned no streaming response body.');
84
+ const decoder = new TextDecoder();
85
+ const maxResponseBytes = Number(options.maxResponseBytes) > 0
86
+ ? Number(options.maxResponseBytes)
87
+ : 16 * 1024 * 1024;
88
+ const errorFactory = options.errorFactory || ((message) => new Error(String(message || 'Ollama error')));
89
+ const state = {
90
+ callSeed: Date.now(),
91
+ content: '',
92
+ done: false,
93
+ nextCallIndex: 0,
94
+ toolCalls: [],
95
+ };
96
+ let buffer = '';
97
+ let responseBytes = 0;
98
+
99
+ const emitLine = (line) => {
100
+ const data = parseLine(line);
101
+ return data ? consumeData(data, state, errorFactory) : [];
102
+ };
103
+
104
+ try {
105
+ while (!state.done) {
106
+ throwIfAborted(options.signal, 'Ollama stream aborted.');
107
+ const { done, value } = await waitForAbortableResult(
108
+ reader.read(),
109
+ options.signal,
110
+ 'Ollama stream aborted.',
111
+ );
112
+ if (done) break;
113
+ responseBytes += value?.byteLength || 0;
114
+ if (responseBytes > maxResponseBytes) {
115
+ const error = new Error('Ollama stream exceeded its response safety limit.');
116
+ error.code = 'HTTP_RESPONSE_TOO_LARGE';
117
+ throw error;
118
+ }
119
+
120
+ buffer += decoder.decode(value, { stream: true });
121
+ const lines = buffer.split('\n');
122
+ buffer = lines.pop() || '';
123
+ for (const line of lines) {
124
+ for (const chunk of emitLine(line)) yield chunk;
125
+ if (state.done) return;
126
+ }
127
+ }
128
+
129
+ buffer += decoder.decode();
130
+ if (buffer.trim()) {
131
+ for (const chunk of emitLine(buffer)) yield chunk;
132
+ }
133
+ if (!state.done) throw streamEndedEarlyError();
134
+ } finally {
135
+ await reader.cancel().catch(() => {});
136
+ reader.releaseLock?.();
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ readOllamaStream,
142
+ };
@@ -1,12 +1,20 @@
1
1
  const OpenAI = require('openai');
2
2
  const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+ const { wrapProviderError } = require('./provider_error');
3
4
 
4
5
  class OpenAIProvider extends OpenAICompatibleProvider {
5
6
  constructor(config = {}) {
6
7
  super(config);
7
8
  this.name = 'openai';
8
9
  this.models = [
10
+ 'gpt-5.6',
11
+ 'gpt-5.6-sol',
12
+ 'gpt-5.6-terra',
13
+ 'gpt-5.6-luna',
9
14
  'gpt-5.5',
15
+ 'gpt-5.4',
16
+ 'gpt-5.4-mini',
17
+ 'gpt-5.4-nano',
10
18
  'gpt-5',
11
19
  'gpt-5-mini',
12
20
  'gpt-5-nano',
@@ -16,9 +24,35 @@ class OpenAIProvider extends OpenAICompatibleProvider {
16
24
  'o4-mini'
17
25
  ];
18
26
  // Reasoning models: no temperature, use max_completion_tokens, support reasoning_effort
19
- this.reasoningModels = new Set(['gpt-5.5', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5.2', 'gpt-5.1', 'o1', 'o3', 'o3-pro', 'o4-mini', 'o3-mini']);
27
+ this.reasoningModels = new Set([
28
+ 'gpt-5.6',
29
+ 'gpt-5.6-sol',
30
+ 'gpt-5.6-terra',
31
+ 'gpt-5.6-luna',
32
+ 'gpt-5.5',
33
+ 'gpt-5.4',
34
+ 'gpt-5.4-mini',
35
+ 'gpt-5.4-nano',
36
+ 'gpt-5',
37
+ 'gpt-5-mini',
38
+ 'gpt-5-nano',
39
+ 'gpt-5.2',
40
+ 'gpt-5.1',
41
+ 'o1',
42
+ 'o3',
43
+ 'o3-pro',
44
+ 'o4-mini',
45
+ 'o3-mini',
46
+ ]);
20
47
  this.contextWindows = {
21
- 'gpt-5.5': 1000000,
48
+ 'gpt-5.6': 1050000,
49
+ 'gpt-5.6-sol': 1050000,
50
+ 'gpt-5.6-terra': 1050000,
51
+ 'gpt-5.6-luna': 1050000,
52
+ 'gpt-5.5': 1050000,
53
+ 'gpt-5.4': 1050000,
54
+ 'gpt-5.4-mini': 400000,
55
+ 'gpt-5.4-nano': 400000,
22
56
  'gpt-5': 400000,
23
57
  'gpt-5-mini': 400000,
24
58
  'gpt-5-nano': 128000,
@@ -36,15 +70,15 @@ class OpenAIProvider extends OpenAICompatibleProvider {
36
70
  });
37
71
  }
38
72
 
39
- async listModels() {
73
+ async listModels(signal = null) {
40
74
  try {
41
- const res = await this.client.models.list();
75
+ const res = await this.client.models.list({ signal });
42
76
  const DROP = /dall-e|whisper|tts|embed|moderat|realtime|audio|transcribe|search-api|-image-|babbage|davinci-002|^sora|-instruct/i;
43
77
  return res.data
44
78
  .filter((m) => !DROP.test(m.id))
45
79
  .map((m) => ({ id: m.id, name: m.id }));
46
80
  } catch (err) {
47
- throw new Error(`Failed to list OpenAI models: ${err.message || String(err)}`);
81
+ throw wrapProviderError(err, 'Failed to list OpenAI models', { signal });
48
82
  }
49
83
  }
50
84
 
@@ -108,7 +142,7 @@ class OpenAIProvider extends OpenAICompatibleProvider {
108
142
  const model = options.model || this.config.model || this.getDefaultModel();
109
143
  const params = this._buildParams(model, messages, tools, options);
110
144
 
111
- const response = await this.client.chat.completions.create(params);
145
+ const response = await this.client.chat.completions.create(params, { signal: options.signal });
112
146
  const choice = response.choices[0];
113
147
 
114
148
  return {
@@ -125,7 +159,7 @@ class OpenAIProvider extends OpenAICompatibleProvider {
125
159
  const params = this._buildParams(model, messages, tools, options);
126
160
  params.stream = true;
127
161
  params.stream_options = { include_usage: true };
128
- const stream = await this.client.chat.completions.create(params);
162
+ const stream = await this.client.chat.completions.create(params, { signal: options.signal });
129
163
 
130
164
  let currentToolCalls = [];
131
165
  let content = '';
@@ -1,6 +1,9 @@
1
+ 'use strict';
2
+
1
3
  const crypto = require('crypto');
2
4
  const OpenAI = require('openai');
3
5
  const { BaseProvider } = require('./base');
6
+ const { wrapProviderError } = require('./provider_error');
4
7
 
5
8
  const DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api/codex';
6
9
  const OPENAI_CODEX_EMPTY_INPUT_TEXT = ' ';
@@ -436,10 +439,13 @@ class OpenAICodexProvider extends BaseProvider {
436
439
  try {
437
440
  response = await this.client.responses.create(
438
441
  { model, ...request },
439
- { headers: this._requestHeaders() },
442
+ { headers: this._requestHeaders(), signal: options.signal },
440
443
  );
441
444
  } catch (err) {
442
- throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
445
+ throw wrapProviderError(err, 'OpenAI Codex request failed', {
446
+ detail: formatOpenAIError(err),
447
+ signal: options.signal,
448
+ });
443
449
  }
444
450
 
445
451
  const toolCalls = extractToolCalls(response);
@@ -464,10 +470,13 @@ class OpenAICodexProvider extends BaseProvider {
464
470
  try {
465
471
  stream = await this.client.responses.create(
466
472
  { model, ...request, stream: true },
467
- { headers: this._requestHeaders() },
473
+ { headers: this._requestHeaders(), signal: options.signal },
468
474
  );
469
475
  } catch (err) {
470
- throw new Error(`OpenAI Codex request failed: ${formatOpenAIError(err)}`);
476
+ throw wrapProviderError(err, 'OpenAI Codex request failed', {
477
+ detail: formatOpenAIError(err),
478
+ signal: options.signal,
479
+ });
471
480
  }
472
481
 
473
482
  let content = '';
@@ -1,5 +1,7 @@
1
1
  const OpenAI = require('openai');
2
2
  const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+ const { fetchResponseText } = require('../../network/http');
4
+ const { wrapProviderError } = require('./provider_error');
3
5
 
4
6
  const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
5
7
 
@@ -25,12 +27,26 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
25
27
  });
26
28
  }
27
29
 
28
- async listModels() {
29
- const res = await fetch(`${this.baseURL}/models`, {
30
+ async listModels(signal = null) {
31
+ const { response, text } = await fetchResponseText(`${this.baseURL}/models`, {
30
32
  headers: { 'Authorization': `Bearer ${this.client.apiKey}` },
33
+ maxResponseBytes: 5 * 1024 * 1024,
34
+ serviceName: 'OpenRouter model catalog',
35
+ signal,
31
36
  });
32
- if (!res.ok) throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
33
- const { data } = await res.json();
37
+ if (!response.ok) {
38
+ const error = new Error(`OpenRouter /models returned HTTP ${response.status}`);
39
+ error.status = response.status;
40
+ error.headers = response.headers;
41
+ throw error;
42
+ }
43
+ let payload;
44
+ try {
45
+ payload = JSON.parse(text || '{}');
46
+ } catch {
47
+ throw new Error('OpenRouter /models returned invalid JSON.');
48
+ }
49
+ const { data } = payload;
34
50
  const models = data || [];
35
51
  for (const m of models) {
36
52
  if (m.context_length) contextWindowCache.set(m.id, m.context_length);
@@ -72,9 +88,11 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
72
88
  const params = this._buildParams(model, messages, tools, options);
73
89
  let response;
74
90
  try {
75
- response = await this.client.chat.completions.create(params);
91
+ response = await this.client.chat.completions.create(params, { signal: options.signal });
76
92
  } catch (err) {
77
- throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
93
+ throw wrapProviderError(err, 'OpenRouter request failed', {
94
+ signal: options.signal,
95
+ });
78
96
  }
79
97
  // OpenRouter returns HTTP 200 even for errors (rate limits, model unavailable, etc.)
80
98
  const orErr = this._extractOpenRouterError(response);
@@ -95,9 +113,11 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
95
113
 
96
114
  let stream;
97
115
  try {
98
- stream = await this.client.chat.completions.create(params);
116
+ stream = await this.client.chat.completions.create(params, { signal: options.signal });
99
117
  } catch (err) {
100
- throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
118
+ throw wrapProviderError(err, 'OpenRouter request failed', {
119
+ signal: options.signal,
120
+ });
101
121
  }
102
122
 
103
123
  let toolCalls = [];
@@ -148,7 +168,9 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
148
168
  }
149
169
  } catch (err) {
150
170
  // Re-throw SDK APIErrors (from OpenRouter SSE error events) with OpenRouter context.
151
- throw new Error(`OpenRouter: ${err?.message || String(err)}`);
171
+ throw wrapProviderError(err, 'OpenRouter stream failed', {
172
+ signal: options.signal,
173
+ });
152
174
  }
153
175
 
154
176
  if (toolCalls.length > 0) {
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { createAbortError, isAbortError } = require('../../../utils/abort');
4
+
5
+ function sanitizeProviderErrorDetail(value) {
6
+ return String(value || 'Unknown provider error')
7
+ .slice(0, 2000)
8
+ .replace(/\b(Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
9
+ .replace(/([?&](?:key|api_key|access_token)=)[^&\s]+/gi, '$1[redacted]')
10
+ .replace(
11
+ /\b(api[_-]?key|access_token|refresh_token|authorization)\b\s*[:=]\s*["']?[^\s,"'}\]]+/gi,
12
+ '$1=[redacted]',
13
+ );
14
+ }
15
+
16
+ function wrapProviderError(error, prefix, options = {}) {
17
+ if (options.signal?.aborted) return createAbortError(options.signal);
18
+ if (isAbortError(error)) return error;
19
+
20
+ const detail = typeof options.detail === 'string'
21
+ ? options.detail
22
+ : error?.message || String(error);
23
+ const wrapped = new Error(
24
+ `${prefix}: ${sanitizeProviderErrorDetail(detail)}`,
25
+ { cause: error },
26
+ );
27
+ for (const property of ['status', 'statusCode', 'code', 'headers', 'response', 'type']) {
28
+ if (error?.[property] !== undefined) wrapped[property] = error[property];
29
+ }
30
+ return wrapped;
31
+ }
32
+
33
+ module.exports = {
34
+ sanitizeProviderErrorDetail,
35
+ wrapProviderError,
36
+ };