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,5 +1,62 @@
1
- const { GoogleGenerativeAI } = require('@google/generative-ai');
1
+ 'use strict';
2
+
3
+ const { GoogleGenAI } = require('@google/genai');
2
4
  const { BaseProvider } = require('./base');
5
+ const { fetchResponseText } = require('../../network/http');
6
+
7
+ function parseToolArguments(value) {
8
+ try {
9
+ const parsed = JSON.parse(value || '{}');
10
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
11
+ ? parsed
12
+ : {};
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+
18
+ function normalizeUsage(usage) {
19
+ if (!usage) return null;
20
+ return {
21
+ inputTokens: usage.promptTokenCount || 0,
22
+ outputTokens: usage.candidatesTokenCount || 0,
23
+ reasoningTokens: usage.thoughtsTokenCount || 0,
24
+ cachedReadTokens: usage.cachedContentTokenCount || 0,
25
+ cacheWriteTokens: 0,
26
+ promptTokens: usage.promptTokenCount || 0,
27
+ completionTokens: usage.candidatesTokenCount || 0,
28
+ totalTokens: usage.totalTokenCount || 0
29
+ };
30
+ }
31
+
32
+ function collectResponseParts(response, toolCalls, seenToolCalls = null) {
33
+ let content = '';
34
+ for (const candidate of response?.candidates || []) {
35
+ for (const part of candidate.content?.parts || []) {
36
+ if (part.text && part.thought !== true) content += part.text;
37
+ if (!part.functionCall?.name) continue;
38
+
39
+ const args = part.functionCall.args || {};
40
+ const providerCallId = String(part.functionCall.id || '').trim();
41
+ const signature = providerCallId
42
+ || `${part.functionCall.name}:${JSON.stringify(args)}`;
43
+ if (seenToolCalls?.has(signature)) continue;
44
+ seenToolCalls?.add(signature);
45
+
46
+ toolCalls.push({
47
+ id: providerCallId
48
+ || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
49
+ type: 'function',
50
+ function: {
51
+ name: part.functionCall.name,
52
+ arguments: JSON.stringify(args),
53
+ thought_signature: part.thoughtSignature
54
+ }
55
+ });
56
+ }
57
+ }
58
+ return content;
59
+ }
3
60
 
4
61
  class GoogleProvider extends BaseProvider {
5
62
  constructor(config = {}) {
@@ -28,16 +85,33 @@ class GoogleProvider extends BaseProvider {
28
85
  'gemini-1.5-flash': 1048576,
29
86
  };
30
87
  this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
31
- this.genAI = new GoogleGenerativeAI(this.apiKey);
88
+ this.genAI = new GoogleGenAI({ apiKey: this.apiKey });
32
89
  }
33
90
 
34
- async listModels() {
91
+ async listModels(signal = null) {
35
92
  const DROP = /tts|lyria|robotics|deep-research|antigravity|computer-use|-image(?!.*it)/i;
36
- const res = await fetch(
37
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.apiKey}&pageSize=200`
93
+ const { response, text } = await fetchResponseText(
94
+ 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200',
95
+ {
96
+ headers: { 'x-goog-api-key': this.apiKey },
97
+ maxResponseBytes: 5 * 1024 * 1024,
98
+ serviceName: 'Google model catalog',
99
+ signal,
100
+ },
38
101
  );
39
- if (!res.ok) throw new Error(`Google models API returned ${res.status}`);
40
- const { models = [] } = await res.json();
102
+ if (!response.ok) {
103
+ const error = new Error(`Google models API returned ${response.status}`);
104
+ error.status = response.status;
105
+ error.headers = response.headers;
106
+ throw error;
107
+ }
108
+ let payload;
109
+ try {
110
+ payload = JSON.parse(text || '{}');
111
+ } catch {
112
+ throw new Error('Google models API returned invalid JSON.');
113
+ }
114
+ const { models = [] } = payload;
41
115
  return models
42
116
  .filter((m) => {
43
117
  const id = m.name.replace('models/', '');
@@ -60,11 +134,23 @@ class GoogleProvider extends BaseProvider {
60
134
  functionDeclarations: tools.map(tool => ({
61
135
  name: tool.name,
62
136
  description: tool.description,
63
- parameters: tool.parameters || { type: 'object', properties: {} }
137
+ parametersJsonSchema: tool.parameters || { type: 'object', properties: {} }
64
138
  }))
65
139
  }];
66
140
  }
67
141
 
142
+ buildGenerateConfig(systemInstruction, tools, options) {
143
+ const config = {};
144
+ if (systemInstruction) config.systemInstruction = systemInstruction;
145
+ if (tools.length > 0) config.tools = this.formatTools(tools);
146
+ if (options.signal) config.abortSignal = options.signal;
147
+ const maxOutputTokens = Number(options.maxTokens);
148
+ if (Number.isFinite(maxOutputTokens) && maxOutputTokens > 0) {
149
+ config.maxOutputTokens = Math.floor(maxOutputTokens);
150
+ }
151
+ return config;
152
+ }
153
+
68
154
  convertMessages(messages) {
69
155
  let systemInstruction = '';
70
156
  const history = [];
@@ -75,14 +161,16 @@ class GoogleProvider extends BaseProvider {
75
161
  continue;
76
162
  }
77
163
  if (msg.role === 'tool') {
164
+ const functionResponse = {
165
+ name: msg.name || 'tool',
166
+ response: { result: msg.content }
167
+ };
168
+ if (msg.tool_call_id) functionResponse.id = msg.tool_call_id;
78
169
  history.push({
79
- role: 'function',
80
- parts: [{
81
- functionResponse: {
82
- name: msg.name || 'tool',
83
- response: { result: msg.content }
84
- }
85
- }]
170
+ // The current Gemini API represents tool results as functionResponse
171
+ // parts on a user turn.
172
+ role: 'user',
173
+ parts: [{ functionResponse }]
86
174
  });
87
175
  continue;
88
176
  }
@@ -93,9 +181,10 @@ class GoogleProvider extends BaseProvider {
93
181
  const functionCallPart = {
94
182
  functionCall: {
95
183
  name: tc.function.name,
96
- args: JSON.parse(tc.function.arguments || '{}')
184
+ args: parseToolArguments(tc.function.arguments)
97
185
  }
98
186
  };
187
+ if (tc.id) functionCallPart.functionCall.id = tc.id;
99
188
  if (tc.function.thought_signature) {
100
189
  functionCallPart.thoughtSignature = tc.function.thought_signature;
101
190
  }
@@ -129,6 +218,18 @@ class GoogleProvider extends BaseProvider {
129
218
  normalizedHistory.push({ role: currentRole, parts: currentParts });
130
219
  }
131
220
 
221
+ // Structured helpers can consist entirely of system instructions. Gemini
222
+ // still requires a user turn to trigger generation.
223
+ if (
224
+ normalizedHistory.length === 0
225
+ || normalizedHistory[normalizedHistory.length - 1].role === 'model'
226
+ ) {
227
+ normalizedHistory.push({
228
+ role: 'user',
229
+ parts: [{ text: 'Provide the requested response using the system instructions.' }]
230
+ });
231
+ }
232
+
132
233
  if (normalizedHistory.length > 0 && normalizedHistory[0].role !== 'user') {
133
234
  normalizedHistory.unshift({
134
235
  role: 'user',
@@ -142,53 +243,18 @@ class GoogleProvider extends BaseProvider {
142
243
  async chat(messages, tools = [], options = {}) {
143
244
  const model = options.model || this.config.model || this.getDefaultModel();
144
245
  const { systemInstruction, history } = this.convertMessages(messages);
145
-
146
- const genModel = this.genAI.getGenerativeModel({
246
+ const response = await this.genAI.models.generateContent({
147
247
  model,
148
- systemInstruction: systemInstruction || undefined,
149
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
248
+ contents: history,
249
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
150
250
  });
151
-
152
- const lastMessage = history.pop();
153
- const chat = genModel.startChat({ history });
154
- const result = await chat.sendMessage(lastMessage.parts);
155
- const response = result.response;
156
-
157
- let content = '';
158
251
  const toolCalls = [];
159
-
160
- for (const candidate of response.candidates || []) {
161
- for (const part of candidate.content?.parts || []) {
162
- if (part.text) content += part.text;
163
- if (part.functionCall) {
164
- toolCalls.push({
165
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
166
- type: 'function',
167
- function: {
168
- name: part.functionCall.name,
169
- arguments: JSON.stringify(part.functionCall.args || {}),
170
- thought_signature: part.thoughtSignature
171
- }
172
- });
173
- }
174
- }
175
- }
176
-
177
- const usage = response.usageMetadata;
252
+ const content = collectResponseParts(response, toolCalls);
178
253
  return {
179
254
  content,
180
255
  toolCalls,
181
256
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
182
- usage: usage ? {
183
- inputTokens: usage.promptTokenCount || 0,
184
- outputTokens: usage.candidatesTokenCount || 0,
185
- reasoningTokens: usage.thoughtsTokenCount || 0,
186
- cachedReadTokens: usage.cachedContentTokenCount || 0,
187
- cacheWriteTokens: 0,
188
- promptTokens: usage.promptTokenCount || 0,
189
- completionTokens: usage.candidatesTokenCount || 0,
190
- totalTokens: usage.totalTokenCount || 0
191
- } : null,
257
+ usage: normalizeUsage(response.usageMetadata),
192
258
  model
193
259
  };
194
260
  }
@@ -196,60 +262,32 @@ class GoogleProvider extends BaseProvider {
196
262
  async *stream(messages, tools = [], options = {}) {
197
263
  const model = options.model || this.config.model || this.getDefaultModel();
198
264
  const { systemInstruction, history } = this.convertMessages(messages);
199
-
200
- const genModel = this.genAI.getGenerativeModel({
265
+ const responseStream = await this.genAI.models.generateContentStream({
201
266
  model,
202
- systemInstruction: systemInstruction || undefined,
203
- tools: tools.length > 0 ? this.formatTools(tools) : undefined
267
+ contents: history,
268
+ config: this.buildGenerateConfig(systemInstruction, tools, options)
204
269
  });
205
-
206
- const lastMessage = history.pop();
207
- const chat = genModel.startChat({ history });
208
- const result = await chat.sendMessageStream(lastMessage.parts);
209
-
210
270
  let content = '';
211
271
  const toolCalls = [];
272
+ const seenToolCalls = new Set();
273
+ let usage = null;
212
274
 
213
- for await (const chunk of result.stream) {
214
- for (const candidate of chunk.candidates || []) {
215
- for (const part of candidate.content?.parts || []) {
216
- if (part.text) {
217
- content += part.text;
218
- yield { type: 'content', content: part.text };
219
- }
220
- if (part.functionCall) {
221
- toolCalls.push({
222
- id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
223
- type: 'function',
224
- function: {
225
- name: part.functionCall.name,
226
- arguments: JSON.stringify(part.functionCall.args || {}),
227
- thought_signature: part.thoughtSignature
228
- }
229
- });
230
- }
231
- }
275
+ for await (const chunk of responseStream) {
276
+ const chunkContent = collectResponseParts(chunk, toolCalls, seenToolCalls);
277
+ if (chunkContent) {
278
+ content += chunkContent;
279
+ yield { type: 'content', content: chunkContent };
232
280
  }
281
+ if (chunk.usageMetadata) usage = chunk.usageMetadata;
233
282
  }
234
283
 
235
- const finalResponse = await result.response;
236
- const usage = finalResponse.usageMetadata;
237
-
238
284
  yield {
239
285
  type: 'done',
240
286
  content,
241
287
  toolCalls,
242
288
  finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
243
- usage: usage ? {
244
- inputTokens: usage.promptTokenCount || 0,
245
- outputTokens: usage.candidatesTokenCount || 0,
246
- reasoningTokens: usage.thoughtsTokenCount || 0,
247
- cachedReadTokens: usage.cachedContentTokenCount || 0,
248
- cacheWriteTokens: 0,
249
- promptTokens: usage.promptTokenCount || 0,
250
- completionTokens: usage.candidatesTokenCount || 0,
251
- totalTokens: usage.totalTokenCount || 0
252
- } : null
289
+ usage: normalizeUsage(usage),
290
+ model
253
291
  };
254
292
  }
255
293
  }
@@ -1,5 +1,6 @@
1
1
  const OpenAI = require('openai');
2
2
  const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+ const { wrapProviderError } = require('./provider_error');
3
4
 
4
5
  class GrokProvider extends OpenAICompatibleProvider {
5
6
  constructor(config = {}) {
@@ -11,15 +12,15 @@ class GrokProvider extends OpenAICompatibleProvider {
11
12
  });
12
13
  }
13
14
 
14
- async listModels() {
15
+ async listModels(signal = null) {
15
16
  try {
16
- const res = await this.client.models.list();
17
+ const res = await this.client.models.list({ signal });
17
18
  const DROP = /imagine|diffus|embed|-tts/i;
18
19
  return res.data
19
20
  .filter((m) => !DROP.test(m.id))
20
21
  .map((m) => ({ id: m.id, name: m.id }));
21
22
  } catch (err) {
22
- throw new Error(`Failed to list Grok models: ${err.message || String(err)}`);
23
+ throw wrapProviderError(err, 'Failed to list Grok models', { signal });
23
24
  }
24
25
  }
25
26
 
@@ -61,7 +62,7 @@ class GrokProvider extends OpenAICompatibleProvider {
61
62
  const model = options.model || 'grok-4-1-fast-reasoning';
62
63
  const params = this._buildParams(model, messages, tools, options);
63
64
 
64
- const response = await this.client.chat.completions.create(params);
65
+ const response = await this.client.chat.completions.create(params, { signal: options.signal });
65
66
  return this.normalizeResponse(response);
66
67
  }
67
68
 
@@ -73,7 +74,7 @@ class GrokProvider extends OpenAICompatibleProvider {
73
74
  stream_options: { include_usage: true }
74
75
  };
75
76
 
76
- const stream = await this.client.chat.completions.create(params);
77
+ const stream = await this.client.chat.completions.create(params, { signal: options.signal });
77
78
 
78
79
  let toolCalls = [];
79
80
  let content = '';
@@ -1,12 +1,14 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const OpenAI = require('openai');
2
+ const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
3
+ const { fetchResponseText } = require('../../network/http');
4
4
  const { GrokProvider } = require('./grok');
5
5
 
6
6
  const GROK_OAUTH_BASE_URL = 'https://api.x.ai/v1';
7
7
  const GROK_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
8
8
  const GROK_OAUTH_TOKEN_URL = 'https://auth.x.ai/oauth2/token';
9
9
  const GROK_OAUTH_SCOPES = 'openid profile email offline_access grok-cli:access api:access';
10
+ const OAUTH_REFRESH_TIMEOUT_MS = 30000;
11
+ const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
10
12
 
11
13
  function normalizeExpiresAt(data) {
12
14
  if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
@@ -21,29 +23,14 @@ function normalizeExpiresAt(data) {
21
23
  function persistEnvValue(key, value) {
22
24
  if (!value) return;
23
25
  try {
24
- const { ENV_FILE } = require('../../../../runtime/paths');
25
- const safeKey = String(key).replace(/[\r\n]/g, '');
26
- const safeValue = String(value).replace(/[\r\n]/g, '');
27
- const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
28
- const lines = raw ? raw.split('\n') : [];
29
- let replaced = false;
30
- for (let i = 0; i < lines.length; i++) {
31
- if (lines[i].startsWith(`${safeKey}=`)) {
32
- lines[i] = `${safeKey}=${safeValue}`;
33
- replaced = true;
34
- break;
35
- }
36
- }
37
- if (!replaced) lines.push(`${safeKey}=${safeValue}`);
38
- const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
39
- fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
40
- fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
26
+ upsertEnvValue(ENV_FILE, key, value);
41
27
  } catch { }
42
28
  }
43
29
 
44
- async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
30
+ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
45
31
  if (!refreshToken) return null;
46
- const response = await fetchImpl(GROK_OAUTH_TOKEN_URL, {
32
+ const { response, text } = await fetchResponseText(GROK_OAUTH_TOKEN_URL, {
33
+ fetchImpl,
47
34
  method: 'POST',
48
35
  headers: {
49
36
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -54,9 +41,14 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
54
41
  refresh_token: refreshToken,
55
42
  client_id: GROK_OAUTH_CLIENT_ID,
56
43
  }),
44
+ signal,
45
+ timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
46
+ maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
47
+ serviceName: 'Grok OAuth refresh',
48
+ timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
49
+ tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
57
50
  });
58
51
 
59
- const text = await response.text();
60
52
  let data = {};
61
53
  try {
62
54
  data = text ? JSON.parse(text) : {};
@@ -68,7 +60,7 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
68
60
  if (data?.error === 'invalid_grant') {
69
61
  throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
70
62
  }
71
- const detail = data?.error_description || data?.error || text || 'Unknown error';
63
+ const detail = String(data?.error_description || data?.error || text || 'Unknown error').slice(0, 2000);
72
64
  throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
73
65
  }
74
66
  if (!data.access_token) {
@@ -102,8 +94,8 @@ class GrokOAuthProvider extends GrokProvider {
102
94
  this.fetchImpl = config.fetch || fetch;
103
95
  }
104
96
 
105
- async refreshClient() {
106
- const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl);
97
+ async refreshClient(signal = null) {
98
+ const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl, signal);
107
99
  if (!refreshed?.access) return false;
108
100
  this.authToken = refreshed.access;
109
101
  this.refreshToken = refreshed.refresh || this.refreshToken;
@@ -122,7 +114,7 @@ class GrokOAuthProvider extends GrokProvider {
122
114
  return await super.chat(messages, tools, options);
123
115
  } catch (err) {
124
116
  if (err?.status !== 401 || !this.refreshToken) throw err;
125
- await this.refreshClient();
117
+ await this.refreshClient(options.signal);
126
118
  return await super.chat(messages, tools, options);
127
119
  }
128
120
  }
@@ -132,7 +124,7 @@ class GrokOAuthProvider extends GrokProvider {
132
124
  yield* super.stream(messages, tools, options);
133
125
  } catch (err) {
134
126
  if (err?.status !== 401 || !this.refreshToken) throw err;
135
- await this.refreshClient();
127
+ await this.refreshClient(options.signal);
136
128
  yield* super.stream(messages, tools, options);
137
129
  }
138
130
  }
@@ -1,5 +1,6 @@
1
1
  const OpenAI = require('openai');
2
2
  const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+ const { wrapProviderError } = require('./provider_error');
3
4
 
4
5
  const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
5
6
 
@@ -32,15 +33,15 @@ class NvidiaProvider extends OpenAICompatibleProvider {
32
33
  });
33
34
  }
34
35
 
35
- async listModels() {
36
+ async listModels(signal = null) {
36
37
  try {
37
- const res = await this.client.models.list();
38
+ const res = await this.client.models.list({ signal });
38
39
  const DROP = /embed|bge|e5-|rerank|guard|safety|moderat|diffus|flux|stable|imagen|vision-enc|whisper|tts|speech|paraphrase|classif/i;
39
40
  return res.data
40
41
  .filter((m) => !DROP.test(m.id))
41
42
  .map((m) => ({ id: m.id, name: m.id }));
42
43
  } catch (err) {
43
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
44
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', { signal });
44
45
  }
45
46
  }
46
47
 
@@ -76,9 +77,11 @@ class NvidiaProvider extends OpenAICompatibleProvider {
76
77
  const params = this._buildParams(model, messages, tools, options);
77
78
  let response;
78
79
  try {
79
- response = await this.client.chat.completions.create(params);
80
+ response = await this.client.chat.completions.create(params, { signal: options.signal });
80
81
  } catch (err) {
81
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
82
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', {
83
+ signal: options.signal,
84
+ });
82
85
  }
83
86
  return this.normalizeResponse(response);
84
87
  }
@@ -93,9 +96,11 @@ class NvidiaProvider extends OpenAICompatibleProvider {
93
96
 
94
97
  let stream;
95
98
  try {
96
- stream = await this.client.chat.completions.create(params);
99
+ stream = await this.client.chat.completions.create(params, { signal: options.signal });
97
100
  } catch (err) {
98
- throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
101
+ throw wrapProviderError(err, 'NVIDIA NIM request failed', {
102
+ signal: options.signal,
103
+ });
99
104
  }
100
105
 
101
106
  let toolCalls = [];