neoagent 2.4.1-beta.9 → 2.4.1

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 (234) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +36 -4
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -3
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +13 -0
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +2 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +197 -42
  52. package/flutter_app/lib/main_devices.dart +436 -4
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +673 -871
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +46 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
  70. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  71. package/flutter_app/lib/src/stream_renderer.dart +42 -3
  72. package/flutter_app/lib/src/theme/palette.dart +76 -34
  73. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  74. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  75. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  81. package/flutter_app/pubspec.lock +2 -2
  82. package/flutter_app/pubspec.yaml +7 -1
  83. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  84. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  85. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  94. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  95. package/flutter_app/web/favicon.png +0 -0
  96. package/flutter_app/web/favicon.svg +40 -9
  97. package/flutter_app/web/favicon_light.svg +43 -0
  98. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  99. package/flutter_app/web/icons/Icon-192.png +0 -0
  100. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-512.png +0 -0
  102. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  106. package/flutter_app/windows/runner/main.cpp +7 -1
  107. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  108. package/lib/manager.js +445 -83
  109. package/package.json +17 -3
  110. package/runtime/paths.js +3 -1
  111. package/server/admin/access.js +198 -0
  112. package/server/admin/admin.css +268 -0
  113. package/server/admin/admin.js +348 -0
  114. package/server/admin/analytics.js +128 -0
  115. package/server/admin/index.html +1015 -0
  116. package/server/admin/login.html +290 -0
  117. package/server/admin/logo.svg +43 -0
  118. package/server/admin/sql.js +134 -0
  119. package/server/admin/users.js +147 -0
  120. package/server/config/origins.js +3 -1
  121. package/server/db/database.js +92 -0
  122. package/server/db/ftsQuery.js +27 -0
  123. package/server/http/routes.js +1 -0
  124. package/server/http/static.js +23 -6
  125. package/server/index.js +1 -1
  126. package/server/middleware/adminAuth.js +48 -0
  127. package/server/middleware/auth.js +1 -40
  128. package/server/public/.last_build_id +1 -1
  129. package/server/public/app_icon.png +0 -0
  130. package/server/public/assets/AssetManifest.bin +1 -1
  131. package/server/public/assets/AssetManifest.bin.json +1 -1
  132. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  133. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  134. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  138. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  139. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  140. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  141. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  142. package/server/public/assets/web/icons/Icon-192.png +0 -0
  143. package/server/public/favicon.png +0 -0
  144. package/server/public/favicon.svg +40 -9
  145. package/server/public/favicon_light.svg +43 -0
  146. package/server/public/flutter_bootstrap.js +2 -2
  147. package/server/public/icons/Icon-192-light.png +0 -0
  148. package/server/public/icons/Icon-192.png +0 -0
  149. package/server/public/icons/Icon-512-light.png +0 -0
  150. package/server/public/icons/Icon-512.png +0 -0
  151. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  152. package/server/public/icons/Icon-maskable-192.png +0 -0
  153. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-512.png +0 -0
  155. package/server/public/main.dart.js +88537 -85936
  156. package/server/routes/admin.js +632 -0
  157. package/server/routes/agent_profiles.js +3 -0
  158. package/server/routes/agents.js +1 -1
  159. package/server/routes/auth.js +3 -1
  160. package/server/routes/mcp.js +29 -13
  161. package/server/routes/memory.js +23 -1
  162. package/server/routes/messaging.js +2 -0
  163. package/server/routes/screenHistory.js +14 -8
  164. package/server/routes/settings.js +10 -2
  165. package/server/routes/stream.js +1 -1
  166. package/server/routes/triggers.js +4 -4
  167. package/server/routes/voice_assistant.js +36 -1
  168. package/server/routes/workspace.js +86 -0
  169. package/server/services/account/admin_two_factor.js +132 -0
  170. package/server/services/account/password_policy.js +6 -1
  171. package/server/services/ai/completion.js +44 -0
  172. package/server/services/ai/engine.js +321 -500
  173. package/server/services/ai/imageAnalysis.js +9 -5
  174. package/server/services/ai/logFormat.js +46 -0
  175. package/server/services/ai/loopPolicy.js +11 -0
  176. package/server/services/ai/messagingFallback.js +228 -0
  177. package/server/services/ai/models.js +194 -55
  178. package/server/services/ai/providerRetry.js +169 -0
  179. package/server/services/ai/providers/anthropic.js +15 -4
  180. package/server/services/ai/providers/google.js +38 -11
  181. package/server/services/ai/providers/grok.js +19 -68
  182. package/server/services/ai/providers/grokOauth.js +141 -0
  183. package/server/services/ai/providers/nvidia.js +154 -0
  184. package/server/services/ai/providers/ollama.js +76 -39
  185. package/server/services/ai/providers/openai.js +20 -31
  186. package/server/services/ai/providers/openaiCodex.js +6 -2
  187. package/server/services/ai/providers/openaiCompatible.js +70 -0
  188. package/server/services/ai/providers/openrouter.js +162 -0
  189. package/server/services/ai/settings.js +30 -0
  190. package/server/services/ai/systemPrompt.js +51 -29
  191. package/server/services/ai/taskAnalysis.js +60 -1
  192. package/server/services/ai/toolEvidence.js +207 -0
  193. package/server/services/ai/toolSelector.js +8 -1
  194. package/server/services/ai/tools.js +78 -13
  195. package/server/services/browser/extension/registry.js +94 -9
  196. package/server/services/desktop/registry.js +104 -1
  197. package/server/services/desktop/screenRecorder.js +208 -98
  198. package/server/services/desktop/screen_recorder_support.js +46 -0
  199. package/server/services/integrations/google/provider.js +13 -0
  200. package/server/services/integrations/home_assistant/constants.js +88 -0
  201. package/server/services/integrations/home_assistant/network.js +207 -0
  202. package/server/services/integrations/home_assistant/provider.js +249 -0
  203. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  204. package/server/services/integrations/home_assistant/tools.js +101 -0
  205. package/server/services/integrations/manager.js +5 -0
  206. package/server/services/integrations/registry.js +2 -0
  207. package/server/services/integrations/trello/provider.js +8 -2
  208. package/server/services/manager.js +51 -53
  209. package/server/services/mcp/client.js +208 -268
  210. package/server/services/mcp/client_support.js +172 -0
  211. package/server/services/mcp/recovery.js +116 -0
  212. package/server/services/mcp/tool_operations.js +123 -0
  213. package/server/services/memory/ingestion.js +185 -370
  214. package/server/services/memory/ingestion_coverage.js +129 -0
  215. package/server/services/memory/ingestion_documents.js +123 -0
  216. package/server/services/memory/ingestion_support.js +171 -0
  217. package/server/services/memory/intelligence.js +181 -0
  218. package/server/services/memory/manager.js +476 -32
  219. package/server/services/messaging/automation.js +71 -134
  220. package/server/services/messaging/formatting_guides.js +26 -1
  221. package/server/services/messaging/inbound_queue.js +111 -0
  222. package/server/services/messaging/manager.js +25 -8
  223. package/server/services/messaging/typing_keepalive.js +110 -0
  224. package/server/services/streaming/android-stream.js +293 -40
  225. package/server/services/tasks/integration_runtime.js +4 -1
  226. package/server/services/tasks/runtime.js +415 -56
  227. package/server/services/tasks/task_repository.js +48 -6
  228. package/server/services/websocket.js +8 -2
  229. package/server/services/widgets/service.js +17 -1
  230. package/server/services/workspace/manager.js +116 -0
  231. package/server/utils/logger.js +44 -6
  232. package/server/utils/security.js +3 -0
  233. package/server/utils/version.js +29 -19
  234. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -0,0 +1,154 @@
