neuro-cli 4.1.0

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/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,751 @@
1
+ // ============================================================
2
+ // NeuroCLI - Ollama (Local Model) Provider
3
+ // Supports Ollama API + OpenAI-compatible endpoints
4
+ // Streaming, Tool Use, Embeddings, Model Management
5
+ // ============================================================
6
+ export const DEFAULT_OLLAMA_CONFIG = {
7
+ baseUrl: 'http://localhost:11434',
8
+ defaultModel: 'llama3',
9
+ timeout: 120000,
10
+ maxRetries: 1,
11
+ };
12
+ // ----------------------------------------------------------------
13
+ // Error Classes
14
+ // ----------------------------------------------------------------
15
+ export class OllamaProviderError extends Error {
16
+ statusCode;
17
+ body;
18
+ constructor(message, statusCode, body) {
19
+ super(message);
20
+ this.statusCode = statusCode;
21
+ this.body = body;
22
+ this.name = 'OllamaProviderError';
23
+ }
24
+ }
25
+ export class OllamaTimeoutError extends OllamaProviderError {
26
+ constructor(timeout) {
27
+ super(`Request timed out after ${timeout}ms`);
28
+ this.name = 'OllamaTimeoutError';
29
+ }
30
+ }
31
+ export class OllamaConnectionError extends OllamaProviderError {
32
+ constructor(baseUrl, cause) {
33
+ super(`Cannot connect to Ollama at ${baseUrl}: ${cause instanceof Error ? cause.message : String(cause)}`);
34
+ this.name = 'OllamaConnectionError';
35
+ }
36
+ }
37
+ // ----------------------------------------------------------------
38
+ // OllamaProvider
39
+ // ----------------------------------------------------------------
40
+ export class OllamaProvider {
41
+ config;
42
+ mode = 'ollama';
43
+ constructor(config) {
44
+ this.config = { ...DEFAULT_OLLAMA_CONFIG, ...config };
45
+ this.detectMode();
46
+ }
47
+ // ==============================================================
48
+ // Public API
49
+ // ==============================================================
50
+ /**
51
+ * Chat completion with optional streaming.
52
+ *
53
+ * When callbacks are provided and stream is not explicitly false,
54
+ * the response is streamed token-by-token via the callbacks.
55
+ */
56
+ async chat(messages, options, callbacks) {
57
+ const model = options?.model ?? this.config.defaultModel;
58
+ const shouldStream = options?.stream !== false;
59
+ if (this.mode === 'ollama') {
60
+ return this.chatOllama(model, messages, options, shouldStream, callbacks);
61
+ }
62
+ return this.chatOpenAICompatible(model, messages, options, shouldStream, callbacks);
63
+ }
64
+ /**
65
+ * List locally available models.
66
+ */
67
+ async listModels() {
68
+ if (this.mode === 'ollama') {
69
+ return this.listModelsOllama();
70
+ }
71
+ return this.listModelsOpenAICompatible();
72
+ }
73
+ /**
74
+ * Health check -- returns true if the endpoint is reachable.
75
+ */
76
+ async isAvailable() {
77
+ try {
78
+ if (this.mode === 'ollama') {
79
+ const response = await this.fetch(`${this.config.baseUrl}`, {
80
+ method: 'GET',
81
+ signal: AbortSignal.timeout(5000),
82
+ });
83
+ return response.ok;
84
+ }
85
+ // OpenAI-compatible: try /v1/models
86
+ const response = await this.fetch(`${this.config.baseUrl}/v1/models`, {
87
+ method: 'GET',
88
+ signal: AbortSignal.timeout(5000),
89
+ });
90
+ return response.ok;
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }
96
+ /**
97
+ * Generate embeddings for a prompt using the Ollama native API.
98
+ * Not supported on OpenAI-compatible endpoints through this method.
99
+ */
100
+ async generateEmbeddings(prompt, model) {
101
+ if (this.mode !== 'ollama') {
102
+ throw new OllamaProviderError('Embeddings are only supported when using the native Ollama API. ' +
103
+ 'Switch baseUrl to an Ollama endpoint to use embeddings.');
104
+ }
105
+ const body = {
106
+ model: model ?? this.config.defaultModel,
107
+ prompt,
108
+ };
109
+ const data = await this.request('/api/embeddings', 'POST', body);
110
+ return data.embedding;
111
+ }
112
+ /**
113
+ * Pull a model from the Ollama registry.
114
+ * Returns true on success.
115
+ */
116
+ async pullModel(name) {
117
+ if (this.mode !== 'ollama') {
118
+ throw new OllamaProviderError('Model pulling is only supported when using the native Ollama API.');
119
+ }
120
+ try {
121
+ const response = await this.fetch(`${this.config.baseUrl}/api/pull`, {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ name, stream: false }),
125
+ signal: AbortSignal.timeout(Math.max(this.config.timeout, 600000)), // 10 min floor for pulls
126
+ });
127
+ if (!response.ok) {
128
+ const body = await response.text();
129
+ throw new OllamaProviderError(`Failed to pull model "${name}": ${response.status} ${body}`, response.status, body);
130
+ }
131
+ return true;
132
+ }
133
+ catch (error) {
134
+ if (error instanceof OllamaProviderError)
135
+ throw error;
136
+ throw new OllamaConnectionError(this.config.baseUrl, error);
137
+ }
138
+ }
139
+ /**
140
+ * Estimate token count for a text string.
141
+ *
142
+ * Uses a heuristic based on the model family. Ollama does not expose
143
+ * a tokenization endpoint, so this is an approximation:
144
+ * - For most models: ~4 characters per token (GPT-style)
145
+ * - For CJK-heavy text: ~2 characters per token
146
+ * - A slight overhead is added for special tokens / formatting.
147
+ */
148
+ estimateTokens(text) {
149
+ if (!text)
150
+ return 0;
151
+ // Detect proportion of CJK characters
152
+ const cjkPattern = /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g;
153
+ const cjkMatches = text.match(cjkPattern);
154
+ const cjkRatio = cjkMatches ? cjkMatches.length / text.length : 0;
155
+ // Blend between the two heuristics
156
+ const charsPerToken = 4 - 2 * cjkRatio; // 4 for pure ASCII, 2 for pure CJK
157
+ const baseEstimate = Math.ceil(text.length / charsPerToken);
158
+ // Add ~5% overhead for special tokens, formatting, etc.
159
+ return Math.ceil(baseEstimate * 1.05);
160
+ }
161
+ /**
162
+ * Get the current configuration (read-only copy).
163
+ */
164
+ getConfig() {
165
+ return { ...this.config };
166
+ }
167
+ /**
168
+ * Update configuration. Merges with existing config.
169
+ * Re-detects provider mode if baseUrl changes.
170
+ */
171
+ updateConfig(updates) {
172
+ const oldBaseUrl = this.config.baseUrl;
173
+ this.config = { ...this.config, ...updates };
174
+ if (updates.baseUrl && updates.baseUrl !== oldBaseUrl) {
175
+ this.detectMode();
176
+ }
177
+ }
178
+ // ==============================================================
179
+ // Ollama Native Chat
180
+ // ==============================================================
181
+ async chatOllama(model, messages, options, shouldStream, callbacks) {
182
+ const ollamaMessages = this.convertMessages(messages);
183
+ const body = {
184
+ model,
185
+ messages: ollamaMessages,
186
+ stream: shouldStream,
187
+ options: {
188
+ temperature: options?.temperature ?? this.config.temperature,
189
+ num_predict: options?.maxTokens ?? this.config.maxTokens,
190
+ },
191
+ };
192
+ // Add tools if provided and supported
193
+ if (options?.tools && options.tools.length > 0) {
194
+ body.tools = this.convertTools(options.tools);
195
+ }
196
+ let lastError = null;
197
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
198
+ if (attempt > 0) {
199
+ const waitTime = Math.min(2000 * Math.pow(2, attempt - 1), 10000);
200
+ await this.sleep(waitTime);
201
+ }
202
+ try {
203
+ if (shouldStream && callbacks) {
204
+ return await this.streamOllamaChat(body, callbacks);
205
+ }
206
+ const data = await this.request('/api/chat', 'POST', body);
207
+ return this.normalizeOllamaResponse(data, callbacks);
208
+ }
209
+ catch (error) {
210
+ lastError = error;
211
+ if (!this.isRetryable(error))
212
+ break;
213
+ }
214
+ }
215
+ throw lastError ?? new OllamaProviderError('Max retries exceeded');
216
+ }
217
+ /**
218
+ * Stream an Ollama native /api/chat response (NDJSON).
219
+ */
220
+ async streamOllamaChat(body, callbacks) {
221
+ const response = await this.fetch(`${this.config.baseUrl}/api/chat`, {
222
+ method: 'POST',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify(body),
225
+ signal: AbortSignal.timeout(this.config.timeout),
226
+ });
227
+ if (!response.ok) {
228
+ const text = await response.text();
229
+ throw new OllamaProviderError(`Ollama chat error (${response.status}): ${text}`, response.status, text);
230
+ }
231
+ const reader = response.body?.getReader();
232
+ if (!reader)
233
+ throw new OllamaProviderError('No response body for streaming');
234
+ const decoder = new TextDecoder();
235
+ let fullContent = '';
236
+ let model = '';
237
+ let totalDuration = 0;
238
+ let evalCount = 0;
239
+ let evalDuration = 0;
240
+ const toolCalls = [];
241
+ let buffer = '';
242
+ try {
243
+ while (true) {
244
+ const { done, value } = await reader.read();
245
+ if (done)
246
+ break;
247
+ buffer += decoder.decode(value, { stream: true });
248
+ // NDJSON: each line is a JSON object
249
+ const lines = buffer.split('\n');
250
+ buffer = lines.pop() ?? ''; // keep incomplete line in buffer
251
+ for (const line of lines) {
252
+ const trimmed = line.trim();
253
+ if (!trimmed)
254
+ continue;
255
+ try {
256
+ const chunk = JSON.parse(trimmed);
257
+ // Accumulate metadata from all chunks
258
+ model = chunk.model || model;
259
+ // Stream content tokens
260
+ if (chunk.message?.content) {
261
+ fullContent += chunk.message.content;
262
+ callbacks.onToken?.(chunk.message.content);
263
+ }
264
+ // Handle tool calls
265
+ if (chunk.message?.tool_calls) {
266
+ for (const tc of chunk.message.tool_calls) {
267
+ const toolCall = {
268
+ id: `tc_${toolCalls.length}`,
269
+ type: 'function',
270
+ function: {
271
+ name: tc.function.name,
272
+ arguments: typeof tc.function.arguments === 'string'
273
+ ? tc.function.arguments
274
+ : JSON.stringify(tc.function.arguments),
275
+ },
276
+ };
277
+ toolCalls.push(toolCall);
278
+ callbacks.onToolCall?.(toolCall);
279
+ }
280
+ }
281
+ // Final chunk has stats
282
+ if (chunk.done) {
283
+ totalDuration = chunk.total_duration ?? 0;
284
+ evalCount = chunk.eval_count ?? 0;
285
+ evalDuration = chunk.eval_duration ?? 0;
286
+ }
287
+ }
288
+ catch {
289
+ // Skip malformed NDJSON lines
290
+ }
291
+ }
292
+ }
293
+ // Process any remaining buffer
294
+ if (buffer.trim()) {
295
+ try {
296
+ const chunk = JSON.parse(buffer.trim());
297
+ if (chunk.message?.content) {
298
+ fullContent += chunk.message.content;
299
+ callbacks.onToken?.(chunk.message.content);
300
+ }
301
+ if (chunk.done) {
302
+ totalDuration = chunk.total_duration ?? 0;
303
+ evalCount = chunk.eval_count ?? 0;
304
+ evalDuration = chunk.eval_duration ?? 0;
305
+ }
306
+ }
307
+ catch {
308
+ // Ignore trailing malformed data
309
+ }
310
+ }
311
+ }
312
+ catch (error) {
313
+ callbacks.onError?.(error);
314
+ throw error;
315
+ }
316
+ // Append tool calls information to content if any
317
+ let finalContent = fullContent;
318
+ if (toolCalls.length > 0 && !fullContent) {
319
+ finalContent = JSON.stringify(toolCalls.map(tc => ({
320
+ name: tc.function.name,
321
+ arguments: tc.function.arguments,
322
+ })));
323
+ }
324
+ const result = {
325
+ content: finalContent,
326
+ model,
327
+ totalDuration,
328
+ evalCount,
329
+ evalDuration,
330
+ };
331
+ callbacks.onComplete?.(result);
332
+ return result;
333
+ }
334
+ /**
335
+ * Normalize a non-streaming Ollama /api/chat response.
336
+ */
337
+ normalizeOllamaResponse(data, callbacks) {
338
+ let content = data.message?.content ?? '';
339
+ // If there are tool calls and no text content, serialize the tool calls
340
+ if (data.message?.tool_calls && data.message.tool_calls.length > 0 && !content) {
341
+ content = JSON.stringify(data.message.tool_calls.map((tc) => ({
342
+ name: tc.function.name,
343
+ arguments: tc.function.arguments,
344
+ })));
345
+ }
346
+ const result = {
347
+ content,
348
+ model: data.model,
349
+ totalDuration: data.total_duration ?? 0,
350
+ evalCount: data.eval_count ?? 0,
351
+ evalDuration: data.eval_duration ?? 0,
352
+ };
353
+ callbacks?.onComplete?.(result);
354
+ return result;
355
+ }
356
+ // ==============================================================
357
+ // OpenAI-Compatible Chat
358
+ // ==============================================================
359
+ async chatOpenAICompatible(model, messages, options, shouldStream, callbacks) {
360
+ const openaiMessages = messages.map(m => {
361
+ const msg = {
362
+ role: m.role,
363
+ content: m.content,
364
+ };
365
+ if (m.toolCalls) {
366
+ msg.tool_calls = m.toolCalls;
367
+ }
368
+ if (m.toolCallId) {
369
+ msg.tool_call_id = m.toolCallId;
370
+ }
371
+ return msg;
372
+ });
373
+ const body = {
374
+ model,
375
+ messages: openaiMessages,
376
+ stream: shouldStream,
377
+ temperature: options?.temperature ?? this.config.temperature ?? 0.7,
378
+ max_tokens: options?.maxTokens ?? this.config.maxTokens,
379
+ };
380
+ // Add tools in OpenAI format
381
+ if (options?.tools && options.tools.length > 0) {
382
+ body.tools = options.tools.map(t => ({
383
+ type: 'function',
384
+ function: {
385
+ name: t.name,
386
+ description: t.description,
387
+ parameters: t.parameters,
388
+ },
389
+ }));
390
+ }
391
+ let lastError = null;
392
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
393
+ if (attempt > 0) {
394
+ const waitTime = Math.min(2000 * Math.pow(2, attempt - 1), 10000);
395
+ await this.sleep(waitTime);
396
+ }
397
+ try {
398
+ if (shouldStream && callbacks) {
399
+ return await this.streamOpenAIChat(body, callbacks);
400
+ }
401
+ return await this.nonStreamOpenAIChat(body, callbacks);
402
+ }
403
+ catch (error) {
404
+ lastError = error;
405
+ if (!this.isRetryable(error))
406
+ break;
407
+ }
408
+ }
409
+ throw lastError ?? new OllamaProviderError('Max retries exceeded');
410
+ }
411
+ /**
412
+ * Stream an OpenAI-compatible /v1/chat/completions response (SSE).
413
+ */
414
+ async streamOpenAIChat(body, callbacks) {
415
+ const response = await this.fetch(`${this.config.baseUrl}/v1/chat/completions`, {
416
+ method: 'POST',
417
+ headers: { 'Content-Type': 'application/json' },
418
+ body: JSON.stringify(body),
419
+ signal: AbortSignal.timeout(this.config.timeout),
420
+ });
421
+ if (!response.ok) {
422
+ const text = await response.text();
423
+ throw new OllamaProviderError(`OpenAI-compatible chat error (${response.status}): ${text}`, response.status, text);
424
+ }
425
+ const reader = response.body?.getReader();
426
+ if (!reader)
427
+ throw new OllamaProviderError('No response body for streaming');
428
+ const decoder = new TextDecoder();
429
+ let fullContent = '';
430
+ let model = '';
431
+ const toolCalls = new Map();
432
+ let buffer = '';
433
+ const startTime = Date.now();
434
+ try {
435
+ while (true) {
436
+ const { done, value } = await reader.read();
437
+ if (done)
438
+ break;
439
+ buffer += decoder.decode(value, { stream: true });
440
+ // SSE format: lines starting with "data: "
441
+ const lines = buffer.split('\n');
442
+ buffer = lines.pop() ?? '';
443
+ for (const line of lines) {
444
+ const trimmed = line.trim();
445
+ if (!trimmed.startsWith('data: '))
446
+ continue;
447
+ const data = trimmed.slice(6); // strip "data: "
448
+ if (data === '[DONE]')
449
+ continue;
450
+ try {
451
+ const parsed = JSON.parse(data);
452
+ model = parsed.model || model;
453
+ const delta = parsed.choices?.[0]?.delta;
454
+ if (!delta)
455
+ continue;
456
+ if (delta.content) {
457
+ fullContent += delta.content;
458
+ callbacks.onToken?.(delta.content);
459
+ }
460
+ // Handle tool calls in streaming
461
+ if (delta.tool_calls) {
462
+ for (const tc of delta.tool_calls) {
463
+ const existing = toolCalls.get(tc.index);
464
+ if (existing) {
465
+ if (tc.function?.arguments) {
466
+ existing.function.arguments += tc.function.arguments;
467
+ }
468
+ }
469
+ else {
470
+ const toolCall = {
471
+ id: tc.id || `tc_${tc.index}`,
472
+ type: 'function',
473
+ function: {
474
+ name: tc.function?.name || '',
475
+ arguments: tc.function?.arguments || '',
476
+ },
477
+ };
478
+ toolCalls.set(tc.index, toolCall);
479
+ }
480
+ }
481
+ }
482
+ // Check for tool_calls finish
483
+ if (parsed.choices?.[0]?.finish_reason === 'tool_calls') {
484
+ for (const tc of Array.from(toolCalls.values())) {
485
+ callbacks.onToolCall?.(tc);
486
+ }
487
+ }
488
+ }
489
+ catch {
490
+ // Skip malformed SSE data
491
+ }
492
+ }
493
+ }
494
+ }
495
+ catch (error) {
496
+ callbacks.onError?.(error);
497
+ throw error;
498
+ }
499
+ // Notify tool calls
500
+ if (toolCalls.size > 0) {
501
+ for (const tc of Array.from(toolCalls.values())) {
502
+ callbacks.onToolCall?.(tc);
503
+ }
504
+ }
505
+ const elapsed = Date.now() - startTime;
506
+ const result = {
507
+ content: fullContent,
508
+ model,
509
+ totalDuration: elapsed * 1_000_000, // convert ms to ns for consistency
510
+ evalCount: this.estimateTokens(fullContent),
511
+ evalDuration: elapsed * 1_000_000,
512
+ };
513
+ callbacks.onComplete?.(result);
514
+ return result;
515
+ }
516
+ /**
517
+ * Non-streaming OpenAI-compatible /v1/chat/completions response.
518
+ */
519
+ async nonStreamOpenAIChat(body, callbacks) {
520
+ const response = await this.fetch(`${this.config.baseUrl}/v1/chat/completions`, {
521
+ method: 'POST',
522
+ headers: { 'Content-Type': 'application/json' },
523
+ body: JSON.stringify({ ...body, stream: false }),
524
+ signal: AbortSignal.timeout(this.config.timeout),
525
+ });
526
+ if (!response.ok) {
527
+ const text = await response.text();
528
+ throw new OllamaProviderError(`OpenAI-compatible chat error (${response.status}): ${text}`, response.status, text);
529
+ }
530
+ const data = await response.json();
531
+ const content = data.choices?.[0]?.message?.content ?? '';
532
+ const model = data.model ?? '';
533
+ const result = {
534
+ content,
535
+ model,
536
+ totalDuration: (data.usage?.total_tokens ?? 0) * 50_000_000, // rough ns estimate
537
+ evalCount: data.usage?.completion_tokens ?? this.estimateTokens(content),
538
+ evalDuration: (data.usage?.completion_tokens ?? 0) * 50_000_000,
539
+ };
540
+ callbacks?.onComplete?.(result);
541
+ return result;
542
+ }
543
+ // ==============================================================
544
+ // Model Listing
545
+ // ==============================================================
546
+ async listModelsOllama() {
547
+ const data = await this.request('/api/tags', 'GET');
548
+ return data.models ?? [];
549
+ }
550
+ async listModelsOpenAICompatible() {
551
+ const response = await this.fetch(`${this.config.baseUrl}/v1/models`, {
552
+ method: 'GET',
553
+ signal: AbortSignal.timeout(this.config.timeout),
554
+ });
555
+ if (!response.ok) {
556
+ const text = await response.text();
557
+ throw new OllamaProviderError(`Failed to list models (${response.status}): ${text}`, response.status, text);
558
+ }
559
+ const data = await response.json();
560
+ const models = (data.data ?? []).map((m) => ({
561
+ name: m.id ?? m.name,
562
+ size: 0,
563
+ modified_at: m.created ? new Date(m.created * 1000).toISOString() : new Date().toISOString(),
564
+ details: {
565
+ format: 'unknown',
566
+ family: 'unknown',
567
+ parameter_size: 'unknown',
568
+ quantization_level: 'unknown',
569
+ },
570
+ }));
571
+ return models;
572
+ }
573
+ // ==============================================================
574
+ // Mode Detection
575
+ // ==============================================================
576
+ /**
577
+ * Detect whether the configured endpoint is native Ollama or an
578
+ * OpenAI-compatible server (LM Studio, etc.).
579
+ *
580
+ * Strategy: try the Ollama-specific GET /api/version endpoint. If it
581
+ * responds with a JSON object containing a "version" field, we treat
582
+ * it as native Ollama. Otherwise, fall back to OpenAI-compatible mode.
583
+ */
584
+ async detectMode() {
585
+ try {
586
+ const controller = new AbortController();
587
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
588
+ const response = await fetch(`${this.config.baseUrl}/api/version`, {
589
+ method: 'GET',
590
+ signal: controller.signal,
591
+ });
592
+ clearTimeout(timeoutId);
593
+ if (response.ok) {
594
+ const data = await response.json();
595
+ if (data && typeof data.version === 'string') {
596
+ this.mode = 'ollama';
597
+ return;
598
+ }
599
+ }
600
+ this.mode = 'openai-compatible';
601
+ }
602
+ catch {
603
+ this.mode = 'openai-compatible';
604
+ }
605
+ }
606
+ /**
607
+ * Get the current provider mode.
608
+ */
609
+ getMode() {
610
+ return this.mode;
611
+ }
612
+ // ==============================================================
613
+ // HTTP Helpers
614
+ // ==============================================================
615
+ /**
616
+ * Fetch wrapper with timeout and error handling.
617
+ */
618
+ async fetch(url, init) {
619
+ const controller = new AbortController();
620
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
621
+ // Merge abort signals
622
+ const externalSignal = init?.signal;
623
+ if (externalSignal) {
624
+ externalSignal.addEventListener('abort', () => controller.abort());
625
+ }
626
+ try {
627
+ const response = await fetch(url, {
628
+ ...init,
629
+ signal: controller.signal,
630
+ });
631
+ return response;
632
+ }
633
+ catch (error) {
634
+ if (error instanceof DOMException && error.name === 'AbortError') {
635
+ throw new OllamaTimeoutError(this.config.timeout);
636
+ }
637
+ throw new OllamaConnectionError(this.config.baseUrl, error);
638
+ }
639
+ finally {
640
+ clearTimeout(timeoutId);
641
+ }
642
+ }
643
+ /**
644
+ * Typed request helper for non-streaming JSON endpoints.
645
+ */
646
+ async request(path, method, body) {
647
+ let lastError = null;
648
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
649
+ if (attempt > 0) {
650
+ const waitTime = Math.min(2000 * Math.pow(2, attempt - 1), 10000);
651
+ await this.sleep(waitTime);
652
+ }
653
+ try {
654
+ const init = {
655
+ method,
656
+ headers: { 'Content-Type': 'application/json' },
657
+ signal: AbortSignal.timeout(this.config.timeout),
658
+ };
659
+ if (body !== undefined) {
660
+ init.body = JSON.stringify(body);
661
+ }
662
+ const response = await this.fetch(`${this.config.baseUrl}${path}`, init);
663
+ if (!response.ok) {
664
+ const text = await response.text();
665
+ throw new OllamaProviderError(`Ollama API error (${response.status}): ${text}`, response.status, text);
666
+ }
667
+ return await response.json();
668
+ }
669
+ catch (error) {
670
+ lastError = error;
671
+ if (!this.isRetryable(error))
672
+ break;
673
+ }
674
+ }
675
+ throw lastError ?? new OllamaProviderError('Max retries exceeded');
676
+ }
677
+ // ==============================================================
678
+ // Message / Tool Conversion
679
+ // ==============================================================
680
+ convertMessages(messages) {
681
+ return messages.map(m => {
682
+ const msg = {
683
+ role: m.role,
684
+ content: m.content,
685
+ };
686
+ // Convert tool calls to Ollama format
687
+ if (m.toolCalls && m.toolCalls.length > 0) {
688
+ msg.tool_calls = m.toolCalls.map((tc) => {
689
+ let args;
690
+ if (typeof tc.function?.arguments === 'string') {
691
+ try {
692
+ args = JSON.parse(tc.function.arguments);
693
+ }
694
+ catch {
695
+ args = { raw: tc.function.arguments };
696
+ }
697
+ }
698
+ else if (tc.function?.arguments) {
699
+ args = tc.function.arguments;
700
+ }
701
+ else {
702
+ args = {};
703
+ }
704
+ return {
705
+ function: {
706
+ name: tc.function?.name ?? '',
707
+ arguments: args,
708
+ },
709
+ };
710
+ });
711
+ }
712
+ // Tool result reference
713
+ if (m.toolCallId) {
714
+ msg.tool_call_id = m.toolCallId;
715
+ }
716
+ return msg;
717
+ });
718
+ }
719
+ convertTools(tools) {
720
+ return tools.map(t => ({
721
+ type: 'function',
722
+ function: {
723
+ name: t.name,
724
+ description: t.description,
725
+ parameters: t.parameters,
726
+ },
727
+ }));
728
+ }
729
+ // ==============================================================
730
+ // Utility
731
+ // ==============================================================
732
+ isRetryable(error) {
733
+ if (error instanceof OllamaProviderError) {
734
+ // Retry on server errors and rate limits
735
+ return (error.statusCode === 429 ||
736
+ (error.statusCode !== undefined && error.statusCode >= 500));
737
+ }
738
+ if (error instanceof OllamaTimeoutError)
739
+ return true;
740
+ if (error instanceof OllamaConnectionError)
741
+ return true;
742
+ // Network-level errors are retryable
743
+ if (error instanceof TypeError && error.message.includes('fetch'))
744
+ return true;
745
+ return false;
746
+ }
747
+ sleep(ms) {
748
+ return new Promise(resolve => setTimeout(resolve, ms));
749
+ }
750
+ }
751
+ //# sourceMappingURL=ollama.js.map