sessioncast-cli 2.2.1 → 2.3.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.
- package/dist/agent/api-client.js +17 -3
- package/dist/agent/llm-service.d.ts +16 -0
- package/dist/agent/llm-service.js +790 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/index.js +0 -0
- package/package.json +1 -1
package/dist/agent/api-client.js
CHANGED
|
@@ -170,9 +170,23 @@ class ApiWebSocketClient {
|
|
|
170
170
|
try {
|
|
171
171
|
const payload = meta.payload ? JSON.parse(meta.payload) : {};
|
|
172
172
|
const { model, messages, temperature, max_tokens, stream } = payload;
|
|
173
|
-
console.log(`[API] llm_chat: model=${model}, messages=${messages?.length || 0}`);
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
console.log(`[API] llm_chat: model=${model}, messages=${messages?.length || 0}, stream=${!!stream}`);
|
|
174
|
+
if (stream) {
|
|
175
|
+
const result = await this.llmService.chatStream(model, messages, temperature, max_tokens, (chunk) => {
|
|
176
|
+
this.send({
|
|
177
|
+
type: 'api_response_stream',
|
|
178
|
+
meta: {
|
|
179
|
+
requestId: meta.requestId,
|
|
180
|
+
payload: JSON.stringify(chunk)
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
this.sendApiResponse(meta.requestId, result);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const result = await this.llmService.chat(model, messages, temperature, max_tokens, stream);
|
|
188
|
+
this.sendApiResponse(meta.requestId, result);
|
|
189
|
+
}
|
|
176
190
|
}
|
|
177
191
|
catch (error) {
|
|
178
192
|
this.sendApiResponse(meta.requestId, {
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { LlmConfig, LlmMessage, LlmResponse } from './types';
|
|
2
|
+
export type StreamChunk = {
|
|
3
|
+
content: string;
|
|
4
|
+
done: boolean;
|
|
5
|
+
};
|
|
2
6
|
export declare class LlmService {
|
|
3
7
|
private config;
|
|
4
8
|
constructor(config?: LlmConfig);
|
|
5
9
|
chat(model?: string, messages?: LlmMessage[], temperature?: number, maxTokens?: number, stream?: boolean): Promise<LlmResponse>;
|
|
10
|
+
chatStream(model?: string, messages?: LlmMessage[], temperature?: number, maxTokens?: number, onChunk?: (chunk: StreamChunk) => void): Promise<LlmResponse>;
|
|
6
11
|
private callOllama;
|
|
7
12
|
private convertOllamaToOpenAiFormat;
|
|
8
13
|
private callOpenAi;
|
|
14
|
+
private callAnthropic;
|
|
15
|
+
private callClaudeCode;
|
|
16
|
+
private callCodex;
|
|
17
|
+
private callGemini;
|
|
18
|
+
private convertGeminiToOpenAiFormat;
|
|
19
|
+
private callCursor;
|
|
20
|
+
private callClaudeCodeStream;
|
|
21
|
+
private callOllamaStream;
|
|
22
|
+
private callOpenAiStream;
|
|
23
|
+
private callAnthropicStream;
|
|
24
|
+
private convertAnthropicToOpenAiFormat;
|
|
9
25
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LlmService = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const readline_1 = require("readline");
|
|
4
6
|
class LlmService {
|
|
5
7
|
constructor(config) {
|
|
6
8
|
this.config = config || {
|
|
@@ -32,6 +34,16 @@ class LlmService {
|
|
|
32
34
|
return await this.callOllama(actualModel, messages || [], temperature, maxTokens);
|
|
33
35
|
case 'openai':
|
|
34
36
|
return await this.callOpenAi(actualModel, messages || [], temperature, maxTokens);
|
|
37
|
+
case 'anthropic':
|
|
38
|
+
return await this.callAnthropic(actualModel, messages || [], temperature, maxTokens);
|
|
39
|
+
case 'claude-code':
|
|
40
|
+
return await this.callClaudeCode(actualModel, messages || [], maxTokens);
|
|
41
|
+
case 'codex':
|
|
42
|
+
return await this.callCodex(actualModel, messages || [], maxTokens);
|
|
43
|
+
case 'gemini':
|
|
44
|
+
return await this.callGemini(actualModel, messages || [], temperature, maxTokens);
|
|
45
|
+
case 'cursor':
|
|
46
|
+
return await this.callCursor(actualModel, messages || [], maxTokens);
|
|
35
47
|
default:
|
|
36
48
|
return {
|
|
37
49
|
id: '',
|
|
@@ -60,6 +72,42 @@ class LlmService {
|
|
|
60
72
|
};
|
|
61
73
|
}
|
|
62
74
|
}
|
|
75
|
+
async chatStream(model, messages, temperature, maxTokens, onChunk) {
|
|
76
|
+
if (!this.config.enabled || !onChunk) {
|
|
77
|
+
return this.chat(model, messages, temperature, maxTokens);
|
|
78
|
+
}
|
|
79
|
+
const provider = this.config.provider || 'ollama';
|
|
80
|
+
const actualModel = model || this.config.model;
|
|
81
|
+
try {
|
|
82
|
+
switch (provider.toLowerCase()) {
|
|
83
|
+
case 'claude-code':
|
|
84
|
+
return await this.callClaudeCodeStream(actualModel, messages || [], maxTokens, onChunk);
|
|
85
|
+
case 'ollama':
|
|
86
|
+
return await this.callOllamaStream(actualModel, messages || [], temperature, maxTokens, onChunk);
|
|
87
|
+
case 'openai':
|
|
88
|
+
return await this.callOpenAiStream(actualModel, messages || [], temperature, maxTokens, onChunk);
|
|
89
|
+
case 'anthropic':
|
|
90
|
+
return await this.callAnthropicStream(actualModel, messages || [], temperature, maxTokens, onChunk);
|
|
91
|
+
default:
|
|
92
|
+
// For providers without streaming, fall back to non-streaming
|
|
93
|
+
const result = await this.chat(model, messages, temperature, maxTokens);
|
|
94
|
+
if (result.choices?.[0]?.message?.content) {
|
|
95
|
+
onChunk({ content: result.choices[0].message.content, done: true });
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
id: '',
|
|
103
|
+
object: 'error',
|
|
104
|
+
created: Date.now(),
|
|
105
|
+
model: '',
|
|
106
|
+
choices: [],
|
|
107
|
+
error: { message: error.message, type: 'internal_error' }
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
63
111
|
async callOllama(model, messages, temperature, maxTokens) {
|
|
64
112
|
const baseUrl = this.config.baseUrl || 'http://localhost:11434';
|
|
65
113
|
const requestBody = {
|
|
@@ -152,5 +200,747 @@ class LlmService {
|
|
|
152
200
|
}
|
|
153
201
|
return await response.json();
|
|
154
202
|
}
|
|
203
|
+
async callAnthropic(model, messages, temperature, maxTokens) {
|
|
204
|
+
const baseUrl = this.config.baseUrl || 'https://api.anthropic.com';
|
|
205
|
+
const apiKey = this.config.apiKey;
|
|
206
|
+
if (!apiKey) {
|
|
207
|
+
return {
|
|
208
|
+
id: '',
|
|
209
|
+
object: 'error',
|
|
210
|
+
created: Date.now(),
|
|
211
|
+
model: '',
|
|
212
|
+
choices: [],
|
|
213
|
+
error: {
|
|
214
|
+
message: 'Anthropic API key not configured',
|
|
215
|
+
type: 'configuration_error'
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Separate system message from user/assistant messages
|
|
220
|
+
let systemPrompt;
|
|
221
|
+
const chatMessages = [];
|
|
222
|
+
for (const msg of messages) {
|
|
223
|
+
if (msg.role === 'system') {
|
|
224
|
+
systemPrompt = msg.content;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
chatMessages.push({ role: msg.role, content: msg.content });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const requestBody = {
|
|
231
|
+
model,
|
|
232
|
+
messages: chatMessages,
|
|
233
|
+
max_tokens: maxTokens || 1024
|
|
234
|
+
};
|
|
235
|
+
if (systemPrompt) {
|
|
236
|
+
requestBody.system = systemPrompt;
|
|
237
|
+
}
|
|
238
|
+
if (temperature !== undefined) {
|
|
239
|
+
requestBody.temperature = temperature;
|
|
240
|
+
}
|
|
241
|
+
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
'x-api-key': apiKey,
|
|
246
|
+
'anthropic-version': '2023-06-01'
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify(requestBody)
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
const errorBody = await response.text();
|
|
252
|
+
throw new Error(`Anthropic returned status ${response.status}: ${errorBody}`);
|
|
253
|
+
}
|
|
254
|
+
const anthropicResponse = await response.json();
|
|
255
|
+
return this.convertAnthropicToOpenAiFormat(anthropicResponse, model);
|
|
256
|
+
}
|
|
257
|
+
async callClaudeCode(model, messages, maxTokens) {
|
|
258
|
+
// Build prompt from messages
|
|
259
|
+
const parts = [];
|
|
260
|
+
for (const msg of messages) {
|
|
261
|
+
if (msg.role === 'system') {
|
|
262
|
+
parts.push(`[System] ${msg.content}`);
|
|
263
|
+
}
|
|
264
|
+
else if (msg.role === 'user') {
|
|
265
|
+
parts.push(msg.content);
|
|
266
|
+
}
|
|
267
|
+
else if (msg.role === 'assistant') {
|
|
268
|
+
parts.push(`[Assistant] ${msg.content}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const prompt = parts.join('\n\n');
|
|
272
|
+
const args = ['-p', '--output-format', 'json', '--no-session-persistence'];
|
|
273
|
+
// Map model names: 'sonnet', 'opus', 'haiku' or full model ID
|
|
274
|
+
if (model && model !== 'default') {
|
|
275
|
+
args.push('--model', model);
|
|
276
|
+
}
|
|
277
|
+
return new Promise((resolve) => {
|
|
278
|
+
const env = { ...process.env };
|
|
279
|
+
delete env.CLAUDECODE;
|
|
280
|
+
delete env.CLAUDE_CODE;
|
|
281
|
+
const proc = (0, child_process_1.spawn)('claude', args, {
|
|
282
|
+
env,
|
|
283
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
284
|
+
});
|
|
285
|
+
// Send prompt via stdin
|
|
286
|
+
proc.stdin.write(prompt);
|
|
287
|
+
proc.stdin.end();
|
|
288
|
+
let stdout = '';
|
|
289
|
+
let stderr = '';
|
|
290
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
291
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
292
|
+
const timer = setTimeout(() => {
|
|
293
|
+
proc.kill();
|
|
294
|
+
resolve({
|
|
295
|
+
id: '',
|
|
296
|
+
object: 'error',
|
|
297
|
+
created: Math.floor(Date.now() / 1000),
|
|
298
|
+
model,
|
|
299
|
+
choices: [],
|
|
300
|
+
error: { message: 'Claude Code timed out after 120s', type: 'timeout' }
|
|
301
|
+
});
|
|
302
|
+
}, 120000);
|
|
303
|
+
proc.on('close', (code) => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
if (code !== 0) {
|
|
306
|
+
resolve({
|
|
307
|
+
id: '',
|
|
308
|
+
object: 'error',
|
|
309
|
+
created: Math.floor(Date.now() / 1000),
|
|
310
|
+
model,
|
|
311
|
+
choices: [],
|
|
312
|
+
error: {
|
|
313
|
+
message: `Claude Code exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
314
|
+
type: 'internal_error'
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
// claude --output-format json returns a JSON array of events
|
|
321
|
+
// Find the "result" event which contains the final response
|
|
322
|
+
const events = JSON.parse(stdout);
|
|
323
|
+
const resultEvent = Array.isArray(events)
|
|
324
|
+
? events.find((e) => e.type === 'result')
|
|
325
|
+
: events;
|
|
326
|
+
const content = resultEvent?.result || '';
|
|
327
|
+
const sessionId = resultEvent?.session_id || '';
|
|
328
|
+
const usage = resultEvent?.usage || {};
|
|
329
|
+
resolve({
|
|
330
|
+
id: sessionId || `claude-${Math.random().toString(36).substring(2, 10)}`,
|
|
331
|
+
object: 'chat.completion',
|
|
332
|
+
created: Math.floor(Date.now() / 1000),
|
|
333
|
+
model: model || 'claude-code',
|
|
334
|
+
choices: [{
|
|
335
|
+
index: 0,
|
|
336
|
+
message: {
|
|
337
|
+
role: 'assistant',
|
|
338
|
+
content
|
|
339
|
+
},
|
|
340
|
+
finish_reason: 'stop'
|
|
341
|
+
}],
|
|
342
|
+
usage: {
|
|
343
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
344
|
+
completion_tokens: usage.output_tokens || 0,
|
|
345
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (parseError) {
|
|
350
|
+
// Fallback: treat stdout as plain text
|
|
351
|
+
resolve({
|
|
352
|
+
id: `claude-${Math.random().toString(36).substring(2, 10)}`,
|
|
353
|
+
object: 'chat.completion',
|
|
354
|
+
created: Math.floor(Date.now() / 1000),
|
|
355
|
+
model: model || 'claude-code',
|
|
356
|
+
choices: [{
|
|
357
|
+
index: 0,
|
|
358
|
+
message: {
|
|
359
|
+
role: 'assistant',
|
|
360
|
+
content: stdout.trim()
|
|
361
|
+
},
|
|
362
|
+
finish_reason: 'stop'
|
|
363
|
+
}],
|
|
364
|
+
usage: {
|
|
365
|
+
prompt_tokens: 0,
|
|
366
|
+
completion_tokens: 0,
|
|
367
|
+
total_tokens: 0
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async callCodex(model, messages, maxTokens) {
|
|
375
|
+
const parts = [];
|
|
376
|
+
for (const msg of messages) {
|
|
377
|
+
if (msg.role === 'system') {
|
|
378
|
+
parts.push(`[System] ${msg.content}`);
|
|
379
|
+
}
|
|
380
|
+
else if (msg.role === 'user') {
|
|
381
|
+
parts.push(msg.content);
|
|
382
|
+
}
|
|
383
|
+
else if (msg.role === 'assistant') {
|
|
384
|
+
parts.push(`[Assistant] ${msg.content}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const prompt = parts.join('\n\n');
|
|
388
|
+
const args = ['exec', '--json'];
|
|
389
|
+
if (model && model !== 'default') {
|
|
390
|
+
args.push('-c', `model="${model}"`);
|
|
391
|
+
}
|
|
392
|
+
return new Promise((resolve) => {
|
|
393
|
+
const proc = (0, child_process_1.spawn)('codex', args, {
|
|
394
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
395
|
+
});
|
|
396
|
+
proc.stdin.write(prompt);
|
|
397
|
+
proc.stdin.end();
|
|
398
|
+
let stdout = '';
|
|
399
|
+
let stderr = '';
|
|
400
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
401
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
402
|
+
const timer = setTimeout(() => {
|
|
403
|
+
proc.kill();
|
|
404
|
+
resolve({
|
|
405
|
+
id: '',
|
|
406
|
+
object: 'error',
|
|
407
|
+
created: Math.floor(Date.now() / 1000),
|
|
408
|
+
model,
|
|
409
|
+
choices: [],
|
|
410
|
+
error: { message: 'Codex CLI timed out after 120s', type: 'timeout' }
|
|
411
|
+
});
|
|
412
|
+
}, 120000);
|
|
413
|
+
proc.on('close', (code) => {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
if (code !== 0) {
|
|
416
|
+
resolve({
|
|
417
|
+
id: '',
|
|
418
|
+
object: 'error',
|
|
419
|
+
created: Math.floor(Date.now() / 1000),
|
|
420
|
+
model,
|
|
421
|
+
choices: [],
|
|
422
|
+
error: {
|
|
423
|
+
message: `Codex CLI exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
424
|
+
type: 'internal_error'
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
// codex exec --json outputs JSONL events, find the last assistant message
|
|
431
|
+
const lines = stdout.trim().split('\n');
|
|
432
|
+
let content = '';
|
|
433
|
+
let usage = { input_tokens: 0, output_tokens: 0 };
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
try {
|
|
436
|
+
const event = JSON.parse(line);
|
|
437
|
+
if (event.type === 'message' && event.role === 'assistant') {
|
|
438
|
+
content = typeof event.content === 'string'
|
|
439
|
+
? event.content
|
|
440
|
+
: (event.content || []).filter((c) => c.type === 'output_text').map((c) => c.text).join('');
|
|
441
|
+
}
|
|
442
|
+
if (event.usage) {
|
|
443
|
+
usage = event.usage;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch { /* skip non-JSON lines */ }
|
|
447
|
+
}
|
|
448
|
+
resolve({
|
|
449
|
+
id: `codex-${Math.random().toString(36).substring(2, 10)}`,
|
|
450
|
+
object: 'chat.completion',
|
|
451
|
+
created: Math.floor(Date.now() / 1000),
|
|
452
|
+
model: model || 'codex',
|
|
453
|
+
choices: [{
|
|
454
|
+
index: 0,
|
|
455
|
+
message: { role: 'assistant', content: content || stdout.trim() },
|
|
456
|
+
finish_reason: 'stop'
|
|
457
|
+
}],
|
|
458
|
+
usage: {
|
|
459
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
460
|
+
completion_tokens: usage.output_tokens || 0,
|
|
461
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
resolve({
|
|
467
|
+
id: `codex-${Math.random().toString(36).substring(2, 10)}`,
|
|
468
|
+
object: 'chat.completion',
|
|
469
|
+
created: Math.floor(Date.now() / 1000),
|
|
470
|
+
model: model || 'codex',
|
|
471
|
+
choices: [{
|
|
472
|
+
index: 0,
|
|
473
|
+
message: { role: 'assistant', content: stdout.trim() },
|
|
474
|
+
finish_reason: 'stop'
|
|
475
|
+
}],
|
|
476
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async callGemini(model, messages, temperature, maxTokens) {
|
|
483
|
+
const apiKey = this.config.apiKey;
|
|
484
|
+
if (!apiKey) {
|
|
485
|
+
return {
|
|
486
|
+
id: '',
|
|
487
|
+
object: 'error',
|
|
488
|
+
created: Date.now(),
|
|
489
|
+
model: '',
|
|
490
|
+
choices: [],
|
|
491
|
+
error: {
|
|
492
|
+
message: 'Gemini API key not configured',
|
|
493
|
+
type: 'configuration_error'
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const actualModel = model || 'gemini-2.0-flash';
|
|
498
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${actualModel}:generateContent?key=${apiKey}`;
|
|
499
|
+
// Separate system message from conversation
|
|
500
|
+
let systemInstruction;
|
|
501
|
+
const contents = [];
|
|
502
|
+
for (const msg of messages) {
|
|
503
|
+
if (msg.role === 'system') {
|
|
504
|
+
systemInstruction = msg.content;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
contents.push({
|
|
508
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
509
|
+
parts: [{ text: msg.content }]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const requestBody = { contents };
|
|
514
|
+
if (systemInstruction) {
|
|
515
|
+
requestBody.systemInstruction = { parts: [{ text: systemInstruction }] };
|
|
516
|
+
}
|
|
517
|
+
const generationConfig = {};
|
|
518
|
+
if (maxTokens)
|
|
519
|
+
generationConfig.maxOutputTokens = maxTokens;
|
|
520
|
+
if (temperature !== undefined)
|
|
521
|
+
generationConfig.temperature = temperature;
|
|
522
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
523
|
+
requestBody.generationConfig = generationConfig;
|
|
524
|
+
}
|
|
525
|
+
const response = await fetch(url, {
|
|
526
|
+
method: 'POST',
|
|
527
|
+
headers: { 'Content-Type': 'application/json' },
|
|
528
|
+
body: JSON.stringify(requestBody)
|
|
529
|
+
});
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
const errorBody = await response.text();
|
|
532
|
+
throw new Error(`Gemini returned status ${response.status}: ${errorBody}`);
|
|
533
|
+
}
|
|
534
|
+
const geminiResponse = await response.json();
|
|
535
|
+
return this.convertGeminiToOpenAiFormat(geminiResponse, actualModel);
|
|
536
|
+
}
|
|
537
|
+
convertGeminiToOpenAiFormat(geminiResponse, model) {
|
|
538
|
+
const candidate = (geminiResponse.candidates || [])[0] || {};
|
|
539
|
+
const content = (candidate.content?.parts || [])
|
|
540
|
+
.map((p) => p.text || '')
|
|
541
|
+
.join('');
|
|
542
|
+
const usage = geminiResponse.usageMetadata || {};
|
|
543
|
+
return {
|
|
544
|
+
id: `gemini-${Math.random().toString(36).substring(2, 10)}`,
|
|
545
|
+
object: 'chat.completion',
|
|
546
|
+
created: Math.floor(Date.now() / 1000),
|
|
547
|
+
model,
|
|
548
|
+
choices: [{
|
|
549
|
+
index: 0,
|
|
550
|
+
message: { role: 'assistant', content },
|
|
551
|
+
finish_reason: candidate.finishReason === 'STOP' ? 'stop' : (candidate.finishReason || 'stop')
|
|
552
|
+
}],
|
|
553
|
+
usage: {
|
|
554
|
+
prompt_tokens: usage.promptTokenCount || 0,
|
|
555
|
+
completion_tokens: usage.candidatesTokenCount || 0,
|
|
556
|
+
total_tokens: usage.totalTokenCount || 0
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async callCursor(model, messages, maxTokens) {
|
|
561
|
+
const parts = [];
|
|
562
|
+
for (const msg of messages) {
|
|
563
|
+
if (msg.role === 'system') {
|
|
564
|
+
parts.push(`[System] ${msg.content}`);
|
|
565
|
+
}
|
|
566
|
+
else if (msg.role === 'user') {
|
|
567
|
+
parts.push(msg.content);
|
|
568
|
+
}
|
|
569
|
+
else if (msg.role === 'assistant') {
|
|
570
|
+
parts.push(`[Assistant] ${msg.content}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const prompt = parts.join('\n\n');
|
|
574
|
+
const args = ['-p', '--output-format', 'json'];
|
|
575
|
+
if (model && model !== 'default') {
|
|
576
|
+
args.push('--model', model);
|
|
577
|
+
}
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
const proc = (0, child_process_1.spawn)('cursor', args, {
|
|
580
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
581
|
+
});
|
|
582
|
+
proc.stdin.write(prompt);
|
|
583
|
+
proc.stdin.end();
|
|
584
|
+
let stdout = '';
|
|
585
|
+
let stderr = '';
|
|
586
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
587
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
proc.kill();
|
|
590
|
+
resolve({
|
|
591
|
+
id: '',
|
|
592
|
+
object: 'error',
|
|
593
|
+
created: Math.floor(Date.now() / 1000),
|
|
594
|
+
model,
|
|
595
|
+
choices: [],
|
|
596
|
+
error: { message: 'Cursor CLI timed out after 120s', type: 'timeout' }
|
|
597
|
+
});
|
|
598
|
+
}, 120000);
|
|
599
|
+
proc.on('close', (code) => {
|
|
600
|
+
clearTimeout(timer);
|
|
601
|
+
if (code !== 0) {
|
|
602
|
+
resolve({
|
|
603
|
+
id: '',
|
|
604
|
+
object: 'error',
|
|
605
|
+
created: Math.floor(Date.now() / 1000),
|
|
606
|
+
model,
|
|
607
|
+
choices: [],
|
|
608
|
+
error: {
|
|
609
|
+
message: `Cursor CLI exited with code ${code}${stderr ? `: ${stderr.trim()}` : ''}`,
|
|
610
|
+
type: 'internal_error'
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const events = JSON.parse(stdout);
|
|
617
|
+
const resultEvent = Array.isArray(events)
|
|
618
|
+
? events.find((e) => e.type === 'result')
|
|
619
|
+
: events;
|
|
620
|
+
const content = resultEvent?.result || '';
|
|
621
|
+
const usage = resultEvent?.usage || {};
|
|
622
|
+
resolve({
|
|
623
|
+
id: `cursor-${Math.random().toString(36).substring(2, 10)}`,
|
|
624
|
+
object: 'chat.completion',
|
|
625
|
+
created: Math.floor(Date.now() / 1000),
|
|
626
|
+
model: model || 'cursor',
|
|
627
|
+
choices: [{
|
|
628
|
+
index: 0,
|
|
629
|
+
message: { role: 'assistant', content },
|
|
630
|
+
finish_reason: 'stop'
|
|
631
|
+
}],
|
|
632
|
+
usage: {
|
|
633
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
634
|
+
completion_tokens: usage.output_tokens || 0,
|
|
635
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
resolve({
|
|
641
|
+
id: `cursor-${Math.random().toString(36).substring(2, 10)}`,
|
|
642
|
+
object: 'chat.completion',
|
|
643
|
+
created: Math.floor(Date.now() / 1000),
|
|
644
|
+
model: model || 'cursor',
|
|
645
|
+
choices: [{
|
|
646
|
+
index: 0,
|
|
647
|
+
message: { role: 'assistant', content: stdout.trim() },
|
|
648
|
+
finish_reason: 'stop'
|
|
649
|
+
}],
|
|
650
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async callClaudeCodeStream(model, messages, maxTokens, onChunk) {
|
|
657
|
+
const parts = [];
|
|
658
|
+
for (const msg of messages) {
|
|
659
|
+
if (msg.role === 'system')
|
|
660
|
+
parts.push(`[System] ${msg.content}`);
|
|
661
|
+
else if (msg.role === 'user')
|
|
662
|
+
parts.push(msg.content);
|
|
663
|
+
else if (msg.role === 'assistant')
|
|
664
|
+
parts.push(`[Assistant] ${msg.content}`);
|
|
665
|
+
}
|
|
666
|
+
const prompt = parts.join('\n\n');
|
|
667
|
+
const args = ['-p', '--output-format', 'stream-json', '--no-session-persistence'];
|
|
668
|
+
if (model && model !== 'default') {
|
|
669
|
+
args.push('--model', model);
|
|
670
|
+
}
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
const env = { ...process.env };
|
|
673
|
+
delete env.CLAUDECODE;
|
|
674
|
+
delete env.CLAUDE_CODE;
|
|
675
|
+
const proc = (0, child_process_1.spawn)('claude', args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
676
|
+
proc.stdin.write(prompt);
|
|
677
|
+
proc.stdin.end();
|
|
678
|
+
let fullContent = '';
|
|
679
|
+
let lastResult = null;
|
|
680
|
+
let stderr = '';
|
|
681
|
+
const rl = (0, readline_1.createInterface)({ input: proc.stdout });
|
|
682
|
+
rl.on('line', (line) => {
|
|
683
|
+
try {
|
|
684
|
+
const event = JSON.parse(line);
|
|
685
|
+
if (event.type === 'assistant' && event.message) {
|
|
686
|
+
// Extract text content from assistant message
|
|
687
|
+
const text = typeof event.message === 'string'
|
|
688
|
+
? event.message
|
|
689
|
+
: (event.message.content || [])
|
|
690
|
+
.filter((c) => c.type === 'text')
|
|
691
|
+
.map((c) => c.text)
|
|
692
|
+
.join('');
|
|
693
|
+
if (text) {
|
|
694
|
+
fullContent += text;
|
|
695
|
+
onChunk({ content: text, done: false });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
else if (event.type === 'content_block_delta') {
|
|
699
|
+
const delta = event.delta?.text || '';
|
|
700
|
+
if (delta) {
|
|
701
|
+
fullContent += delta;
|
|
702
|
+
onChunk({ content: delta, done: false });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
else if (event.type === 'result') {
|
|
706
|
+
lastResult = event;
|
|
707
|
+
if (!fullContent && event.result) {
|
|
708
|
+
fullContent = event.result;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch { /* skip non-JSON lines */ }
|
|
713
|
+
});
|
|
714
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
715
|
+
const timer = setTimeout(() => {
|
|
716
|
+
proc.kill();
|
|
717
|
+
onChunk({ content: '', done: true });
|
|
718
|
+
resolve({
|
|
719
|
+
id: '', object: 'error', created: Math.floor(Date.now() / 1000), model,
|
|
720
|
+
choices: [], error: { message: 'Claude Code timed out after 120s', type: 'timeout' }
|
|
721
|
+
});
|
|
722
|
+
}, 120000);
|
|
723
|
+
proc.on('close', (code) => {
|
|
724
|
+
clearTimeout(timer);
|
|
725
|
+
rl.close();
|
|
726
|
+
onChunk({ content: '', done: true });
|
|
727
|
+
const usage = lastResult?.usage || {};
|
|
728
|
+
resolve({
|
|
729
|
+
id: lastResult?.session_id || `claude-${Math.random().toString(36).substring(2, 10)}`,
|
|
730
|
+
object: 'chat.completion',
|
|
731
|
+
created: Math.floor(Date.now() / 1000),
|
|
732
|
+
model: model || 'claude-code',
|
|
733
|
+
choices: [{
|
|
734
|
+
index: 0,
|
|
735
|
+
message: { role: 'assistant', content: fullContent || lastResult?.result || '' },
|
|
736
|
+
finish_reason: 'stop'
|
|
737
|
+
}],
|
|
738
|
+
usage: {
|
|
739
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
740
|
+
completion_tokens: usage.output_tokens || 0,
|
|
741
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async callOllamaStream(model, messages, temperature, maxTokens, onChunk) {
|
|
748
|
+
const baseUrl = this.config.baseUrl || 'http://localhost:11434';
|
|
749
|
+
const requestBody = { model, messages, stream: true };
|
|
750
|
+
const options = {};
|
|
751
|
+
if (temperature !== undefined)
|
|
752
|
+
options.temperature = temperature;
|
|
753
|
+
if (maxTokens !== undefined)
|
|
754
|
+
options.num_predict = maxTokens;
|
|
755
|
+
if (Object.keys(options).length > 0)
|
|
756
|
+
requestBody.options = options;
|
|
757
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
758
|
+
method: 'POST',
|
|
759
|
+
headers: { 'Content-Type': 'application/json' },
|
|
760
|
+
body: JSON.stringify(requestBody)
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok)
|
|
763
|
+
throw new Error(`Ollama returned status ${response.status}`);
|
|
764
|
+
let fullContent = '';
|
|
765
|
+
const reader = response.body?.getReader();
|
|
766
|
+
const decoder = new TextDecoder();
|
|
767
|
+
if (reader) {
|
|
768
|
+
let buffer = '';
|
|
769
|
+
while (true) {
|
|
770
|
+
const { done, value } = await reader.read();
|
|
771
|
+
if (done)
|
|
772
|
+
break;
|
|
773
|
+
buffer += decoder.decode(value, { stream: true });
|
|
774
|
+
const lines = buffer.split('\n');
|
|
775
|
+
buffer = lines.pop() || '';
|
|
776
|
+
for (const line of lines) {
|
|
777
|
+
if (!line.trim())
|
|
778
|
+
continue;
|
|
779
|
+
try {
|
|
780
|
+
const event = JSON.parse(line);
|
|
781
|
+
if (event.message?.content) {
|
|
782
|
+
fullContent += event.message.content;
|
|
783
|
+
onChunk({ content: event.message.content, done: false });
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch { /* skip */ }
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
onChunk({ content: '', done: true });
|
|
791
|
+
return {
|
|
792
|
+
id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,
|
|
793
|
+
object: 'chat.completion',
|
|
794
|
+
created: Math.floor(Date.now() / 1000),
|
|
795
|
+
model,
|
|
796
|
+
choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
|
|
797
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async callOpenAiStream(model, messages, temperature, maxTokens, onChunk) {
|
|
801
|
+
const baseUrl = this.config.baseUrl || 'https://api.openai.com/v1';
|
|
802
|
+
const apiKey = this.config.apiKey;
|
|
803
|
+
if (!apiKey)
|
|
804
|
+
throw new Error('OpenAI API key not configured');
|
|
805
|
+
const requestBody = { model, messages, stream: true };
|
|
806
|
+
if (temperature !== undefined)
|
|
807
|
+
requestBody.temperature = temperature;
|
|
808
|
+
if (maxTokens !== undefined)
|
|
809
|
+
requestBody.max_tokens = maxTokens;
|
|
810
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
811
|
+
method: 'POST',
|
|
812
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
813
|
+
body: JSON.stringify(requestBody)
|
|
814
|
+
});
|
|
815
|
+
if (!response.ok)
|
|
816
|
+
throw new Error(`OpenAI returned status ${response.status}`);
|
|
817
|
+
let fullContent = '';
|
|
818
|
+
const reader = response.body?.getReader();
|
|
819
|
+
const decoder = new TextDecoder();
|
|
820
|
+
if (reader) {
|
|
821
|
+
let buffer = '';
|
|
822
|
+
while (true) {
|
|
823
|
+
const { done, value } = await reader.read();
|
|
824
|
+
if (done)
|
|
825
|
+
break;
|
|
826
|
+
buffer += decoder.decode(value, { stream: true });
|
|
827
|
+
const lines = buffer.split('\n');
|
|
828
|
+
buffer = lines.pop() || '';
|
|
829
|
+
for (const line of lines) {
|
|
830
|
+
if (!line.startsWith('data: ') || line === 'data: [DONE]')
|
|
831
|
+
continue;
|
|
832
|
+
try {
|
|
833
|
+
const event = JSON.parse(line.slice(6));
|
|
834
|
+
const delta = event.choices?.[0]?.delta?.content || '';
|
|
835
|
+
if (delta) {
|
|
836
|
+
fullContent += delta;
|
|
837
|
+
onChunk({ content: delta, done: false });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch { /* skip */ }
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
onChunk({ content: '', done: true });
|
|
845
|
+
return {
|
|
846
|
+
id: `chatcmpl-${Math.random().toString(36).substring(2, 10)}`,
|
|
847
|
+
object: 'chat.completion',
|
|
848
|
+
created: Math.floor(Date.now() / 1000),
|
|
849
|
+
model,
|
|
850
|
+
choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
|
|
851
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
async callAnthropicStream(model, messages, temperature, maxTokens, onChunk) {
|
|
855
|
+
const baseUrl = this.config.baseUrl || 'https://api.anthropic.com';
|
|
856
|
+
const apiKey = this.config.apiKey;
|
|
857
|
+
if (!apiKey)
|
|
858
|
+
throw new Error('Anthropic API key not configured');
|
|
859
|
+
let systemPrompt;
|
|
860
|
+
const chatMessages = [];
|
|
861
|
+
for (const msg of messages) {
|
|
862
|
+
if (msg.role === 'system')
|
|
863
|
+
systemPrompt = msg.content;
|
|
864
|
+
else
|
|
865
|
+
chatMessages.push({ role: msg.role, content: msg.content });
|
|
866
|
+
}
|
|
867
|
+
const requestBody = { model, messages: chatMessages, max_tokens: maxTokens || 1024, stream: true };
|
|
868
|
+
if (systemPrompt)
|
|
869
|
+
requestBody.system = systemPrompt;
|
|
870
|
+
if (temperature !== undefined)
|
|
871
|
+
requestBody.temperature = temperature;
|
|
872
|
+
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
873
|
+
method: 'POST',
|
|
874
|
+
headers: {
|
|
875
|
+
'Content-Type': 'application/json',
|
|
876
|
+
'x-api-key': apiKey,
|
|
877
|
+
'anthropic-version': '2023-06-01'
|
|
878
|
+
},
|
|
879
|
+
body: JSON.stringify(requestBody)
|
|
880
|
+
});
|
|
881
|
+
if (!response.ok)
|
|
882
|
+
throw new Error(`Anthropic returned status ${response.status}`);
|
|
883
|
+
let fullContent = '';
|
|
884
|
+
const reader = response.body?.getReader();
|
|
885
|
+
const decoder = new TextDecoder();
|
|
886
|
+
if (reader) {
|
|
887
|
+
let buffer = '';
|
|
888
|
+
while (true) {
|
|
889
|
+
const { done, value } = await reader.read();
|
|
890
|
+
if (done)
|
|
891
|
+
break;
|
|
892
|
+
buffer += decoder.decode(value, { stream: true });
|
|
893
|
+
const lines = buffer.split('\n');
|
|
894
|
+
buffer = lines.pop() || '';
|
|
895
|
+
for (const line of lines) {
|
|
896
|
+
if (!line.startsWith('data: '))
|
|
897
|
+
continue;
|
|
898
|
+
try {
|
|
899
|
+
const event = JSON.parse(line.slice(6));
|
|
900
|
+
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
901
|
+
fullContent += event.delta.text;
|
|
902
|
+
onChunk({ content: event.delta.text, done: false });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
catch { /* skip */ }
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
onChunk({ content: '', done: true });
|
|
910
|
+
return {
|
|
911
|
+
id: `msg-${Math.random().toString(36).substring(2, 10)}`,
|
|
912
|
+
object: 'chat.completion',
|
|
913
|
+
created: Math.floor(Date.now() / 1000),
|
|
914
|
+
model,
|
|
915
|
+
choices: [{ index: 0, message: { role: 'assistant', content: fullContent }, finish_reason: 'stop' }],
|
|
916
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
convertAnthropicToOpenAiFormat(anthropicResponse, model) {
|
|
920
|
+
const content = (anthropicResponse.content || [])
|
|
921
|
+
.filter((block) => block.type === 'text')
|
|
922
|
+
.map((block) => block.text)
|
|
923
|
+
.join('');
|
|
924
|
+
const usage = anthropicResponse.usage || {};
|
|
925
|
+
return {
|
|
926
|
+
id: anthropicResponse.id || `msg-${Math.random().toString(36).substring(2, 10)}`,
|
|
927
|
+
object: 'chat.completion',
|
|
928
|
+
created: Math.floor(Date.now() / 1000),
|
|
929
|
+
model: anthropicResponse.model || model,
|
|
930
|
+
choices: [{
|
|
931
|
+
index: 0,
|
|
932
|
+
message: {
|
|
933
|
+
role: 'assistant',
|
|
934
|
+
content
|
|
935
|
+
},
|
|
936
|
+
finish_reason: anthropicResponse.stop_reason === 'end_turn' ? 'stop' : (anthropicResponse.stop_reason || 'stop')
|
|
937
|
+
}],
|
|
938
|
+
usage: {
|
|
939
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
940
|
+
completion_tokens: usage.output_tokens || 0,
|
|
941
|
+
total_tokens: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
}
|
|
155
945
|
}
|
|
156
946
|
exports.LlmService = LlmService;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface ExecConfig {
|
|
|
34
34
|
}
|
|
35
35
|
export interface LlmConfig {
|
|
36
36
|
enabled: boolean;
|
|
37
|
-
provider: 'ollama' | 'openai';
|
|
37
|
+
provider: 'ollama' | 'openai' | 'anthropic' | 'claude-code' | 'codex' | 'gemini' | 'cursor';
|
|
38
38
|
baseUrl: string;
|
|
39
39
|
model: string;
|
|
40
40
|
apiKey?: string;
|
package/dist/index.js
CHANGED
|
File without changes
|