1
+ const OpenAI = require('openai');
2
+ const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+
4
+ const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
5
+
6
+ // Context windows per model (tokens)
7
+ const CONTEXT_WINDOWS = {
8
+ 'nvidia/nemotron-3-super-120b-a12b': 262144,
9
+ 'moonshotai/kimi-k2.5': 262144,
10
+ 'minimaxai/minimax-m2.5': 196608,
11
+ 'z-ai/glm5': 202752,
12
+ 'meta/llama-4-maverick-17b-128e-instruct': 1048576,
13
+ 'meta/llama-4-scout-17b-16e-instruct': 1048576,
14
+ 'deepseek-ai/deepseek-r1-0528': 163840,
15
+ 'qwen/qwq-32b': 131072,
16
+ };
17
+
18
+ // Reasoning models: no temperature, no top_p
19
+ const REASONING_MODELS = new Set([
20
+ 'deepseek-ai/deepseek-r1-0528',
21
+ 'qwen/qwq-32b',
22
+ ]);
23
+
24
+ class NvidiaProvider extends OpenAICompatibleProvider {
25
+ constructor(config = {}) {
26
+ super(config);
27
+ this.name = 'nvidia';
28
+ this.models = Object.keys(CONTEXT_WINDOWS);
29
+ this.client = new OpenAI({
30
+ apiKey: config.apiKey || process.env.NVIDIA_API_KEY,
31
+ baseURL: config.baseUrl || NVIDIA_BASE_URL,
32
+ });
33
+ }
34
+
35
+ async listModels() {
36
+ try {
37
+ const res = await this.client.models.list();
38
+ const DROP = /embed|bge|e5-|rerank|guard|safety|moderat|diffus|flux|stable|imagen|vision-enc|whisper|tts|speech|paraphrase|classif/i;
39
+ return res.data
40
+ .filter((m) => !DROP.test(m.id))
41
+ .map((m) => ({ id: m.id, name: m.id }));
42
+ } catch (err) {
43
+ throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
44
+ }
45
+ }
46
+
47
+ getContextWindow(model) {
48
+ return CONTEXT_WINDOWS[model] ?? 131072;
49
+ }
50
+
51
+ _isReasoningModel(model) {
52
+ return REASONING_MODELS.has(model);
53
+ }
54
+
55
+ _buildParams(model, messages, tools, options) {
56
+ const params = {
57
+ model,
58
+ messages,
59
+ max_tokens: options.maxTokens || 8192,
60
+ };
61
+
62
+ if (!this._isReasoningModel(model)) {
63
+ params.temperature = options.temperature ?? 0.6;
64
+ }
65
+
66
+ if (tools && tools.length > 0) {
67
+ params.tools = this.formatTools(tools);
68
+ params.tool_choice = 'auto';
69
+ }
70
+
71
+ return params;
72
+ }
73
+
74
+ async chat(messages, tools = [], options = {}) {
75
+ const model = options.model || this.getDefaultModel();
76
+ const params = this._buildParams(model, messages, tools, options);
77
+ let response;
78
+ try {
79
+ response = await this.client.chat.completions.create(params);
80
+ } catch (err) {
81
+ throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
82
+ }
83
+ return this.normalizeResponse(response);
84
+ }
85
+
86
+ async *stream(messages, tools = [], options = {}) {
87
+ const model = options.model || this.getDefaultModel();
88
+ const params = {
89
+ ...this._buildParams(model, messages, tools, options),
90
+ stream: true,
91
+ stream_options: { include_usage: true },
92
+ };
93
+
94
+ let stream;
95
+ try {
96
+ stream = await this.client.chat.completions.create(params);
97
+ } catch (err) {
98
+ throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
99
+ }
100
+
101
+ let toolCalls = [];
102
+ let content = '';
103
+ let finalUsage = null;
104
+
105
+ for await (const chunk of stream) {
106
+ if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
107
+ finalUsage = this.normalizeUsage(chunk.usage);
108
+ continue;
109
+ }
110
+
111
+ const delta = chunk.choices?.[0]?.delta;
112
+ if (!delta) continue;
113
+
114
+ if (delta.content) {
115
+ content += delta.content;
116
+ yield { type: 'content', content: delta.content };
117
+ }
118
+
119
+ if (delta.tool_calls) {
120
+ for (const tc of delta.tool_calls) {
121
+ if (!toolCalls[tc.index]) {
122
+ toolCalls[tc.index] = {
123
+ id: tc.id || '',
124
+ type: 'function',
125
+ function: { name: tc.function?.name || '', arguments: '' },
126
+ };
127
+ }
128
+ if (tc.id) toolCalls[tc.index].id = tc.id;
129
+ if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
130
+ if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
131
+ }
132
+ }
133
+
134
+ const finishReason = chunk.choices[0]?.finish_reason;
135
+ if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
136
+ yield { type: 'tool_calls', toolCalls, content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
137
+ return;
138
+ }
139
+ if (finishReason === 'stop') {
140
+ yield { type: 'done', content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
141
+ return;
142
+ }
143
+ }
144
+
145
+ if (toolCalls.length > 0) {
146
+ yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };
147
+ } else {
148
+ yield { type: 'done', content, usage: finalUsage };
149
+ }
150
+ }
151
+
152
+ }
153
+
154
+ module.exports = { NvidiaProvider };
@@ -81,33 +81,78 @@ class OllamaProvider extends BaseProvider {
81
81
  }));
