vibecodingmachine-core 1.0.0 → 1.0.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.
- package/.babelrc +13 -13
- package/README.md +28 -28
- package/__tests__/applescript-manager-claude-fix.test.js +286 -286
- package/__tests__/requirement-2-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-3-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-4-auto-start-looping.test.js +69 -69
- package/__tests__/requirement-6-auto-start-looping.test.js +73 -73
- package/__tests__/requirement-7-status-tracking.test.js +332 -332
- package/jest.config.js +18 -18
- package/jest.setup.js +12 -12
- package/package.json +47 -45
- package/src/auth/access-denied.html +119 -119
- package/src/auth/shared-auth-storage.js +230 -230
- package/src/autonomous-mode/feature-implementer.cjs +70 -70
- package/src/autonomous-mode/feature-implementer.js +425 -425
- package/src/chat-management/chat-manager.cjs +71 -71
- package/src/chat-management/chat-manager.js +342 -342
- package/src/ide-integration/__tests__/applescript-manager-thread-closure.test.js +227 -227
- package/src/ide-integration/aider-cli-manager.cjs +850 -850
- package/src/ide-integration/applescript-manager.cjs +1088 -1088
- package/src/ide-integration/applescript-manager.js +2802 -2802
- package/src/ide-integration/applescript-utils.js +306 -306
- package/src/ide-integration/cdp-manager.cjs +221 -221
- package/src/ide-integration/cdp-manager.js +321 -321
- package/src/ide-integration/claude-code-cli-manager.cjs +301 -301
- package/src/ide-integration/cline-cli-manager.cjs +2252 -2252
- package/src/ide-integration/continue-cli-manager.js +431 -431
- package/src/ide-integration/provider-manager.cjs +354 -354
- package/src/ide-integration/quota-detector.cjs +34 -34
- package/src/ide-integration/quota-detector.js +349 -349
- package/src/ide-integration/windows-automation-manager.js +262 -262
- package/src/index.cjs +43 -43
- package/src/index.js +17 -17
- package/src/llm/direct-llm-manager.cjs +609 -609
- package/src/ui/ButtonComponents.js +247 -247
- package/src/ui/ChatInterface.js +499 -499
- package/src/ui/StateManager.js +259 -259
- package/src/utils/audit-logger.cjs +116 -116
- package/src/utils/config-helpers.cjs +94 -94
- package/src/utils/config-helpers.js +94 -94
- package/src/utils/electron-update-checker.js +85 -78
- package/src/utils/gcloud-auth.cjs +394 -394
- package/src/utils/logger.cjs +193 -193
- package/src/utils/logger.js +191 -191
- package/src/utils/repo-helpers.cjs +120 -120
- package/src/utils/repo-helpers.js +120 -120
- package/src/utils/requirement-helpers.js +432 -432
- package/src/utils/update-checker.js +167 -167
|
@@ -1,609 +1,609 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Direct LLM API Manager - Call LLM APIs directly without IDE CLI tools
|
|
3
|
-
* Supports: Ollama (local), Anthropic, Groq, AWS Bedrock
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const http = require('http');
|
|
8
|
-
|
|
9
|
-
class DirectLLMManager {
|
|
10
|
-
constructor(sharedProviderManager = null) {
|
|
11
|
-
this.logger = console;
|
|
12
|
-
// Use shared ProviderManager if provided, otherwise create new instance
|
|
13
|
-
// IMPORTANT: Pass shared instance to maintain rate limit state across calls
|
|
14
|
-
if (sharedProviderManager) {
|
|
15
|
-
this.providerManager = sharedProviderManager;
|
|
16
|
-
} else {
|
|
17
|
-
try {
|
|
18
|
-
const ProviderManager = require('../ide-integration/provider-manager.cjs');
|
|
19
|
-
this.providerManager = new ProviderManager();
|
|
20
|
-
} catch (err) {
|
|
21
|
-
this.providerManager = null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Detect and save rate limit from error message
|
|
28
|
-
* @param {string} provider - Provider name
|
|
29
|
-
* @param {string} model - Model name
|
|
30
|
-
* @param {string} errorMessage - Error message from API
|
|
31
|
-
*/
|
|
32
|
-
detectAndSaveRateLimit(provider, model, errorMessage) {
|
|
33
|
-
if (!this.providerManager) return;
|
|
34
|
-
|
|
35
|
-
// Check for rate limit indicators
|
|
36
|
-
const isRateLimit = errorMessage.includes('rate limit') ||
|
|
37
|
-
errorMessage.includes('Rate limit') ||
|
|
38
|
-
errorMessage.includes('too many requests') ||
|
|
39
|
-
errorMessage.includes('429') ||
|
|
40
|
-
errorMessage.includes('quota') ||
|
|
41
|
-
errorMessage.includes('Weekly limit reached') ||
|
|
42
|
-
errorMessage.includes('Daily limit reached') ||
|
|
43
|
-
errorMessage.includes('limit reached');
|
|
44
|
-
|
|
45
|
-
if (isRateLimit) {
|
|
46
|
-
this.providerManager.markRateLimited(provider, model, errorMessage);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Call Ollama API directly (local)
|
|
52
|
-
* @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
|
|
53
|
-
* @param {string} prompt - Prompt to send
|
|
54
|
-
* @param {Object} options - Options (onChunk, onComplete, onError)
|
|
55
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
56
|
-
*/
|
|
57
|
-
async callOllama(model, prompt, options = {}) {
|
|
58
|
-
const { onChunk, onComplete, onError, temperature = 0.2 } = options;
|
|
59
|
-
|
|
60
|
-
return new Promise((resolve) => {
|
|
61
|
-
let fullResponse = '';
|
|
62
|
-
|
|
63
|
-
const postData = JSON.stringify({
|
|
64
|
-
model: model,
|
|
65
|
-
prompt: prompt,
|
|
66
|
-
stream: true,
|
|
67
|
-
options: {
|
|
68
|
-
temperature: temperature
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const req = http.request({
|
|
73
|
-
hostname: 'localhost',
|
|
74
|
-
port: 11434,
|
|
75
|
-
path: '/api/generate',
|
|
76
|
-
method: 'POST',
|
|
77
|
-
headers: {
|
|
78
|
-
'Content-Type': 'application/json',
|
|
79
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
80
|
-
}
|
|
81
|
-
}, (res) => {
|
|
82
|
-
let buffer = '';
|
|
83
|
-
|
|
84
|
-
res.on('data', (chunk) => {
|
|
85
|
-
buffer += chunk.toString();
|
|
86
|
-
const lines = buffer.split('\n');
|
|
87
|
-
buffer = lines.pop(); // Keep incomplete line in buffer
|
|
88
|
-
|
|
89
|
-
for (const line of lines) {
|
|
90
|
-
if (!line.trim()) continue;
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const data = JSON.parse(line);
|
|
94
|
-
if (data.response) {
|
|
95
|
-
fullResponse += data.response;
|
|
96
|
-
if (onChunk) onChunk(data.response);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (data.done) {
|
|
100
|
-
if (onComplete) onComplete(fullResponse);
|
|
101
|
-
resolve({
|
|
102
|
-
success: true,
|
|
103
|
-
response: fullResponse,
|
|
104
|
-
model: data.model,
|
|
105
|
-
context: data.context
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
} catch (err) {
|
|
109
|
-
// Ignore JSON parse errors for partial chunks
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
res.on('end', () => {
|
|
115
|
-
if (!fullResponse) {
|
|
116
|
-
const error = 'No response received from Ollama';
|
|
117
|
-
if (onError) onError(error);
|
|
118
|
-
resolve({ success: false, error });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
req.on('error', (error) => {
|
|
124
|
-
const errorMsg = `Ollama API error: ${error.message}`;
|
|
125
|
-
if (onError) onError(errorMsg);
|
|
126
|
-
resolve({ success: false, error: errorMsg });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
req.write(postData);
|
|
130
|
-
req.end();
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Call Anthropic API directly
|
|
136
|
-
* @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
|
|
137
|
-
* @param {string} prompt - Prompt to send
|
|
138
|
-
* @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
|
|
139
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
140
|
-
*/
|
|
141
|
-
async callAnthropic(model, prompt, options = {}) {
|
|
142
|
-
const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
143
|
-
|
|
144
|
-
if (!apiKey) {
|
|
145
|
-
const error = 'Anthropic API key required';
|
|
146
|
-
if (onError) onError(error);
|
|
147
|
-
return { success: false, error };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return new Promise((resolve) => {
|
|
151
|
-
let fullResponse = '';
|
|
152
|
-
|
|
153
|
-
const postData = JSON.stringify({
|
|
154
|
-
model: model,
|
|
155
|
-
max_tokens: maxTokens,
|
|
156
|
-
temperature: temperature,
|
|
157
|
-
messages: [
|
|
158
|
-
{ role: 'user', content: prompt }
|
|
159
|
-
],
|
|
160
|
-
stream: true
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const req = https.request({
|
|
164
|
-
hostname: 'api.anthropic.com',
|
|
165
|
-
path: '/v1/messages',
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: {
|
|
168
|
-
'Content-Type': 'application/json',
|
|
169
|
-
'x-api-key': apiKey,
|
|
170
|
-
'anthropic-version': '2023-06-01',
|
|
171
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
172
|
-
}
|
|
173
|
-
}, (res) => {
|
|
174
|
-
let buffer = '';
|
|
175
|
-
|
|
176
|
-
res.on('data', (chunk) => {
|
|
177
|
-
buffer += chunk.toString();
|
|
178
|
-
const lines = buffer.split('\n');
|
|
179
|
-
buffer = lines.pop();
|
|
180
|
-
|
|
181
|
-
for (const line of lines) {
|
|
182
|
-
if (!line.trim() || !line.startsWith('data: ')) continue;
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
const jsonStr = line.slice(6); // Remove "data: " prefix
|
|
186
|
-
if (jsonStr === '[DONE]') continue;
|
|
187
|
-
|
|
188
|
-
const data = JSON.parse(jsonStr);
|
|
189
|
-
|
|
190
|
-
if (data.type === 'content_block_delta' && data.delta?.text) {
|
|
191
|
-
fullResponse += data.delta.text;
|
|
192
|
-
if (onChunk) onChunk(data.delta.text);
|
|
193
|
-
} else if (data.type === 'message_stop') {
|
|
194
|
-
if (onComplete) onComplete(fullResponse);
|
|
195
|
-
resolve({
|
|
196
|
-
success: true,
|
|
197
|
-
response: fullResponse,
|
|
198
|
-
model: model
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
} catch (err) {
|
|
202
|
-
// Ignore JSON parse errors
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
res.on('end', () => {
|
|
208
|
-
if (!fullResponse) {
|
|
209
|
-
const error = 'No response received from Anthropic';
|
|
210
|
-
if (onError) onError(error);
|
|
211
|
-
resolve({ success: false, error });
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
req.on('error', (error) => {
|
|
217
|
-
const errorMsg = `Anthropic API error: ${error.message}`;
|
|
218
|
-
if (onError) onError(errorMsg);
|
|
219
|
-
resolve({ success: false, error: errorMsg });
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
req.write(postData);
|
|
223
|
-
req.end();
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Call Groq API directly
|
|
229
|
-
* @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
|
|
230
|
-
* @param {string} prompt - Prompt to send
|
|
231
|
-
* @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
|
|
232
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
233
|
-
*/
|
|
234
|
-
async callGroq(model, prompt, options = {}) {
|
|
235
|
-
const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
236
|
-
|
|
237
|
-
if (!apiKey) {
|
|
238
|
-
const error = 'Groq API key required';
|
|
239
|
-
if (onError) onError(error);
|
|
240
|
-
return { success: false, error };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return new Promise((resolve) => {
|
|
244
|
-
let fullResponse = '';
|
|
245
|
-
|
|
246
|
-
const postData = JSON.stringify({
|
|
247
|
-
model: model,
|
|
248
|
-
messages: [
|
|
249
|
-
{ role: 'user', content: prompt }
|
|
250
|
-
],
|
|
251
|
-
temperature: temperature,
|
|
252
|
-
max_tokens: maxTokens,
|
|
253
|
-
stream: true
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const req = https.request({
|
|
257
|
-
hostname: 'api.groq.com',
|
|
258
|
-
path: '/openai/v1/chat/completions',
|
|
259
|
-
method: 'POST',
|
|
260
|
-
headers: {
|
|
261
|
-
'Content-Type': 'application/json',
|
|
262
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
263
|
-
'Content-Length': Buffer.byteLength(postData)
|
|
264
|
-
}
|
|
265
|
-
}, (res) => {
|
|
266
|
-
let buffer = '';
|
|
267
|
-
let statusCode = res.statusCode;
|
|
268
|
-
|
|
269
|
-
// Check for rate limit or error status codes
|
|
270
|
-
if (statusCode === 429 || statusCode >= 400) {
|
|
271
|
-
let errorBody = '';
|
|
272
|
-
res.on('data', (chunk) => {
|
|
273
|
-
errorBody += chunk.toString();
|
|
274
|
-
});
|
|
275
|
-
res.on('end', () => {
|
|
276
|
-
const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
|
|
277
|
-
this.detectAndSaveRateLimit('groq', model, errorMsg);
|
|
278
|
-
if (onError) onError(errorMsg);
|
|
279
|
-
resolve({ success: false, error: errorMsg });
|
|
280
|
-
});
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
res.on('data', (chunk) => {
|
|
285
|
-
buffer += chunk.toString();
|
|
286
|
-
const lines = buffer.split('\n');
|
|
287
|
-
buffer = lines.pop();
|
|
288
|
-
|
|
289
|
-
for (const line of lines) {
|
|
290
|
-
if (!line.trim() || !line.startsWith('data: ')) continue;
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const jsonStr = line.slice(6);
|
|
294
|
-
if (jsonStr === '[DONE]') {
|
|
295
|
-
if (onComplete) onComplete(fullResponse);
|
|
296
|
-
resolve({
|
|
297
|
-
success: true,
|
|
298
|
-
response: fullResponse,
|
|
299
|
-
model: model
|
|
300
|
-
});
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const data = JSON.parse(jsonStr);
|
|
305
|
-
const content = data.choices?.[0]?.delta?.content;
|
|
306
|
-
|
|
307
|
-
if (content) {
|
|
308
|
-
fullResponse += content;
|
|
309
|
-
if (onChunk) onChunk(content);
|
|
310
|
-
}
|
|
311
|
-
} catch (err) {
|
|
312
|
-
// Ignore JSON parse errors
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
res.on('end', () => {
|
|
318
|
-
if (fullResponse) {
|
|
319
|
-
if (onComplete) onComplete(fullResponse);
|
|
320
|
-
resolve({ success: true, response: fullResponse, model });
|
|
321
|
-
} else {
|
|
322
|
-
const error = buffer || 'No response received from Groq';
|
|
323
|
-
this.detectAndSaveRateLimit('groq', model, error);
|
|
324
|
-
if (onError) onError(error);
|
|
325
|
-
resolve({ success: false, error });
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
req.on('error', (error) => {
|
|
331
|
-
const errorMsg = `Groq API error: ${error.message}`;
|
|
332
|
-
this.detectAndSaveRateLimit('groq', model, errorMsg);
|
|
333
|
-
if (onError) onError(errorMsg);
|
|
334
|
-
resolve({ success: false, error: errorMsg });
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
req.write(postData);
|
|
338
|
-
req.end();
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Call AWS Bedrock API directly
|
|
344
|
-
* @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
|
|
345
|
-
* @param {string} prompt - Prompt to send
|
|
346
|
-
* @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
|
|
347
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
348
|
-
*/
|
|
349
|
-
async callBedrock(model, prompt, options = {}) {
|
|
350
|
-
const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
351
|
-
|
|
352
|
-
if (!region || !accessKeyId || !secretAccessKey) {
|
|
353
|
-
const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
|
|
354
|
-
if (onError) onError(error);
|
|
355
|
-
return { success: false, error };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
// Use AWS SDK v3 for Bedrock
|
|
360
|
-
const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
|
|
361
|
-
|
|
362
|
-
const client = new BedrockRuntimeClient({
|
|
363
|
-
region: region,
|
|
364
|
-
credentials: {
|
|
365
|
-
accessKeyId: accessKeyId,
|
|
366
|
-
secretAccessKey: secretAccessKey
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// Format request based on model provider
|
|
371
|
-
let requestBody;
|
|
372
|
-
if (model.startsWith('anthropic.')) {
|
|
373
|
-
requestBody = {
|
|
374
|
-
anthropic_version: 'bedrock-2023-05-31',
|
|
375
|
-
max_tokens: maxTokens,
|
|
376
|
-
temperature: temperature,
|
|
377
|
-
messages: [
|
|
378
|
-
{ role: 'user', content: prompt }
|
|
379
|
-
]
|
|
380
|
-
};
|
|
381
|
-
} else if (model.startsWith('meta.')) {
|
|
382
|
-
requestBody = {
|
|
383
|
-
prompt: prompt,
|
|
384
|
-
temperature: temperature,
|
|
385
|
-
max_gen_len: maxTokens
|
|
386
|
-
};
|
|
387
|
-
} else {
|
|
388
|
-
return { success: false, error: `Unsupported Bedrock model: ${model}` };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const command = new InvokeModelWithResponseStreamCommand({
|
|
392
|
-
modelId: model,
|
|
393
|
-
contentType: 'application/json',
|
|
394
|
-
accept: 'application/json',
|
|
395
|
-
body: JSON.stringify(requestBody)
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const response = await client.send(command);
|
|
399
|
-
let fullResponse = '';
|
|
400
|
-
|
|
401
|
-
for await (const event of response.body) {
|
|
402
|
-
if (event.chunk) {
|
|
403
|
-
const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
|
|
404
|
-
|
|
405
|
-
let text = '';
|
|
406
|
-
if (chunk.delta?.text) {
|
|
407
|
-
text = chunk.delta.text; // Anthropic format
|
|
408
|
-
} else if (chunk.generation) {
|
|
409
|
-
text = chunk.generation; // Meta Llama format
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (text) {
|
|
413
|
-
fullResponse += text;
|
|
414
|
-
if (onChunk) onChunk(text);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (onComplete) onComplete(fullResponse);
|
|
420
|
-
return { success: true, response: fullResponse, model };
|
|
421
|
-
|
|
422
|
-
} catch (error) {
|
|
423
|
-
const errorMsg = `AWS Bedrock error: ${error.message}`;
|
|
424
|
-
if (onError) onError(errorMsg);
|
|
425
|
-
return { success: false, error: errorMsg };
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Call Claude Code CLI
|
|
431
|
-
* @param {string} model - Model name (ignored, uses Claude Pro subscription)
|
|
432
|
-
* @param {string} prompt - Prompt to send
|
|
433
|
-
* @param {Object} options - Options (onChunk, onComplete, onError)
|
|
434
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
435
|
-
*/
|
|
436
|
-
async callClaudeCode(model, prompt, options = {}) {
|
|
437
|
-
const { onChunk, onComplete, onError } = options;
|
|
438
|
-
const { spawn } = require('child_process');
|
|
439
|
-
|
|
440
|
-
return new Promise((resolve) => {
|
|
441
|
-
let fullResponse = '';
|
|
442
|
-
let errorOutput = '';
|
|
443
|
-
|
|
444
|
-
// Call claude CLI with the prompt
|
|
445
|
-
const claude = spawn('claude', ['--dangerously-skip-permissions'], {
|
|
446
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// Send prompt to stdin
|
|
450
|
-
claude.stdin.write(prompt);
|
|
451
|
-
claude.stdin.end();
|
|
452
|
-
|
|
453
|
-
// Capture stdout
|
|
454
|
-
claude.stdout.on('data', (data) => {
|
|
455
|
-
const chunk = data.toString();
|
|
456
|
-
fullResponse += chunk;
|
|
457
|
-
if (onChunk) onChunk(chunk);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
// Capture stderr
|
|
461
|
-
claude.stderr.on('data', (data) => {
|
|
462
|
-
errorOutput += data.toString();
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Handle completion
|
|
466
|
-
claude.on('close', (code) => {
|
|
467
|
-
if (code === 0) {
|
|
468
|
-
if (onComplete) onComplete(fullResponse);
|
|
469
|
-
resolve({ success: true, response: fullResponse });
|
|
470
|
-
} else {
|
|
471
|
-
const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
|
|
472
|
-
if (onError) onError(error);
|
|
473
|
-
// Check for rate limits
|
|
474
|
-
this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
|
|
475
|
-
resolve({ success: false, error });
|
|
476
|
-
}
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// Handle spawn errors
|
|
480
|
-
claude.on('error', (err) => {
|
|
481
|
-
const error = `Failed to start Claude CLI: ${err.message}`;
|
|
482
|
-
if (onError) onError(error);
|
|
483
|
-
resolve({ success: false, error });
|
|
484
|
-
});
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Call any LLM provider
|
|
490
|
-
* @param {Object} config - Provider configuration
|
|
491
|
-
* @param {string} prompt - Prompt to send
|
|
492
|
-
* @param {Object} options - Options
|
|
493
|
-
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
494
|
-
*/
|
|
495
|
-
async call(config, prompt, options = {}) {
|
|
496
|
-
const { provider, model, apiKey, region, accessKeyId, secretAccessKey } = config;
|
|
497
|
-
|
|
498
|
-
switch (provider) {
|
|
499
|
-
case 'ollama':
|
|
500
|
-
return this.callOllama(model, prompt, options);
|
|
501
|
-
|
|
502
|
-
case 'anthropic':
|
|
503
|
-
return this.callAnthropic(model, prompt, { ...options, apiKey });
|
|
504
|
-
|
|
505
|
-
case 'groq':
|
|
506
|
-
return this.callGroq(model, prompt, { ...options, apiKey });
|
|
507
|
-
|
|
508
|
-
case 'bedrock':
|
|
509
|
-
return this.callBedrock(model, prompt, { ...options, region, accessKeyId, secretAccessKey });
|
|
510
|
-
|
|
511
|
-
case 'claude-code':
|
|
512
|
-
return this.callClaudeCode(model, prompt, options);
|
|
513
|
-
|
|
514
|
-
default:
|
|
515
|
-
return { success: false, error: `Unknown provider: ${provider}` };
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Check if Ollama is available
|
|
521
|
-
* @returns {Promise<boolean>}
|
|
522
|
-
*/
|
|
523
|
-
async isOllamaAvailable() {
|
|
524
|
-
return new Promise((resolve) => {
|
|
525
|
-
const req = http.request({
|
|
526
|
-
hostname: 'localhost',
|
|
527
|
-
port: 11434,
|
|
528
|
-
path: '/api/tags',
|
|
529
|
-
method: 'GET',
|
|
530
|
-
timeout: 2000
|
|
531
|
-
}, (res) => {
|
|
532
|
-
resolve(res.statusCode === 200);
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
req.on('error', () => resolve(false));
|
|
536
|
-
req.on('timeout', () => {
|
|
537
|
-
req.destroy();
|
|
538
|
-
resolve(false);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
req.end();
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Check if Claude Code CLI is available
|
|
547
|
-
* @returns {Promise<boolean>}
|
|
548
|
-
*/
|
|
549
|
-
async isClaudeCodeAvailable() {
|
|
550
|
-
const { spawn } = require('child_process');
|
|
551
|
-
|
|
552
|
-
return new Promise((resolve) => {
|
|
553
|
-
const claude = spawn('claude', ['--version'], {
|
|
554
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
claude.on('close', (code) => {
|
|
558
|
-
resolve(code === 0);
|
|
559
|
-
});
|
|
560
|
-
|
|
561
|
-
claude.on('error', () => {
|
|
562
|
-
resolve(false);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Timeout after 2 seconds
|
|
566
|
-
setTimeout(() => {
|
|
567
|
-
claude.kill();
|
|
568
|
-
resolve(false);
|
|
569
|
-
}, 2000);
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Get list of installed Ollama models
|
|
575
|
-
* @returns {Promise<string[]>}
|
|
576
|
-
*/
|
|
577
|
-
async getOllamaModels() {
|
|
578
|
-
return new Promise((resolve) => {
|
|
579
|
-
const req = http.request({
|
|
580
|
-
hostname: 'localhost',
|
|
581
|
-
port: 11434,
|
|
582
|
-
path: '/api/tags',
|
|
583
|
-
method: 'GET'
|
|
584
|
-
}, (res) => {
|
|
585
|
-
let data = '';
|
|
586
|
-
|
|
587
|
-
res.on('data', (chunk) => {
|
|
588
|
-
data += chunk.toString();
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
res.on('end', () => {
|
|
592
|
-
try {
|
|
593
|
-
const json = JSON.parse(data);
|
|
594
|
-
const models = json.models?.map(m => m.name) || [];
|
|
595
|
-
resolve(models);
|
|
596
|
-
} catch (err) {
|
|
597
|
-
resolve([]);
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
req.on('error', () => resolve([]));
|
|
603
|
-
req.end();
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
module.exports = DirectLLMManager;
|
|
609
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Direct LLM API Manager - Call LLM APIs directly without IDE CLI tools
|
|
3
|
+
* Supports: Ollama (local), Anthropic, Groq, AWS Bedrock
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const https = require('https');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
|
|
9
|
+
class DirectLLMManager {
|
|
10
|
+
constructor(sharedProviderManager = null) {
|
|
11
|
+
this.logger = console;
|
|
12
|
+
// Use shared ProviderManager if provided, otherwise create new instance
|
|
13
|
+
// IMPORTANT: Pass shared instance to maintain rate limit state across calls
|
|
14
|
+
if (sharedProviderManager) {
|
|
15
|
+
this.providerManager = sharedProviderManager;
|
|
16
|
+
} else {
|
|
17
|
+
try {
|
|
18
|
+
const ProviderManager = require('../ide-integration/provider-manager.cjs');
|
|
19
|
+
this.providerManager = new ProviderManager();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
this.providerManager = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Detect and save rate limit from error message
|
|
28
|
+
* @param {string} provider - Provider name
|
|
29
|
+
* @param {string} model - Model name
|
|
30
|
+
* @param {string} errorMessage - Error message from API
|
|
31
|
+
*/
|
|
32
|
+
detectAndSaveRateLimit(provider, model, errorMessage) {
|
|
33
|
+
if (!this.providerManager) return;
|
|
34
|
+
|
|
35
|
+
// Check for rate limit indicators
|
|
36
|
+
const isRateLimit = errorMessage.includes('rate limit') ||
|
|
37
|
+
errorMessage.includes('Rate limit') ||
|
|
38
|
+
errorMessage.includes('too many requests') ||
|
|
39
|
+
errorMessage.includes('429') ||
|
|
40
|
+
errorMessage.includes('quota') ||
|
|
41
|
+
errorMessage.includes('Weekly limit reached') ||
|
|
42
|
+
errorMessage.includes('Daily limit reached') ||
|
|
43
|
+
errorMessage.includes('limit reached');
|
|
44
|
+
|
|
45
|
+
if (isRateLimit) {
|
|
46
|
+
this.providerManager.markRateLimited(provider, model, errorMessage);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Call Ollama API directly (local)
|
|
52
|
+
* @param {string} model - Model name (e.g., "qwen2.5-coder:32b")
|
|
53
|
+
* @param {string} prompt - Prompt to send
|
|
54
|
+
* @param {Object} options - Options (onChunk, onComplete, onError)
|
|
55
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
56
|
+
*/
|
|
57
|
+
async callOllama(model, prompt, options = {}) {
|
|
58
|
+
const { onChunk, onComplete, onError, temperature = 0.2 } = options;
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let fullResponse = '';
|
|
62
|
+
|
|
63
|
+
const postData = JSON.stringify({
|
|
64
|
+
model: model,
|
|
65
|
+
prompt: prompt,
|
|
66
|
+
stream: true,
|
|
67
|
+
options: {
|
|
68
|
+
temperature: temperature
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const req = http.request({
|
|
73
|
+
hostname: 'localhost',
|
|
74
|
+
port: 11434,
|
|
75
|
+
path: '/api/generate',
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
80
|
+
}
|
|
81
|
+
}, (res) => {
|
|
82
|
+
let buffer = '';
|
|
83
|
+
|
|
84
|
+
res.on('data', (chunk) => {
|
|
85
|
+
buffer += chunk.toString();
|
|
86
|
+
const lines = buffer.split('\n');
|
|
87
|
+
buffer = lines.pop(); // Keep incomplete line in buffer
|
|
88
|
+
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
if (!line.trim()) continue;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const data = JSON.parse(line);
|
|
94
|
+
if (data.response) {
|
|
95
|
+
fullResponse += data.response;
|
|
96
|
+
if (onChunk) onChunk(data.response);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (data.done) {
|
|
100
|
+
if (onComplete) onComplete(fullResponse);
|
|
101
|
+
resolve({
|
|
102
|
+
success: true,
|
|
103
|
+
response: fullResponse,
|
|
104
|
+
model: data.model,
|
|
105
|
+
context: data.context
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// Ignore JSON parse errors for partial chunks
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
res.on('end', () => {
|
|
115
|
+
if (!fullResponse) {
|
|
116
|
+
const error = 'No response received from Ollama';
|
|
117
|
+
if (onError) onError(error);
|
|
118
|
+
resolve({ success: false, error });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
req.on('error', (error) => {
|
|
124
|
+
const errorMsg = `Ollama API error: ${error.message}`;
|
|
125
|
+
if (onError) onError(errorMsg);
|
|
126
|
+
resolve({ success: false, error: errorMsg });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
req.write(postData);
|
|
130
|
+
req.end();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Call Anthropic API directly
|
|
136
|
+
* @param {string} model - Model name (e.g., "claude-sonnet-4-20250514")
|
|
137
|
+
* @param {string} prompt - Prompt to send
|
|
138
|
+
* @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
|
|
139
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
140
|
+
*/
|
|
141
|
+
async callAnthropic(model, prompt, options = {}) {
|
|
142
|
+
const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
143
|
+
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
const error = 'Anthropic API key required';
|
|
146
|
+
if (onError) onError(error);
|
|
147
|
+
return { success: false, error };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
let fullResponse = '';
|
|
152
|
+
|
|
153
|
+
const postData = JSON.stringify({
|
|
154
|
+
model: model,
|
|
155
|
+
max_tokens: maxTokens,
|
|
156
|
+
temperature: temperature,
|
|
157
|
+
messages: [
|
|
158
|
+
{ role: 'user', content: prompt }
|
|
159
|
+
],
|
|
160
|
+
stream: true
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const req = https.request({
|
|
164
|
+
hostname: 'api.anthropic.com',
|
|
165
|
+
path: '/v1/messages',
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: {
|
|
168
|
+
'Content-Type': 'application/json',
|
|
169
|
+
'x-api-key': apiKey,
|
|
170
|
+
'anthropic-version': '2023-06-01',
|
|
171
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
172
|
+
}
|
|
173
|
+
}, (res) => {
|
|
174
|
+
let buffer = '';
|
|
175
|
+
|
|
176
|
+
res.on('data', (chunk) => {
|
|
177
|
+
buffer += chunk.toString();
|
|
178
|
+
const lines = buffer.split('\n');
|
|
179
|
+
buffer = lines.pop();
|
|
180
|
+
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (!line.trim() || !line.startsWith('data: ')) continue;
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const jsonStr = line.slice(6); // Remove "data: " prefix
|
|
186
|
+
if (jsonStr === '[DONE]') continue;
|
|
187
|
+
|
|
188
|
+
const data = JSON.parse(jsonStr);
|
|
189
|
+
|
|
190
|
+
if (data.type === 'content_block_delta' && data.delta?.text) {
|
|
191
|
+
fullResponse += data.delta.text;
|
|
192
|
+
if (onChunk) onChunk(data.delta.text);
|
|
193
|
+
} else if (data.type === 'message_stop') {
|
|
194
|
+
if (onComplete) onComplete(fullResponse);
|
|
195
|
+
resolve({
|
|
196
|
+
success: true,
|
|
197
|
+
response: fullResponse,
|
|
198
|
+
model: model
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
// Ignore JSON parse errors
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
res.on('end', () => {
|
|
208
|
+
if (!fullResponse) {
|
|
209
|
+
const error = 'No response received from Anthropic';
|
|
210
|
+
if (onError) onError(error);
|
|
211
|
+
resolve({ success: false, error });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
req.on('error', (error) => {
|
|
217
|
+
const errorMsg = `Anthropic API error: ${error.message}`;
|
|
218
|
+
if (onError) onError(errorMsg);
|
|
219
|
+
resolve({ success: false, error: errorMsg });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
req.write(postData);
|
|
223
|
+
req.end();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Call Groq API directly
|
|
229
|
+
* @param {string} model - Model name (e.g., "llama-3.3-70b-versatile")
|
|
230
|
+
* @param {string} prompt - Prompt to send
|
|
231
|
+
* @param {Object} options - Options (apiKey, onChunk, onComplete, onError)
|
|
232
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
233
|
+
*/
|
|
234
|
+
async callGroq(model, prompt, options = {}) {
|
|
235
|
+
const { apiKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
236
|
+
|
|
237
|
+
if (!apiKey) {
|
|
238
|
+
const error = 'Groq API key required';
|
|
239
|
+
if (onError) onError(error);
|
|
240
|
+
return { success: false, error };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return new Promise((resolve) => {
|
|
244
|
+
let fullResponse = '';
|
|
245
|
+
|
|
246
|
+
const postData = JSON.stringify({
|
|
247
|
+
model: model,
|
|
248
|
+
messages: [
|
|
249
|
+
{ role: 'user', content: prompt }
|
|
250
|
+
],
|
|
251
|
+
temperature: temperature,
|
|
252
|
+
max_tokens: maxTokens,
|
|
253
|
+
stream: true
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const req = https.request({
|
|
257
|
+
hostname: 'api.groq.com',
|
|
258
|
+
path: '/openai/v1/chat/completions',
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
263
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
264
|
+
}
|
|
265
|
+
}, (res) => {
|
|
266
|
+
let buffer = '';
|
|
267
|
+
let statusCode = res.statusCode;
|
|
268
|
+
|
|
269
|
+
// Check for rate limit or error status codes
|
|
270
|
+
if (statusCode === 429 || statusCode >= 400) {
|
|
271
|
+
let errorBody = '';
|
|
272
|
+
res.on('data', (chunk) => {
|
|
273
|
+
errorBody += chunk.toString();
|
|
274
|
+
});
|
|
275
|
+
res.on('end', () => {
|
|
276
|
+
const errorMsg = `Groq API error (${statusCode}): ${errorBody || 'No error details'}`;
|
|
277
|
+
this.detectAndSaveRateLimit('groq', model, errorMsg);
|
|
278
|
+
if (onError) onError(errorMsg);
|
|
279
|
+
resolve({ success: false, error: errorMsg });
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
res.on('data', (chunk) => {
|
|
285
|
+
buffer += chunk.toString();
|
|
286
|
+
const lines = buffer.split('\n');
|
|
287
|
+
buffer = lines.pop();
|
|
288
|
+
|
|
289
|
+
for (const line of lines) {
|
|
290
|
+
if (!line.trim() || !line.startsWith('data: ')) continue;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const jsonStr = line.slice(6);
|
|
294
|
+
if (jsonStr === '[DONE]') {
|
|
295
|
+
if (onComplete) onComplete(fullResponse);
|
|
296
|
+
resolve({
|
|
297
|
+
success: true,
|
|
298
|
+
response: fullResponse,
|
|
299
|
+
model: model
|
|
300
|
+
});
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const data = JSON.parse(jsonStr);
|
|
305
|
+
const content = data.choices?.[0]?.delta?.content;
|
|
306
|
+
|
|
307
|
+
if (content) {
|
|
308
|
+
fullResponse += content;
|
|
309
|
+
if (onChunk) onChunk(content);
|
|
310
|
+
}
|
|
311
|
+
} catch (err) {
|
|
312
|
+
// Ignore JSON parse errors
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
res.on('end', () => {
|
|
318
|
+
if (fullResponse) {
|
|
319
|
+
if (onComplete) onComplete(fullResponse);
|
|
320
|
+
resolve({ success: true, response: fullResponse, model });
|
|
321
|
+
} else {
|
|
322
|
+
const error = buffer || 'No response received from Groq';
|
|
323
|
+
this.detectAndSaveRateLimit('groq', model, error);
|
|
324
|
+
if (onError) onError(error);
|
|
325
|
+
resolve({ success: false, error });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
req.on('error', (error) => {
|
|
331
|
+
const errorMsg = `Groq API error: ${error.message}`;
|
|
332
|
+
this.detectAndSaveRateLimit('groq', model, errorMsg);
|
|
333
|
+
if (onError) onError(errorMsg);
|
|
334
|
+
resolve({ success: false, error: errorMsg });
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
req.write(postData);
|
|
338
|
+
req.end();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Call AWS Bedrock API directly
|
|
344
|
+
* @param {string} model - Model ID (e.g., "anthropic.claude-sonnet-4-v1")
|
|
345
|
+
* @param {string} prompt - Prompt to send
|
|
346
|
+
* @param {Object} options - Options (region, accessKeyId, secretAccessKey, onChunk, onComplete, onError)
|
|
347
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
348
|
+
*/
|
|
349
|
+
async callBedrock(model, prompt, options = {}) {
|
|
350
|
+
const { region, accessKeyId, secretAccessKey, onChunk, onComplete, onError, temperature = 0.2, maxTokens = 8192 } = options;
|
|
351
|
+
|
|
352
|
+
if (!region || !accessKeyId || !secretAccessKey) {
|
|
353
|
+
const error = 'AWS credentials required (region, accessKeyId, secretAccessKey)';
|
|
354
|
+
if (onError) onError(error);
|
|
355
|
+
return { success: false, error };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
// Use AWS SDK v3 for Bedrock
|
|
360
|
+
const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require('@aws-sdk/client-bedrock-runtime');
|
|
361
|
+
|
|
362
|
+
const client = new BedrockRuntimeClient({
|
|
363
|
+
region: region,
|
|
364
|
+
credentials: {
|
|
365
|
+
accessKeyId: accessKeyId,
|
|
366
|
+
secretAccessKey: secretAccessKey
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Format request based on model provider
|
|
371
|
+
let requestBody;
|
|
372
|
+
if (model.startsWith('anthropic.')) {
|
|
373
|
+
requestBody = {
|
|
374
|
+
anthropic_version: 'bedrock-2023-05-31',
|
|
375
|
+
max_tokens: maxTokens,
|
|
376
|
+
temperature: temperature,
|
|
377
|
+
messages: [
|
|
378
|
+
{ role: 'user', content: prompt }
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
} else if (model.startsWith('meta.')) {
|
|
382
|
+
requestBody = {
|
|
383
|
+
prompt: prompt,
|
|
384
|
+
temperature: temperature,
|
|
385
|
+
max_gen_len: maxTokens
|
|
386
|
+
};
|
|
387
|
+
} else {
|
|
388
|
+
return { success: false, error: `Unsupported Bedrock model: ${model}` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const command = new InvokeModelWithResponseStreamCommand({
|
|
392
|
+
modelId: model,
|
|
393
|
+
contentType: 'application/json',
|
|
394
|
+
accept: 'application/json',
|
|
395
|
+
body: JSON.stringify(requestBody)
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const response = await client.send(command);
|
|
399
|
+
let fullResponse = '';
|
|
400
|
+
|
|
401
|
+
for await (const event of response.body) {
|
|
402
|
+
if (event.chunk) {
|
|
403
|
+
const chunk = JSON.parse(new TextDecoder().decode(event.chunk.bytes));
|
|
404
|
+
|
|
405
|
+
let text = '';
|
|
406
|
+
if (chunk.delta?.text) {
|
|
407
|
+
text = chunk.delta.text; // Anthropic format
|
|
408
|
+
} else if (chunk.generation) {
|
|
409
|
+
text = chunk.generation; // Meta Llama format
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (text) {
|
|
413
|
+
fullResponse += text;
|
|
414
|
+
if (onChunk) onChunk(text);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (onComplete) onComplete(fullResponse);
|
|
420
|
+
return { success: true, response: fullResponse, model };
|
|
421
|
+
|
|
422
|
+
} catch (error) {
|
|
423
|
+
const errorMsg = `AWS Bedrock error: ${error.message}`;
|
|
424
|
+
if (onError) onError(errorMsg);
|
|
425
|
+
return { success: false, error: errorMsg };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Call Claude Code CLI
|
|
431
|
+
* @param {string} model - Model name (ignored, uses Claude Pro subscription)
|
|
432
|
+
* @param {string} prompt - Prompt to send
|
|
433
|
+
* @param {Object} options - Options (onChunk, onComplete, onError)
|
|
434
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
435
|
+
*/
|
|
436
|
+
async callClaudeCode(model, prompt, options = {}) {
|
|
437
|
+
const { onChunk, onComplete, onError } = options;
|
|
438
|
+
const { spawn } = require('child_process');
|
|
439
|
+
|
|
440
|
+
return new Promise((resolve) => {
|
|
441
|
+
let fullResponse = '';
|
|
442
|
+
let errorOutput = '';
|
|
443
|
+
|
|
444
|
+
// Call claude CLI with the prompt
|
|
445
|
+
const claude = spawn('claude', ['--dangerously-skip-permissions'], {
|
|
446
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Send prompt to stdin
|
|
450
|
+
claude.stdin.write(prompt);
|
|
451
|
+
claude.stdin.end();
|
|
452
|
+
|
|
453
|
+
// Capture stdout
|
|
454
|
+
claude.stdout.on('data', (data) => {
|
|
455
|
+
const chunk = data.toString();
|
|
456
|
+
fullResponse += chunk;
|
|
457
|
+
if (onChunk) onChunk(chunk);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Capture stderr
|
|
461
|
+
claude.stderr.on('data', (data) => {
|
|
462
|
+
errorOutput += data.toString();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Handle completion
|
|
466
|
+
claude.on('close', (code) => {
|
|
467
|
+
if (code === 0) {
|
|
468
|
+
if (onComplete) onComplete(fullResponse);
|
|
469
|
+
resolve({ success: true, response: fullResponse });
|
|
470
|
+
} else {
|
|
471
|
+
const error = `Claude CLI exited with code ${code}: ${errorOutput}`;
|
|
472
|
+
if (onError) onError(error);
|
|
473
|
+
// Check for rate limits
|
|
474
|
+
this.detectAndSaveRateLimit('claude-code', 'claude-code-cli', errorOutput);
|
|
475
|
+
resolve({ success: false, error });
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Handle spawn errors
|
|
480
|
+
claude.on('error', (err) => {
|
|
481
|
+
const error = `Failed to start Claude CLI: ${err.message}`;
|
|
482
|
+
if (onError) onError(error);
|
|
483
|
+
resolve({ success: false, error });
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Call any LLM provider
|
|
490
|
+
* @param {Object} config - Provider configuration
|
|
491
|
+
* @param {string} prompt - Prompt to send
|
|
492
|
+
* @param {Object} options - Options
|
|
493
|
+
* @returns {Promise<{success: boolean, response?: string, error?: string}>}
|
|
494
|
+
*/
|
|
495
|
+
async call(config, prompt, options = {}) {
|
|
496
|
+
const { provider, model, apiKey, region, accessKeyId, secretAccessKey } = config;
|
|
497
|
+
|
|
498
|
+
switch (provider) {
|
|
499
|
+
case 'ollama':
|
|
500
|
+
return this.callOllama(model, prompt, options);
|
|
501
|
+
|
|
502
|
+
case 'anthropic':
|
|
503
|
+
return this.callAnthropic(model, prompt, { ...options, apiKey });
|
|
504
|
+
|
|
505
|
+
case 'groq':
|
|
506
|
+
return this.callGroq(model, prompt, { ...options, apiKey });
|
|
507
|
+
|
|
508
|
+
case 'bedrock':
|
|
509
|
+
return this.callBedrock(model, prompt, { ...options, region, accessKeyId, secretAccessKey });
|
|
510
|
+
|
|
511
|
+
case 'claude-code':
|
|
512
|
+
return this.callClaudeCode(model, prompt, options);
|
|
513
|
+
|
|
514
|
+
default:
|
|
515
|
+
return { success: false, error: `Unknown provider: ${provider}` };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check if Ollama is available
|
|
521
|
+
* @returns {Promise<boolean>}
|
|
522
|
+
*/
|
|
523
|
+
async isOllamaAvailable() {
|
|
524
|
+
return new Promise((resolve) => {
|
|
525
|
+
const req = http.request({
|
|
526
|
+
hostname: 'localhost',
|
|
527
|
+
port: 11434,
|
|
528
|
+
path: '/api/tags',
|
|
529
|
+
method: 'GET',
|
|
530
|
+
timeout: 2000
|
|
531
|
+
}, (res) => {
|
|
532
|
+
resolve(res.statusCode === 200);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
req.on('error', () => resolve(false));
|
|
536
|
+
req.on('timeout', () => {
|
|
537
|
+
req.destroy();
|
|
538
|
+
resolve(false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
req.end();
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Check if Claude Code CLI is available
|
|
547
|
+
* @returns {Promise<boolean>}
|
|
548
|
+
*/
|
|
549
|
+
async isClaudeCodeAvailable() {
|
|
550
|
+
const { spawn } = require('child_process');
|
|
551
|
+
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
const claude = spawn('claude', ['--version'], {
|
|
554
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
claude.on('close', (code) => {
|
|
558
|
+
resolve(code === 0);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
claude.on('error', () => {
|
|
562
|
+
resolve(false);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Timeout after 2 seconds
|
|
566
|
+
setTimeout(() => {
|
|
567
|
+
claude.kill();
|
|
568
|
+
resolve(false);
|
|
569
|
+
}, 2000);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get list of installed Ollama models
|
|
575
|
+
* @returns {Promise<string[]>}
|
|
576
|
+
*/
|
|
577
|
+
async getOllamaModels() {
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
const req = http.request({
|
|
580
|
+
hostname: 'localhost',
|
|
581
|
+
port: 11434,
|
|
582
|
+
path: '/api/tags',
|
|
583
|
+
method: 'GET'
|
|
584
|
+
}, (res) => {
|
|
585
|
+
let data = '';
|
|
586
|
+
|
|
587
|
+
res.on('data', (chunk) => {
|
|
588
|
+
data += chunk.toString();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
res.on('end', () => {
|
|
592
|
+
try {
|
|
593
|
+
const json = JSON.parse(data);
|
|
594
|
+
const models = json.models?.map(m => m.name) || [];
|
|
595
|
+
resolve(models);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
resolve([]);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
req.on('error', () => resolve([]));
|
|
603
|
+
req.end();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
module.exports = DirectLLMManager;
|
|
609
|
+
|