nex-code 0.3.4 → 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.
@@ -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 };
@@ -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 };