82
82
  }
83
83
 
84
- async chat(messages, tools = [], options = {}) {
85
- const model = options.model || this.config.model || 'llama3.1';
86
- await this.ensureModel(model);
84
+ buildChatBody(messages, tools, options, stream) {
87
85
  const body = {
88
- model,
89
- messages: messages.map(m => ({
90
- role: m.role,
91
- content: m.content || '',
92
- ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
93
- ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
94
- })),
95
- stream: false,
86
+ model: options.model || this.config.model || 'llama3.1',
87
+ messages: messages.map(m => {
88
+ const msg = {
89
+ role: m.role,
90
+ content: m.content || '',
91
+ ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
92
+ };
93
+ if (m.tool_calls) {
94
+ msg.tool_calls = m.tool_calls.map(tc => ({
95
+ ...tc,
96
+ function: {
97
+ ...tc.function,
98
+ arguments: typeof tc.function.arguments === 'string'
99
+ ? (function() { try { return JSON.parse(tc.function.arguments || '{}'); } catch(e) { return {}; } })()
100
+ : tc.function.arguments
101
+ }
102
+ }));
103
+ }
104
+ return msg;
105
+ }),
106
+ stream,
96
107
  options: {
97
108
  temperature: options.temperature ?? 0.7,
98
109
  num_predict: options.maxTokens || 16384
99
110
  }
100
111
  };
101
-
102
112
  if (tools.length > 0) {
103
113
  body.tools = this.formatToolsForOllama(tools);
104
114
  }
115
+ return body;
116
+ }
105
117
 
118
+ // Ollama returns HTTP 200 with an error body for some failures and a non-2xx
119
+ // status for others; surface both as real errors instead of letting callers
120
+ // see a silently empty response. Tags models that reject tools so the caller
121
+ // can transparently retry without them.
122
+ async postChat(body) {
106
123
  const res = await fetch(`${this.baseUrl}/api/chat`, {
107
124
  method: 'POST',
108
125
  headers: { 'Content-Type': 'application/json' },
109
126
  body: JSON.stringify(body)
110
127
  });
128
+ if (!res.ok) {
129
+ const detail = await res.text().catch(() => '');
130
+ let message = detail;
131
+ try { message = JSON.parse(detail)?.error || detail; } catch {}
132
+ const err = new Error(`Ollama /api/chat failed (HTTP ${res.status}): ${message || res.statusText}`);
133
+ if (/does not support tools|tools.*not supported/i.test(message)) {
134
+ err.code = 'OLLAMA_TOOLS_UNSUPPORTED';
135
+ }
136
+ throw err;
137
+ }
138
+ return res;
139
+ }
140
+
141
+ async chat(messages, tools = [], options = {}) {
142
+ const model = options.model || this.config.model || 'llama3.1';
143
+ await this.ensureModel(model);
144
+
145
+ let res;
146
+ try {
147
+ res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, false));
148
+ } catch (err) {
149
+ if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
150
+ console.warn(`[Ollama] Model '${model}' does not support tools; retrying without them.`);
151
+ res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, false));
152
+ } else {
153
+ throw err;
154
+ }
155
+ }
111
156
 
