nex-code 0.3.5 → 0.3.7
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.
- package/README.md +23 -1
- package/dist/bundle.js +505 -0
- package/dist/nex-code.js +485 -0
- package/package.json +8 -6
- package/bin/nex-code.js +0 -99
- package/cli/agent.js +0 -835
- package/cli/compactor.js +0 -85
- package/cli/context-engine.js +0 -507
- package/cli/context.js +0 -98
- package/cli/costs.js +0 -290
- package/cli/diff.js +0 -366
- package/cli/file-history.js +0 -94
- package/cli/format.js +0 -211
- package/cli/fuzzy-match.js +0 -270
- package/cli/git.js +0 -211
- package/cli/hooks.js +0 -173
- package/cli/index.js +0 -1289
- package/cli/mcp.js +0 -284
- package/cli/memory.js +0 -170
- package/cli/ollama.js +0 -130
- package/cli/permissions.js +0 -124
- package/cli/picker.js +0 -201
- package/cli/planner.js +0 -282
- package/cli/providers/anthropic.js +0 -333
- package/cli/providers/base.js +0 -116
- package/cli/providers/gemini.js +0 -239
- package/cli/providers/local.js +0 -249
- package/cli/providers/ollama.js +0 -228
- package/cli/providers/openai.js +0 -237
- package/cli/providers/registry.js +0 -454
- package/cli/render.js +0 -495
- package/cli/safety.js +0 -241
- package/cli/session.js +0 -133
- package/cli/skills.js +0 -412
- package/cli/spinner.js +0 -371
- package/cli/sub-agent.js +0 -441
- package/cli/tasks.js +0 -179
- package/cli/tool-tiers.js +0 -164
- package/cli/tool-validator.js +0 -138
- package/cli/tools.js +0 -1050
- package/cli/ui.js +0 -93
package/cli/providers/local.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/providers/local.js — Local Ollama Server Provider
|
|
3
|
-
* Connects to localhost:11434 (default Ollama install). No auth required.
|
|
4
|
-
* Auto-detects available models via /api/tags.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const axios = require('axios');
|
|
8
|
-
const { BaseProvider } = require('./base');
|
|
9
|
-
|
|
10
|
-
const DEFAULT_LOCAL_URL = 'http://localhost:11434';
|
|
11
|
-
|
|
12
|
-
class LocalProvider extends BaseProvider {
|
|
13
|
-
constructor(config = {}) {
|
|
14
|
-
super({
|
|
15
|
-
name: 'local',
|
|
16
|
-
baseUrl: config.baseUrl || process.env.OLLAMA_HOST || process.env.OLLAMA_LOCAL_URL || DEFAULT_LOCAL_URL,
|
|
17
|
-
models: config.models || {},
|
|
18
|
-
defaultModel: config.defaultModel || null,
|
|
19
|
-
...config,
|
|
20
|
-
});
|
|
21
|
-
this.timeout = config.timeout || 300000;
|
|
22
|
-
this.temperature = config.temperature ?? 0.2;
|
|
23
|
-
this._modelsLoaded = false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
isConfigured() {
|
|
27
|
-
return true; // No API key needed
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Fetch available models from local Ollama server.
|
|
32
|
-
* Caches result after first call.
|
|
33
|
-
*/
|
|
34
|
-
async loadModels() {
|
|
35
|
-
if (this._modelsLoaded) return this.models;
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const response = await axios.get(`${this.baseUrl}/api/tags`, { timeout: 5000 });
|
|
39
|
-
const tags = response.data?.models || [];
|
|
40
|
-
|
|
41
|
-
this.models = {};
|
|
42
|
-
for (const m of tags) {
|
|
43
|
-
const name = m.name || m.model;
|
|
44
|
-
if (!name) continue;
|
|
45
|
-
const id = name.replace(/:latest$/, '');
|
|
46
|
-
|
|
47
|
-
// Try to get actual context window from model metadata
|
|
48
|
-
let contextWindow = 32768; // Conservative fallback
|
|
49
|
-
try {
|
|
50
|
-
const showResp = await axios.post(
|
|
51
|
-
`${this.baseUrl}/api/show`,
|
|
52
|
-
{ name },
|
|
53
|
-
{ timeout: 5000 }
|
|
54
|
-
);
|
|
55
|
-
const params = showResp.data?.model_info || showResp.data?.details || {};
|
|
56
|
-
// Ollama exposes context length in model_info
|
|
57
|
-
contextWindow = params['general.context_length']
|
|
58
|
-
|| params['llama.context_length']
|
|
59
|
-
|| this._parseContextFromModelfile(showResp.data?.modelfile)
|
|
60
|
-
|| 32768;
|
|
61
|
-
} catch {
|
|
62
|
-
// /api/show failed — use fallback
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
this.models[id] = {
|
|
66
|
-
id,
|
|
67
|
-
name: m.name,
|
|
68
|
-
maxTokens: Math.min(8192, Math.floor(contextWindow * 0.1)),
|
|
69
|
-
contextWindow,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!this.defaultModel && Object.keys(this.models).length > 0) {
|
|
74
|
-
this.defaultModel = Object.keys(this.models)[0];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this._modelsLoaded = true;
|
|
78
|
-
} catch {
|
|
79
|
-
// Server not running or unreachable
|
|
80
|
-
this.models = {};
|
|
81
|
-
this._modelsLoaded = false;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return this.models;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getModels() {
|
|
88
|
-
return this.models;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getModelNames() {
|
|
92
|
-
return Object.keys(this.models);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async chat(messages, tools, options = {}) {
|
|
96
|
-
if (!this._modelsLoaded) await this.loadModels();
|
|
97
|
-
|
|
98
|
-
const model = options.model || this.defaultModel;
|
|
99
|
-
if (!model) throw new Error('No local model available. Is Ollama running?');
|
|
100
|
-
|
|
101
|
-
const response = await axios.post(
|
|
102
|
-
`${this.baseUrl}/api/chat`,
|
|
103
|
-
{
|
|
104
|
-
model,
|
|
105
|
-
messages,
|
|
106
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
107
|
-
stream: false,
|
|
108
|
-
options: {
|
|
109
|
-
temperature: options.temperature ?? this.temperature,
|
|
110
|
-
num_predict: options.maxTokens || 8192,
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
{ timeout: options.timeout || this.timeout }
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
return this.normalizeResponse(response.data);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async stream(messages, tools, options = {}) {
|
|
120
|
-
if (!this._modelsLoaded) await this.loadModels();
|
|
121
|
-
|
|
122
|
-
const model = options.model || this.defaultModel;
|
|
123
|
-
if (!model) throw new Error('No local model available. Is Ollama running?');
|
|
124
|
-
const onToken = options.onToken || (() => {});
|
|
125
|
-
|
|
126
|
-
let response;
|
|
127
|
-
try {
|
|
128
|
-
response = await axios.post(
|
|
129
|
-
`${this.baseUrl}/api/chat`,
|
|
130
|
-
{
|
|
131
|
-
model,
|
|
132
|
-
messages,
|
|
133
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
134
|
-
stream: true,
|
|
135
|
-
options: {
|
|
136
|
-
temperature: options.temperature ?? this.temperature,
|
|
137
|
-
num_predict: options.maxTokens || 8192,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
timeout: options.timeout || this.timeout,
|
|
142
|
-
responseType: 'stream',
|
|
143
|
-
signal: options.signal,
|
|
144
|
-
}
|
|
145
|
-
);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
if (err.name === 'CanceledError' || err.name === 'AbortError' || err.code === 'ERR_CANCELED') throw err;
|
|
148
|
-
const msg = err.response?.data?.error || err.message;
|
|
149
|
-
throw new Error(`API Error: ${msg}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return new Promise((resolve, reject) => {
|
|
153
|
-
let content = '';
|
|
154
|
-
let toolCalls = [];
|
|
155
|
-
let buffer = '';
|
|
156
|
-
|
|
157
|
-
// Abort listener: destroy stream on signal
|
|
158
|
-
if (options.signal) {
|
|
159
|
-
options.signal.addEventListener('abort', () => {
|
|
160
|
-
response.data.destroy();
|
|
161
|
-
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
162
|
-
}, { once: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
response.data.on('data', (chunk) => {
|
|
166
|
-
buffer += chunk.toString();
|
|
167
|
-
const lines = buffer.split('\n');
|
|
168
|
-
buffer = lines.pop() || '';
|
|
169
|
-
|
|
170
|
-
for (const line of lines) {
|
|
171
|
-
if (!line.trim()) continue;
|
|
172
|
-
let parsed;
|
|
173
|
-
try {
|
|
174
|
-
parsed = JSON.parse(line);
|
|
175
|
-
} catch {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (parsed.message?.content) {
|
|
180
|
-
onToken(parsed.message.content);
|
|
181
|
-
content += parsed.message.content;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (parsed.message?.tool_calls) {
|
|
185
|
-
toolCalls = toolCalls.concat(parsed.message.tool_calls);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (parsed.done) {
|
|
189
|
-
resolve({ content, tool_calls: this._normalizeToolCalls(toolCalls) });
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
response.data.on('error', (err) => {
|
|
196
|
-
if (options.signal?.aborted) return; // Ignore errors after abort
|
|
197
|
-
reject(new Error(`Stream error: ${err.message}`));
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
response.data.on('end', () => {
|
|
201
|
-
if (buffer.trim()) {
|
|
202
|
-
try {
|
|
203
|
-
const parsed = JSON.parse(buffer);
|
|
204
|
-
if (parsed.message?.content) {
|
|
205
|
-
onToken(parsed.message.content);
|
|
206
|
-
content += parsed.message.content;
|
|
207
|
-
}
|
|
208
|
-
if (parsed.message?.tool_calls) {
|
|
209
|
-
toolCalls = toolCalls.concat(parsed.message.tool_calls);
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
/* ignore */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
resolve({ content, tool_calls: this._normalizeToolCalls(toolCalls) });
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
normalizeResponse(data) {
|
|
221
|
-
const msg = data.message || {};
|
|
222
|
-
return {
|
|
223
|
-
content: msg.content || '',
|
|
224
|
-
tool_calls: this._normalizeToolCalls(msg.tool_calls || []),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Parse num_ctx from Ollama modelfile string.
|
|
230
|
-
* Modelfiles contain lines like: PARAMETER num_ctx 131072
|
|
231
|
-
*/
|
|
232
|
-
_parseContextFromModelfile(modelfile) {
|
|
233
|
-
if (!modelfile) return null;
|
|
234
|
-
const match = modelfile.match(/PARAMETER\s+num_ctx\s+(\d+)/i);
|
|
235
|
-
return match ? parseInt(match[1], 10) : null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
_normalizeToolCalls(toolCalls) {
|
|
239
|
-
return toolCalls.map((tc, i) => ({
|
|
240
|
-
id: tc.id || `local-${Date.now()}-${i}`,
|
|
241
|
-
function: {
|
|
242
|
-
name: tc.function?.name || tc.name || 'unknown',
|
|
243
|
-
arguments: tc.function?.arguments || tc.arguments || {},
|
|
244
|
-
},
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
module.exports = { LocalProvider, DEFAULT_LOCAL_URL };
|
package/cli/providers/ollama.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cli/providers/ollama.js — Ollama Cloud Provider
|
|
3
|
-
* Connects to https://ollama.com API with Bearer auth and NDJSON streaming.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const axios = require('axios');
|
|
7
|
-
const { BaseProvider } = require('./base');
|
|
8
|
-
|
|
9
|
-
const OLLAMA_MODELS = {
|
|
10
|
-
// Primary: Best coding models for agentic workflows
|
|
11
|
-
'qwen3-coder:480b': { id: 'qwen3-coder:480b', name: 'Qwen3 Coder 480B', maxTokens: 16384, contextWindow: 131072 },
|
|
12
|
-
'qwen3-coder-next': { id: 'qwen3-coder-next', name: 'Qwen3 Coder Next', maxTokens: 16384, contextWindow: 131072 },
|
|
13
|
-
'devstral-2:123b': { id: 'devstral-2:123b', name: 'Devstral 2 123B', maxTokens: 16384, contextWindow: 131072 },
|
|
14
|
-
'devstral-small-2:24b': { id: 'devstral-small-2:24b', name: 'Devstral Small 2 24B', maxTokens: 16384, contextWindow: 131072 },
|
|
15
|
-
// Large general-purpose models
|
|
16
|
-
'kimi-k2.5': { id: 'kimi-k2.5', name: 'Kimi K2.5', maxTokens: 16384, contextWindow: 256000 },
|
|
17
|
-
'kimi-k2:1t': { id: 'kimi-k2:1t', name: 'Kimi K2 1T', maxTokens: 16384, contextWindow: 256000 },
|
|
18
|
-
'kimi-k2-thinking': { id: 'kimi-k2-thinking', name: 'Kimi K2 Thinking', maxTokens: 16384, contextWindow: 256000 },
|
|
19
|
-
'deepseek-v3.2': { id: 'deepseek-v3.2', name: 'DeepSeek V3.2', maxTokens: 16384, contextWindow: 131072 },
|
|
20
|
-
'deepseek-v3.1:671b': { id: 'deepseek-v3.1:671b', name: 'DeepSeek V3.1 671B', maxTokens: 16384, contextWindow: 131072 },
|
|
21
|
-
'cogito-2.1:671b': { id: 'cogito-2.1:671b', name: 'Cogito 2.1 671B', maxTokens: 16384, contextWindow: 131072 },
|
|
22
|
-
// Medium models
|
|
23
|
-
'qwen3-next:80b': { id: 'qwen3-next:80b', name: 'Qwen3 Next 80B', maxTokens: 16384, contextWindow: 131072 },
|
|
24
|
-
'qwen3.5:397b': { id: 'qwen3.5:397b', name: 'Qwen3.5 397B', maxTokens: 16384, contextWindow: 131072 },
|
|
25
|
-
'mistral-large-3:675b': { id: 'mistral-large-3:675b', name: 'Mistral Large 3 675B', maxTokens: 16384, contextWindow: 131072 },
|
|
26
|
-
'gpt-oss:120b': { id: 'gpt-oss:120b', name: 'GPT-OSS 120B', maxTokens: 16384, contextWindow: 131072 },
|
|
27
|
-
'minimax-m2.5': { id: 'minimax-m2.5', name: 'MiniMax M2.5', maxTokens: 16384, contextWindow: 131072 },
|
|
28
|
-
'glm-5': { id: 'glm-5', name: 'GLM 5', maxTokens: 16384, contextWindow: 128000 },
|
|
29
|
-
'glm-4.7': { id: 'glm-4.7', name: 'GLM 4.7', maxTokens: 16384, contextWindow: 128000 },
|
|
30
|
-
// Small / fast models
|
|
31
|
-
'gemma3:27b': { id: 'gemma3:27b', name: 'Gemma 3 27B', maxTokens: 8192, contextWindow: 131072 },
|
|
32
|
-
'gemma3:12b': { id: 'gemma3:12b', name: 'Gemma 3 12B', maxTokens: 8192, contextWindow: 131072 },
|
|
33
|
-
'gemma3:4b': { id: 'gemma3:4b', name: 'Gemma 3 4B', maxTokens: 8192, contextWindow: 131072 },
|
|
34
|
-
'ministral-3:14b': { id: 'ministral-3:14b', name: 'Ministral 3 14B', maxTokens: 8192, contextWindow: 131072 },
|
|
35
|
-
'ministral-3:8b': { id: 'ministral-3:8b', name: 'Ministral 3 8B', maxTokens: 8192, contextWindow: 131072 },
|
|
36
|
-
// Special
|
|
37
|
-
'gemini-3-flash-preview': { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', maxTokens: 16384, contextWindow: 131072 },
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
class OllamaProvider extends BaseProvider {
|
|
41
|
-
constructor(config = {}) {
|
|
42
|
-
super({
|
|
43
|
-
name: 'ollama',
|
|
44
|
-
baseUrl: config.baseUrl || 'https://ollama.com',
|
|
45
|
-
models: config.models || OLLAMA_MODELS,
|
|
46
|
-
defaultModel: config.defaultModel || 'qwen3-coder:480b',
|
|
47
|
-
...config,
|
|
48
|
-
});
|
|
49
|
-
this.timeout = config.timeout || 180000;
|
|
50
|
-
this.temperature = config.temperature ?? 0.2;
|
|
51
|
-
this._discovered = false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Discover available models from the Ollama API.
|
|
56
|
-
* Merges discovered models with the hardcoded fallback list.
|
|
57
|
-
* Cached after first call.
|
|
58
|
-
*/
|
|
59
|
-
async discoverModels() {
|
|
60
|
-
if (this._discovered) return;
|
|
61
|
-
this._discovered = true;
|
|
62
|
-
try {
|
|
63
|
-
const resp = await axios.get(`${this.baseUrl}/api/tags`, {
|
|
64
|
-
timeout: 5000, headers: this._getHeaders(),
|
|
65
|
-
});
|
|
66
|
-
const tags = resp.data?.models || [];
|
|
67
|
-
for (const m of tags) {
|
|
68
|
-
const id = (m.name || m.model || '').replace(/:latest$/, '');
|
|
69
|
-
if (!id || this.models[id]) continue;
|
|
70
|
-
this.models[id] = { id, name: m.name || id, maxTokens: 16384, contextWindow: 131072 };
|
|
71
|
-
}
|
|
72
|
-
} catch { /* API unavailable — use hardcoded list */ }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
isConfigured() {
|
|
76
|
-
return !!this.getApiKey();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
getApiKey() {
|
|
80
|
-
return process.env.OLLAMA_API_KEY || null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
_getHeaders() {
|
|
84
|
-
const key = this.getApiKey();
|
|
85
|
-
if (!key) throw new Error('OLLAMA_API_KEY not set');
|
|
86
|
-
return { Authorization: `Bearer ${key}` };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async chat(messages, tools, options = {}) {
|
|
90
|
-
await this.discoverModels();
|
|
91
|
-
const model = options.model || this.defaultModel;
|
|
92
|
-
const modelInfo = this.getModel(model);
|
|
93
|
-
const maxTokens = options.maxTokens || modelInfo?.maxTokens || 16384;
|
|
94
|
-
|
|
95
|
-
const response = await axios.post(
|
|
96
|
-
`${this.baseUrl}/api/chat`,
|
|
97
|
-
{
|
|
98
|
-
model,
|
|
99
|
-
messages,
|
|
100
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
101
|
-
stream: false,
|
|
102
|
-
options: { temperature: options.temperature ?? this.temperature, num_predict: maxTokens },
|
|
103
|
-
},
|
|
104
|
-
{ timeout: options.timeout || this.timeout, headers: this._getHeaders() }
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
return this.normalizeResponse(response.data);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async stream(messages, tools, options = {}) {
|
|
111
|
-
await this.discoverModels();
|
|
112
|
-
const model = options.model || this.defaultModel;
|
|
113
|
-
const modelInfo = this.getModel(model);
|
|
114
|
-
const maxTokens = options.maxTokens || modelInfo?.maxTokens || 16384;
|
|
115
|
-
const onToken = options.onToken || (() => {});
|
|
116
|
-
|
|
117
|
-
let response;
|
|
118
|
-
try {
|
|
119
|
-
response = await axios.post(
|
|
120
|
-
`${this.baseUrl}/api/chat`,
|
|
121
|
-
{
|
|
122
|
-
model,
|
|
123
|
-
messages,
|
|
124
|
-
tools: tools && tools.length > 0 ? tools : undefined,
|
|
125
|
-
stream: true,
|
|
126
|
-
options: { temperature: options.temperature ?? this.temperature, num_predict: maxTokens },
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
timeout: options.timeout || this.timeout,
|
|
130
|
-
headers: this._getHeaders(),
|
|
131
|
-
responseType: 'stream',
|
|
132
|
-
signal: options.signal,
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
} catch (err) {
|
|
136
|
-
if (err.name === 'CanceledError' || err.name === 'AbortError' || err.code === 'ERR_CANCELED') throw err;
|
|
137
|
-
const msg = err.response?.data?.error || err.message;
|
|
138
|
-
throw new Error(`API Error: ${msg}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
let content = '';
|
|
143
|
-
let toolCalls = [];
|
|
144
|
-
let buffer = '';
|
|
145
|
-
|
|
146
|
-
// Abort listener: destroy stream on signal
|
|
147
|
-
if (options.signal) {
|
|
148
|
-
options.signal.addEventListener('abort', () => {
|
|
149
|
-
response.data.destroy();
|
|
150
|
-
reject(new DOMException('The operation was aborted', 'AbortError'));
|
|
151
|
-
}, { once: true });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
response.data.on('data', (chunk) => {
|
|
155
|
-
buffer += chunk.toString();
|
|
156
|
-
const lines = buffer.split('\n');
|
|
157
|
-
buffer = lines.pop() || '';
|
|
158
|
-
|
|
159
|
-
for (const line of lines) {
|
|
160
|
-
if (!line.trim()) continue;
|
|
161
|
-
let parsed;
|
|
162
|
-
try {
|
|
163
|
-
parsed = JSON.parse(line);
|
|
164
|
-
} catch {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (parsed.message?.content) {
|
|
169
|
-
onToken(parsed.message.content);
|
|
170
|
-
content += parsed.message.content;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (parsed.message?.tool_calls) {
|
|
174
|
-
toolCalls = toolCalls.concat(parsed.message.tool_calls);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (parsed.done) {
|
|
178
|
-
resolve({ content, tool_calls: this._normalizeToolCalls(toolCalls) });
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
response.data.on('error', (err) => {
|
|
185
|
-
if (options.signal?.aborted) return; // Ignore errors after abort
|
|
186
|
-
reject(new Error(`Stream error: ${err.message}`));
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
response.data.on('end', () => {
|
|
190
|
-
if (buffer.trim()) {
|
|
191
|
-
try {
|
|
192
|
-
const parsed = JSON.parse(buffer);
|
|
193
|
-
if (parsed.message?.content) {
|
|
194
|
-
onToken(parsed.message.content);
|
|
195
|
-
content += parsed.message.content;
|
|
196
|
-
}
|
|
197
|
-
if (parsed.message?.tool_calls) {
|
|
198
|
-
toolCalls = toolCalls.concat(parsed.message.tool_calls);
|
|
199
|
-
}
|
|
200
|
-
} catch {
|
|
201
|
-
/* ignore */
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
resolve({ content, tool_calls: this._normalizeToolCalls(toolCalls) });
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
normalizeResponse(data) {
|
|
210
|
-
const msg = data.message || {};
|
|
211
|
-
return {
|
|
212
|
-
content: msg.content || '',
|
|
213
|
-
tool_calls: this._normalizeToolCalls(msg.tool_calls || []),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
_normalizeToolCalls(toolCalls) {
|
|
218
|
-
return toolCalls.map((tc, i) => ({
|
|
219
|
-
id: tc.id || `ollama-${Date.now()}-${i}`,
|
|
220
|
-
function: {
|
|
221
|
-
name: tc.function?.name || tc.name || 'unknown',
|
|
222
|
-
arguments: tc.function?.arguments || tc.arguments || {},
|
|
223
|
-
},
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = { OllamaProvider, OLLAMA_MODELS };
|