112
157
  const data = await res.json();
113
158
  const msg = data.message || {};
@@ -135,35 +180,24 @@ class OllamaProvider extends BaseProvider {
135
180
  async *stream(messages, tools = [], options = {}) {
136
181
  const model = options.model || this.config.model || 'llama3.1';
137
182
  await this.ensureModel(model);
138
- const body = {
139
- model,
140
- messages: messages.map(m => ({
141
- role: m.role,
142
- content: m.content || '',
143
- ...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
144
- ...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
145
- })),
146
- stream: true,
147
- options: {
148
- temperature: options.temperature ?? 0.7,
149
- num_predict: options.maxTokens || 16384
150
- }
151
- };
152
183
 
153
- if (tools.length > 0) {
154
- body.tools = this.formatToolsForOllama(tools);
184
+ let res;
185
+ try {
186
+ res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, true));
187
+ } catch (err) {
188
+ if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
189
+ console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
190
+ res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, true));
191
+ } else {
192
+ throw err;
193
+ }
155
194
  }
156
195
 
157
- const res = await fetch(`${this.baseUrl}/api/chat`, {
158
- method: 'POST',
159
- headers: { 'Content-Type': 'application/json' },
160
- body: JSON.stringify(body)
161
- });
162
-
163
196
  const reader = res.body.getReader();
164
197
  const decoder = new TextDecoder();
165
198
  let content = '';
166
199
  let buffer = '';
200
+ let accumulatedToolCalls = [];
167
201
 
168
202
  while (true) {
169
203
  const { done, value } = await reader.read();
@@ -181,20 +215,23 @@ class OllamaProvider extends BaseProvider {
181
215
  content += data.message.content;
182
216
  yield { type: 'content', content: data.message.content };
183
217
  }
184
- if (data.done) {
185
- const toolCalls = (data.message?.tool_calls || []).map((tc, i) => ({
218
+ if (data.message?.tool_calls && Array.isArray(data.message.tool_calls)) {
219
+ const mapped = data.message.tool_calls.map((tc, i) => ({
186
220
  id: `call_ollama_${Date.now()}_${i}`,
187
221
  type: 'function',
188
222
  function: {
189
223
  name: tc.function.name,
190
- arguments: JSON.stringify(tc.function.arguments || {})
224
+ arguments: typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments || {})
191
225
  }
192
226
  }));
227
+ accumulatedToolCalls = accumulatedToolCalls.concat(mapped);
228
+ }
229
+ if (data.done) {
193
230
  yield {
194
231
  type: 'done',
195
232
  content,
196
- toolCalls,
197
- finishReason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
233
+ toolCalls: accumulatedToolCalls,
234
+ finishReason: accumulatedToolCalls.length > 0 ? 'tool_calls' : 'stop',
198
235
  usage: data.prompt_eval_count ? {
199
236
  promptTokens: data.prompt_eval_count || 0,
200
237
  completionTokens: data.eval_count || 0,
@@ -1,29 +1,31 @@
1
1
  const OpenAI = require('openai');
2
- const { BaseProvider } = require('./base');
2
+ const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
3
 
4
- class OpenAIProvider extends BaseProvider {
4
+ class OpenAIProvider extends OpenAICompatibleProvider {
5
5
  constructor(config = {}) {
6
6
  super(config);
7
7
  this.name = 'openai';
8
8
  this.models = [
9
+ 'gpt-5.5',
9
10
  'gpt-5',
10
11
  'gpt-5-mini',
11
12
  'gpt-5-nano',
12
13
  'gpt-5.2',
13
- 'gpt-4.1',
14
14
  'o3',
15
+ 'o3-pro',
15
16
  'o4-mini'
16
17
  ];
17
18
  // Reasoning models: no temperature, use max_completion_tokens, support reasoning_effort
18
- this.reasoningModels = new Set(['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5.2', 'gpt-5.1', 'o1', 'o3', 'o4-mini', 'o3-mini']);
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']);
19
20
  this.contextWindows = {
21
+ 'gpt-5.5': 1000000,
20
22
  'gpt-5': 400000,
21
- 'gpt-5-mini': 200000,
23
+ 'gpt-5-mini': 400000,
22
24
  'gpt-5-nano': 128000,
23
25
  'gpt-5.2': 400000,
24
26
  'gpt-5.1': 400000,
25
- 'gpt-4.1': 1047576,
26
27
  'o3': 200000,
28
+ 'o3-pro': 200000,
27
29
  'o4-mini': 200000,
28
30
  'o3-mini': 200000
29
31
  };
@@ -34,6 +36,18 @@ class OpenAIProvider extends BaseProvider {
34
36
  });
35
37
  }
36
38
 
39
+ async listModels() {
40
+ try {
41
+ const res = await this.client.models.list();
42
+ const DROP = /dall-e|whisper|tts|embed|moderat|realtime|audio|transcribe|search-api|-image-|babbage|davinci-002|^sora|-instruct/i;
43
+ return res.data
44
+ .filter((m) => !DROP.test(m.id))
45
+ .map((m) => ({ id: m.id, name: m.id }));
46
+ } catch (err) {
47
+ throw new Error(`Failed to list OpenAI models: ${err.message || String(err)}`);
48
+ }
49
+ }
50
+
37
51
  isReasoningModel(model) {
38
52
  // Match exact IDs and prefix variants (gpt-5-2025-08-07 etc)
39
53
  for (const id of this.reasoningModels) {
@@ -173,31 +187,6 @@ class OpenAIProvider extends BaseProvider {
173
187
  }
174
188
  }
175
189
 
176
- async analyzeImage(options = {}) {
177
- const model = options.model || this.getDefaultVisionModel();
178
- const b64 = BaseProvider.readImageAsBase64(options.imagePath);
179
- const response = await this.client.chat.completions.create({
180
- model,
181
- max_tokens: options.maxTokens || 4096,
182
- messages: [{
183
- role: 'user',
184
- content: [
185
- { type: 'text', text: options.question || 'Describe this image in detail.' },
186
- {
187
- type: 'image_url',
188
- image_url: {
189
- url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`
190
- }
191
- }
192
- ]
193
- }]
194
- });
195
-
196
- return {
197
- content: response.choices[0]?.message?.content || '',
198
- model: response.model || model,
199
- };
200
- }
201
190
  }
202
191
 
203
192
  module.exports = { OpenAIProvider };
@@ -340,8 +340,12 @@ class OpenAICodexProvider extends BaseProvider {
340
340
  request.tool_choice = 'auto';
341
341
  }
342
342
  request.parallel_tool_calls = false;
343
- // reasoning: both effort and summary required for Codex reasoning models
344
- if (this._isReasoningModel(model)) {
343
+ // Reasoning parameters are only sent when there are no tools: on the Codex
344
+ // backend, combining reasoning mode with function-calling causes the model to
345
+ // produce only internal reasoning output (no function_calls, no text), which
346
+ // makes the engine see steps=0 and finalResponse=no on every run.
347
+ const hasTools = tools && tools.length > 0;
348
+ if (this._isReasoningModel(model) && !hasTools) {
345
349
  const effort = options.reasoningEffort || options.reasoning_effort || 'medium';
346
350
  request.reasoning = { effort, summary: 'auto' };
347
351
  request.include = ['reasoning.encrypted_content'];
@@ -0,0 +1,70 @@
1
+ const { BaseProvider } = require('./base');
2
+
3
+ // Shared base for providers that speak the OpenAI Chat Completions wire format
4
+ // (OpenAI, Grok, NVIDIA NIM, GitHub Copilot, ...). It owns the response/usage
5
+ // normalization and vision request that were previously copy-pasted into each
6
+ // provider. Per-provider concerns — client construction, model lists, context
7
+ // windows, reasoning detection, and the streaming loop — stay in the subclasses,
8
+ // since those genuinely differ between vendors.
9
+ class OpenAICompatibleProvider extends BaseProvider {
10
+ normalizeUsage(usage) {
11
+ if (!usage) return null;
12
+ return {
13
+ promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
14
+ completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
15
+ totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0,
16
+ };
17
+ }
18
+
19
+ normalizeResponse(response) {
20
+ const choice = response?.choices?.[0];
21
+ if (!choice) {
22
+ throw new Error(`Provider '${this.name}' returned no choices in the response`);
23
+ }
24
+ const msg = choice.message || {};
25
+ return {
26
+ content: msg.content || '',
27
+ toolCalls: (msg.tool_calls || [])
28
+ .filter((tc) => tc?.function)
29
+ .map((tc) => ({
30
+ id: tc.id,
31
+ type: 'function',
32
+ function: { name: tc.function.name, arguments: tc.function.arguments },
33
+ })),
34
+ finishReason: choice.finish_reason,
35
+ usage: this.normalizeUsage(response.usage),
36
+ };
37
+ }
38
+
39
+ async analyzeImage(options = {}) {
40
+ if (!this.supportsVision()) {
41
+ throw new Error(`Provider '${this.name}' does not support image analysis`);
42
+ }
43
+
44
+ const model = options.model || this.getDefaultVisionModel();
45
+ const b64 = BaseProvider.readImageAsBase64(options.imagePath);
46
+ const response = await this.client.chat.completions.create({
47
+ model,
48
+ max_tokens: options.maxTokens || 4096,
49
+ messages: [{
50
+ role: 'user',
51
+ content: [
52
+ { type: 'text', text: options.question || 'Describe this image in detail.' },
53
+ {
54
+ type: 'image_url',
55
+ image_url: {
56
+ url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`,
57
+ },
58
+ },
59
+ ],
60
+ }],
61
+ });
62
+
63
+ return {
64
+ content: response.choices[0]?.message?.content || '',
65
+ model: response.model || model,
66
+ };
67
+ }
68
+ }
69
+
70
+ module.exports = { OpenAICompatibleProvider };
@@ -0,0 +1,162 @@
1
+ const OpenAI = require('openai');
2
+ const { OpenAICompatibleProvider } = require('./openaiCompatible');
3
+
4
+ const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
5
+
6
+ // Context windows fetched from the API are cached here so getContextWindow
7
+ // can serve them without a network call at inference time.
8
+ const contextWindowCache = new Map();
9
+
10
+ class OpenRouterProvider extends OpenAICompatibleProvider {
11
+ constructor(config = {}) {
12
+ super(config);
13
+ this.name = 'openrouter';
14
+ this.models = [];
15
+ this.baseURL = config.baseUrl || OPENROUTER_BASE_URL;
16
+ this.client = new OpenAI({
17
+ apiKey: config.apiKey || process.env.OPENROUTER_API_KEY,
18
+ baseURL: this.baseURL,
19
+ timeout: 90_000, // 90 s — free models can queue; avoids hanging forever
20
+ maxRetries: 0, // engine handles retries; avoid SDK silently re-queuing slow models
21
+ defaultHeaders: {
22
+ 'HTTP-Referer': 'https://github.com/NeoLabs-Systems/NeoAgent',
23
+ 'X-Title': 'NeoAgent',
24
+ },
25
+ });
26
+ }
27
+
28
+ async listModels() {
29
+ const res = await fetch(`${this.baseURL}/models`, {
30
+ headers: { 'Authorization': `Bearer ${this.client.apiKey}` },
31
+ });
32
+ if (!res.ok) throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
33
+ const { data } = await res.json();
34
+ const models = data || [];
35
+ for (const m of models) {
36
+ if (m.context_length) contextWindowCache.set(m.id, m.context_length);
37
+ }
38
+ this.models = models.map((m) => m.id);
39
+ return models;
40
+ }
41
+
42
+ getContextWindow(model) {
43
+ return contextWindowCache.get(model) ?? 128000;
44
+ }
45
+
46
+ _buildParams(model, messages, tools, options) {
47
+ const params = {
48
+ model,
49
+ messages,
50
+ temperature: options.temperature ?? 0.7,
51
+ max_tokens: options.maxTokens || 16384,
52
+ };
53
+
54
+ if (tools && tools.length > 0) {
55
+ params.tools = this.formatTools(tools);
56
+ params.tool_choice = 'auto';
57
+ }
58
+
59
+ return params;
60
+ }
61
+
62
+ _extractOpenRouterError(obj) {
63
+ if (!obj) return null;
64
+ const e = obj.error;
65
+ if (!e) return null;
66
+ const msg = e.message || (typeof e === 'string' ? e : JSON.stringify(e));
67
+ return `OpenRouter: ${msg}${e.code ? ` (code ${e.code})` : ''}`;
68
+ }
69
+
70
+ async chat(messages, tools = [], options = {}) {
71
+ const model = options.model || this.getDefaultModel();
72
+ const params = this._buildParams(model, messages, tools, options);
73
+ let response;
74
+ try {
75
+ response = await this.client.chat.completions.create(params);
76
+ } catch (err) {
77
+ throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
78
+ }
79
+ // OpenRouter returns HTTP 200 even for errors (rate limits, model unavailable, etc.)
80
+ const orErr = this._extractOpenRouterError(response);
81
+ if (orErr) throw new Error(orErr);
82
+ if (!response?.choices?.length) {
83
+ throw new Error(`OpenRouter: model returned no choices (may be rate-limited or unavailable)`);
84
+ }
85
+ return this.normalizeResponse(response);
86
+ }
87
+
88
+ async *stream(messages, tools = [], options = {}) {
89
+ const model = options.model || this.getDefaultModel();
90
+ const params = {
91
+ ...this._buildParams(model, messages, tools, options),
92
+ stream: true,
93
+ stream_options: { include_usage: true },
94
+ };
95
+
96
+ let stream;
97
+ try {
98
+ stream = await this.client.chat.completions.create(params);
99
+ } catch (err) {
100
+ throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
101
+ }
102
+
103
+ let toolCalls = [];
104
+ let content = '';
105
+ let finalUsage = null;
106
+
107
+ // The OpenAI SDK converts OpenRouter's SSE error events (data.error) into
108
+ // APIErrors before yielding — so we wrap the loop to add context.
109
+ try {
110
+ for await (const chunk of stream) {
111
+ if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
112
+ finalUsage = this.normalizeUsage(chunk.usage);
113
+ continue;
114
+ }
115
+
116
+ const delta = chunk.choices?.[0]?.delta;
117
+ if (!delta) continue;
118
+
119
+ if (delta.content) {
120
+ content += delta.content;
121
+ yield { type: 'content', content: delta.content };
122
+ }
123
+
124
+ if (delta.tool_calls) {
125
+ for (const tc of delta.tool_calls) {
126
+ if (!toolCalls[tc.index]) {
127
+ toolCalls[tc.index] = {
128
+ id: tc.id || '',
129
+ type: 'function',
130
+ function: { name: tc.function?.name || '', arguments: '' },
131
+ };
132
+ }
133
+ if (tc.id) toolCalls[tc.index].id = tc.id;
134
+ if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
135
+ if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
136
+ }
137
+ }
138
+
139
+ const finishReason = chunk.choices[0]?.finish_reason;
140
+ if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
141
+ yield { type: 'tool_calls', toolCalls, content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
142
+ return;
143
+ }
144
+ if (finishReason === 'stop') {
145
+ yield { type: 'done', content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
146
+ return;
147
+ }
148
+ }
149
+ } catch (err) {
150
+ // Re-throw SDK APIErrors (from OpenRouter SSE error events) with OpenRouter context.
151
+ throw new Error(`OpenRouter: ${err?.message || String(err)}`);
152
+ }
153
+
154
+ if (toolCalls.length > 0) {
155
+ yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };
156
+ } else {
157
+ yield { type: 'done', content, usage: finalUsage };
158
+ }
159
+ }
160
+ }
161
+
162
+ module.exports = { OpenRouterProvider };