hedgequantx 2.9.19 → 2.9.20
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/package.json +1 -1
- package/src/app.js +42 -64
- package/src/lib/m/hqx-2b.js +7 -0
- package/src/lib/m/index.js +138 -0
- package/src/lib/m/ultra-scalping.js +7 -0
- package/src/menus/connect.js +14 -17
- package/src/menus/dashboard.js +58 -76
- package/src/pages/accounts.js +38 -49
- package/src/pages/algo/copy-trading.js +546 -178
- package/src/pages/algo/index.js +18 -75
- package/src/pages/algo/one-account.js +322 -57
- package/src/pages/algo/ui.js +15 -15
- package/src/pages/orders.js +19 -22
- package/src/pages/positions.js +19 -22
- package/src/pages/stats/index.js +15 -16
- package/src/pages/user.js +7 -11
- package/src/services/ai-supervision/health.js +35 -47
- package/src/services/index.js +1 -9
- package/src/services/rithmic/accounts.js +8 -6
- package/src/ui/box.js +9 -5
- package/src/ui/index.js +5 -18
- package/src/ui/menu.js +4 -4
- package/src/pages/ai-agents-ui.js +0 -388
- package/src/pages/ai-agents.js +0 -494
- package/src/pages/ai-models.js +0 -389
- package/src/pages/algo/algo-executor.js +0 -307
- package/src/pages/algo/copy-executor.js +0 -331
- package/src/pages/algo/custom-strategy.js +0 -313
- package/src/services/ai-supervision/consensus.js +0 -284
- package/src/services/ai-supervision/context.js +0 -275
- package/src/services/ai-supervision/directive.js +0 -167
- package/src/services/ai-supervision/index.js +0 -359
- package/src/services/ai-supervision/parser.js +0 -278
- package/src/services/ai-supervision/symbols.js +0 -259
- package/src/services/cliproxy/index.js +0 -256
- package/src/services/cliproxy/installer.js +0 -111
- package/src/services/cliproxy/manager.js +0 -387
- package/src/services/llmproxy/index.js +0 -166
- package/src/services/llmproxy/manager.js +0 -411
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLIProxyAPI Manager
|
|
3
|
-
*
|
|
4
|
-
* Downloads, installs and manages CLIProxyAPI binary for OAuth connections
|
|
5
|
-
* to paid AI plans (Claude Pro, ChatGPT Plus, Gemini, etc.)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const os = require('os');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const http = require('http');
|
|
12
|
-
const { spawn } = require('child_process');
|
|
13
|
-
const { downloadFile, extractTarGz, extractZip } = require('./installer');
|
|
14
|
-
|
|
15
|
-
// CLIProxyAPI version and download URLs
|
|
16
|
-
const CLIPROXY_VERSION = '6.6.88';
|
|
17
|
-
const GITHUB_RELEASE_BASE = 'https://github.com/router-for-me/CLIProxyAPI/releases/download';
|
|
18
|
-
|
|
19
|
-
// Installation directory
|
|
20
|
-
const INSTALL_DIR = path.join(os.homedir(), '.hqx', 'cliproxy');
|
|
21
|
-
const BINARY_NAME = process.platform === 'win32' ? 'cli-proxy-api.exe' : 'cli-proxy-api';
|
|
22
|
-
const BINARY_PATH = path.join(INSTALL_DIR, BINARY_NAME);
|
|
23
|
-
const PID_FILE = path.join(INSTALL_DIR, 'cliproxy.pid');
|
|
24
|
-
// Use default CLIProxyAPI auth directory (where -claude-login saves tokens)
|
|
25
|
-
const AUTH_DIR = path.join(os.homedir(), '.cli-proxy-api');
|
|
26
|
-
|
|
27
|
-
// Default port
|
|
28
|
-
const DEFAULT_PORT = 8317;
|
|
29
|
-
|
|
30
|
-
// OAuth callback ports per provider (from CLIProxyAPI)
|
|
31
|
-
const CALLBACK_PORTS = {
|
|
32
|
-
anthropic: 54545, // Claude: /callback
|
|
33
|
-
openai: 1455, // Codex: /auth/callback
|
|
34
|
-
google: 8085, // Gemini: /oauth2callback
|
|
35
|
-
qwen: null, // Qwen uses polling, no callback
|
|
36
|
-
antigravity: 51121, // Antigravity: /oauth-callback
|
|
37
|
-
iflow: 11451 // iFlow: /oauth2callback
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// OAuth callback paths per provider
|
|
41
|
-
const CALLBACK_PATHS = {
|
|
42
|
-
anthropic: '/callback',
|
|
43
|
-
openai: '/auth/callback',
|
|
44
|
-
google: '/oauth2callback',
|
|
45
|
-
qwen: null,
|
|
46
|
-
antigravity: '/oauth-callback',
|
|
47
|
-
iflow: '/oauth2callback'
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/** Detect if running in headless/VPS environment (no browser access) */
|
|
51
|
-
const isHeadless = () => {
|
|
52
|
-
// SSH/Docker/CI = headless
|
|
53
|
-
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
|
|
54
|
-
if (process.env.DOCKER_CONTAINER || process.env.KUBERNETES_SERVICE_HOST) return true;
|
|
55
|
-
if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) return true;
|
|
56
|
-
// Linux without display = headless
|
|
57
|
-
if (process.platform === 'linux') {
|
|
58
|
-
return !(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
59
|
-
}
|
|
60
|
-
// macOS/Windows = local with GUI
|
|
61
|
-
return false;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** Get download URL for current platform */
|
|
65
|
-
const getDownloadUrl = () => {
|
|
66
|
-
const platform = process.platform, arch = process.arch;
|
|
67
|
-
const osMap = { darwin: 'darwin', linux: 'linux', win32: 'windows' };
|
|
68
|
-
const extMap = { darwin: 'tar.gz', linux: 'tar.gz', win32: 'zip' };
|
|
69
|
-
const archMap = { x64: 'amd64', amd64: 'amd64', arm64: 'arm64' };
|
|
70
|
-
|
|
71
|
-
const osName = osMap[platform], ext = extMap[platform], archName = archMap[arch];
|
|
72
|
-
if (!osName || !archName) return null;
|
|
73
|
-
|
|
74
|
-
const filename = `CLIProxyAPI_${CLIPROXY_VERSION}_${osName}_${archName}.${ext}`;
|
|
75
|
-
return { url: `${GITHUB_RELEASE_BASE}/v${CLIPROXY_VERSION}/${filename}`, filename, ext };
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/** Check if CLIProxyAPI is installed */
|
|
79
|
-
const isInstalled = () => fs.existsSync(BINARY_PATH);
|
|
80
|
-
|
|
81
|
-
/** Install CLIProxyAPI */
|
|
82
|
-
const install = async (onProgress = null) => {
|
|
83
|
-
try {
|
|
84
|
-
const download = getDownloadUrl();
|
|
85
|
-
if (!download) {
|
|
86
|
-
return { success: false, error: 'Unsupported platform' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Create install directory
|
|
90
|
-
if (!fs.existsSync(INSTALL_DIR)) {
|
|
91
|
-
fs.mkdirSync(INSTALL_DIR, { recursive: true });
|
|
92
|
-
}
|
|
93
|
-
if (!fs.existsSync(AUTH_DIR)) {
|
|
94
|
-
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const archivePath = path.join(INSTALL_DIR, download.filename);
|
|
98
|
-
|
|
99
|
-
// Download
|
|
100
|
-
if (onProgress) onProgress('Downloading HQX Connector...', 0);
|
|
101
|
-
await downloadFile(download.url, archivePath, (percent) => {
|
|
102
|
-
if (onProgress) onProgress('Downloading HQX Connector...', percent);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// Extract
|
|
106
|
-
if (onProgress) onProgress('Extracting...', 100);
|
|
107
|
-
if (download.ext === 'tar.gz') {
|
|
108
|
-
await extractTarGz(archivePath, INSTALL_DIR);
|
|
109
|
-
} else {
|
|
110
|
-
await extractZip(archivePath, INSTALL_DIR);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Clean up archive
|
|
114
|
-
if (fs.existsSync(archivePath)) {
|
|
115
|
-
fs.unlinkSync(archivePath);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Make executable on Unix
|
|
119
|
-
if (process.platform !== 'win32' && fs.existsSync(BINARY_PATH)) {
|
|
120
|
-
fs.chmodSync(BINARY_PATH, '755');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!fs.existsSync(BINARY_PATH)) {
|
|
124
|
-
return { success: false, error: 'Binary not found after extraction' };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { success: true, error: null };
|
|
128
|
-
} catch (error) {
|
|
129
|
-
return { success: false, error: error.message };
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/** Check if CLIProxyAPI is running */
|
|
134
|
-
const isRunning = async () => {
|
|
135
|
-
if (fs.existsSync(PID_FILE)) {
|
|
136
|
-
try {
|
|
137
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
138
|
-
process.kill(pid, 0);
|
|
139
|
-
return { running: true, pid };
|
|
140
|
-
} catch (e) { fs.unlinkSync(PID_FILE); }
|
|
141
|
-
}
|
|
142
|
-
return new Promise((resolve) => {
|
|
143
|
-
const req = http.get(`http://127.0.0.1:${DEFAULT_PORT}/v1/models`, (res) => {
|
|
144
|
-
resolve({ running: [200, 401, 403].includes(res.statusCode), pid: null });
|
|
145
|
-
});
|
|
146
|
-
req.on('error', () => resolve({ running: false, pid: null }));
|
|
147
|
-
req.setTimeout(2000, () => { req.destroy(); resolve({ running: false, pid: null }); });
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const CONFIG_PATH = path.join(INSTALL_DIR, 'config.yaml');
|
|
152
|
-
|
|
153
|
-
/** Create or update config file */
|
|
154
|
-
const ensureConfig = () => {
|
|
155
|
-
fs.writeFileSync(CONFIG_PATH, `# HQX CLIProxyAPI Config
|
|
156
|
-
host: "127.0.0.1"
|
|
157
|
-
port: ${DEFAULT_PORT}
|
|
158
|
-
auth-dir: "${AUTH_DIR}"
|
|
159
|
-
debug: false
|
|
160
|
-
api-keys:
|
|
161
|
-
- "hqx-internal-key"
|
|
162
|
-
`);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/** Start CLIProxyAPI */
|
|
166
|
-
const start = async () => {
|
|
167
|
-
if (!isInstalled()) {
|
|
168
|
-
return { success: false, error: 'HQX Connector not installed', pid: null };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const status = await isRunning();
|
|
172
|
-
if (status.running) {
|
|
173
|
-
return { success: true, error: null, pid: status.pid };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
// Ensure config and auth dir exist
|
|
178
|
-
if (!fs.existsSync(AUTH_DIR)) fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
179
|
-
ensureConfig();
|
|
180
|
-
|
|
181
|
-
const args = ['-config', CONFIG_PATH];
|
|
182
|
-
|
|
183
|
-
// Capture stderr for debugging
|
|
184
|
-
const logPath = path.join(INSTALL_DIR, 'cliproxy.log');
|
|
185
|
-
const logFd = fs.openSync(logPath, 'a');
|
|
186
|
-
|
|
187
|
-
const child = spawn(BINARY_PATH, args, {
|
|
188
|
-
detached: true,
|
|
189
|
-
stdio: ['ignore', logFd, logFd],
|
|
190
|
-
cwd: INSTALL_DIR
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
child.unref();
|
|
194
|
-
fs.closeSync(logFd);
|
|
195
|
-
|
|
196
|
-
// Save PID
|
|
197
|
-
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
198
|
-
|
|
199
|
-
// Wait for startup
|
|
200
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
201
|
-
|
|
202
|
-
const runStatus = await isRunning();
|
|
203
|
-
if (runStatus.running) {
|
|
204
|
-
return { success: true, error: null, pid: child.pid };
|
|
205
|
-
} else {
|
|
206
|
-
// Read log for error details
|
|
207
|
-
let errorDetail = 'Failed to start HQX Connector';
|
|
208
|
-
if (fs.existsSync(logPath)) {
|
|
209
|
-
const log = fs.readFileSync(logPath, 'utf8').slice(-500);
|
|
210
|
-
if (log) errorDetail += `: ${log.split('\n').pop()}`;
|
|
211
|
-
}
|
|
212
|
-
return { success: false, error: errorDetail, pid: null };
|
|
213
|
-
}
|
|
214
|
-
} catch (error) {
|
|
215
|
-
return { success: false, error: error.message, pid: null };
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
/** Stop CLIProxyAPI */
|
|
220
|
-
const stop = async () => {
|
|
221
|
-
const status = await isRunning();
|
|
222
|
-
if (!status.running) {
|
|
223
|
-
return { success: true, error: null };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
if (status.pid) {
|
|
228
|
-
process.kill(status.pid, 'SIGTERM');
|
|
229
|
-
} else {
|
|
230
|
-
// No PID - try to find and kill by port (only cli-proxy-api process)
|
|
231
|
-
const { execSync } = require('child_process');
|
|
232
|
-
try {
|
|
233
|
-
if (process.platform === 'win32') {
|
|
234
|
-
// Windows: find PID by port and kill
|
|
235
|
-
const result = execSync(`netstat -ano | findstr :${DEFAULT_PORT} | findstr LISTENING`, { encoding: 'utf8' });
|
|
236
|
-
const match = result.match(/LISTENING\s+(\d+)/);
|
|
237
|
-
if (match) {
|
|
238
|
-
const pid = parseInt(match[1]);
|
|
239
|
-
if (pid !== process.pid) process.kill(pid, 'SIGTERM');
|
|
240
|
-
}
|
|
241
|
-
} else {
|
|
242
|
-
// Unix: find PID listening on port, filter to only cli-proxy-api
|
|
243
|
-
try {
|
|
244
|
-
const result = execSync(`lsof -ti:${DEFAULT_PORT} -sTCP:LISTEN 2>/dev/null || true`, { encoding: 'utf8' });
|
|
245
|
-
const pids = result.trim().split('\n').filter(p => p && parseInt(p) !== process.pid);
|
|
246
|
-
for (const pidStr of pids) {
|
|
247
|
-
const pid = parseInt(pidStr);
|
|
248
|
-
if (pid && pid !== process.pid) {
|
|
249
|
-
try { process.kill(pid, 'SIGTERM'); } catch (e) { /* ignore */ }
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} catch (e) {
|
|
253
|
-
// Ignore errors
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
} catch (e) {
|
|
257
|
-
// Ignore errors - process may already be dead
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (fs.existsSync(PID_FILE)) {
|
|
262
|
-
fs.unlinkSync(PID_FILE);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Wait for port to be released
|
|
266
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
267
|
-
|
|
268
|
-
return { success: true, error: null };
|
|
269
|
-
} catch (error) {
|
|
270
|
-
return { success: false, error: error.message };
|
|
271
|
-
}
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
/** Ensure CLIProxyAPI is installed and running */
|
|
275
|
-
const ensureRunning = async (onProgress = null) => {
|
|
276
|
-
if (!isInstalled()) {
|
|
277
|
-
if (onProgress) onProgress('Installing HQX Connector...', 0);
|
|
278
|
-
const installResult = await install(onProgress);
|
|
279
|
-
if (!installResult.success) return installResult;
|
|
280
|
-
}
|
|
281
|
-
const status = await isRunning();
|
|
282
|
-
if (status.running) return { success: true, error: null };
|
|
283
|
-
if (onProgress) onProgress('Starting HQX Connector...', 100);
|
|
284
|
-
return start();
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
/** Get OAuth login URL for a provider */
|
|
288
|
-
const getLoginUrl = async (provider) => {
|
|
289
|
-
const providerFlags = {
|
|
290
|
-
anthropic: '-claude-login', openai: '-codex-login', google: '-login',
|
|
291
|
-
qwen: '-qwen-login', antigravity: '-antigravity-login', iflow: '-iflow-login'
|
|
292
|
-
};
|
|
293
|
-
const flag = providerFlags[provider];
|
|
294
|
-
if (!flag) return { success: false, url: null, childProcess: null, isHeadless: false, error: 'Provider not supported for OAuth' };
|
|
295
|
-
|
|
296
|
-
const headless = isHeadless();
|
|
297
|
-
const isGemini = (provider === 'google');
|
|
298
|
-
|
|
299
|
-
return new Promise((resolve) => {
|
|
300
|
-
// For Gemini: use 'pipe' stdin so we can send default project selection
|
|
301
|
-
const child = spawn(BINARY_PATH, [flag, '-no-browser'], {
|
|
302
|
-
cwd: INSTALL_DIR,
|
|
303
|
-
stdio: isGemini ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe']
|
|
304
|
-
});
|
|
305
|
-
let output = '', resolved = false;
|
|
306
|
-
|
|
307
|
-
const checkForUrl = () => {
|
|
308
|
-
if (resolved) return;
|
|
309
|
-
const urlMatch = output.match(/https?:\/\/[^\s]+/);
|
|
310
|
-
if (urlMatch) {
|
|
311
|
-
resolved = true;
|
|
312
|
-
resolve({ success: true, url: urlMatch[0], childProcess: child, isHeadless: headless, isGemini, error: null });
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
// For Gemini: auto-select default project when prompted
|
|
317
|
-
if (isGemini && child.stdout) {
|
|
318
|
-
child.stdout.on('data', (data) => {
|
|
319
|
-
output += data.toString();
|
|
320
|
-
checkForUrl();
|
|
321
|
-
// When Gemini asks for project selection, send Enter (default) or ALL
|
|
322
|
-
if (data.toString().includes('Enter project ID') && child.stdin) {
|
|
323
|
-
child.stdin.write('\n'); // Select default project
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
} else if (child.stdout) {
|
|
327
|
-
child.stdout.on('data', (data) => { output += data.toString(); checkForUrl(); });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (child.stderr) {
|
|
331
|
-
child.stderr.on('data', (data) => { output += data.toString(); checkForUrl(); });
|
|
332
|
-
}
|
|
333
|
-
child.on('error', (err) => { if (!resolved) { resolved = true; resolve({ success: false, url: null, childProcess: null, isHeadless: headless, isGemini: false, error: err.message }); }});
|
|
334
|
-
child.on('close', (code) => { if (!resolved) { resolved = true; resolve({ success: false, url: null, childProcess: null, isHeadless: headless, isGemini: false, error: `Process exited with code ${code}` }); }});
|
|
335
|
-
});
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
/** Get callback port for a provider */
|
|
339
|
-
const getCallbackPort = (provider) => CALLBACK_PORTS[provider] || null;
|
|
340
|
-
|
|
341
|
-
/** Process OAuth callback URL manually (for VPS/headless) */
|
|
342
|
-
const processCallback = (callbackUrl, provider = 'anthropic') => {
|
|
343
|
-
return new Promise((resolve) => {
|
|
344
|
-
try {
|
|
345
|
-
const url = new URL(callbackUrl);
|
|
346
|
-
const urlPort = url.port || (url.protocol === 'https:' ? 443 : 80);
|
|
347
|
-
const urlPath = url.pathname + url.search;
|
|
348
|
-
const expectedPort = CALLBACK_PORTS[provider];
|
|
349
|
-
|
|
350
|
-
if (!expectedPort) { resolve({ success: true, error: null }); return; } // Qwen uses polling
|
|
351
|
-
|
|
352
|
-
const targetPort = parseInt(urlPort) || expectedPort;
|
|
353
|
-
const req = http.get(`http://127.0.0.1:${targetPort}${urlPath}`, (res) => {
|
|
354
|
-
let data = '';
|
|
355
|
-
res.on('data', chunk => data += chunk);
|
|
356
|
-
res.on('end', () => {
|
|
357
|
-
resolve(res.statusCode === 200 || res.statusCode === 302
|
|
358
|
-
? { success: true, error: null }
|
|
359
|
-
: { success: false, error: `Callback returned ${res.statusCode}: ${data}` });
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
req.on('error', (err) => resolve({ success: false, error: `Callback error: ${err.message}` }));
|
|
363
|
-
req.setTimeout(10000, () => { req.destroy(); resolve({ success: false, error: 'Callback timeout' }); });
|
|
364
|
-
} catch (err) { resolve({ success: false, error: `Invalid URL: ${err.message}` }); }
|
|
365
|
-
});
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
module.exports = {
|
|
369
|
-
CLIPROXY_VERSION,
|
|
370
|
-
INSTALL_DIR,
|
|
371
|
-
BINARY_PATH,
|
|
372
|
-
AUTH_DIR,
|
|
373
|
-
DEFAULT_PORT,
|
|
374
|
-
CALLBACK_PORTS,
|
|
375
|
-
CALLBACK_PATHS,
|
|
376
|
-
getDownloadUrl,
|
|
377
|
-
isInstalled,
|
|
378
|
-
isHeadless,
|
|
379
|
-
install,
|
|
380
|
-
isRunning,
|
|
381
|
-
start,
|
|
382
|
-
stop,
|
|
383
|
-
ensureRunning,
|
|
384
|
-
getLoginUrl,
|
|
385
|
-
getCallbackPort,
|
|
386
|
-
processCallback
|
|
387
|
-
};
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LLM API Proxy Service
|
|
3
|
-
*
|
|
4
|
-
* Uses LiteLLM (Python) to provide a unified OpenAI-compatible proxy
|
|
5
|
-
* for 50+ LLM providers via API keys.
|
|
6
|
-
*
|
|
7
|
-
* Port: 8318 (different from CLIProxyAPI which uses 8317)
|
|
8
|
-
*
|
|
9
|
-
* Supported providers (API Key only):
|
|
10
|
-
* - MiniMax, DeepSeek, Groq, Mistral, xAI, Perplexity, OpenRouter
|
|
11
|
-
* - And 50+ more via LiteLLM
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const { LLMProxyManager } = require('./manager');
|
|
15
|
-
|
|
16
|
-
// Singleton instance
|
|
17
|
-
let proxyManager = null;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get or create proxy manager instance
|
|
21
|
-
* @returns {LLMProxyManager}
|
|
22
|
-
*/
|
|
23
|
-
const getManager = () => {
|
|
24
|
-
if (!proxyManager) {
|
|
25
|
-
proxyManager = new LLMProxyManager();
|
|
26
|
-
}
|
|
27
|
-
return proxyManager;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Check if LLM Proxy is installed (Python venv + LiteLLM)
|
|
32
|
-
* @returns {boolean}
|
|
33
|
-
*/
|
|
34
|
-
const isInstalled = () => {
|
|
35
|
-
return getManager().isInstalled();
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Install LLM Proxy (creates Python venv, installs LiteLLM)
|
|
40
|
-
* @param {Function} onProgress - Progress callback (message, percent)
|
|
41
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
42
|
-
*/
|
|
43
|
-
const install = async (onProgress = () => {}) => {
|
|
44
|
-
return getManager().install(onProgress);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if LLM Proxy is running
|
|
49
|
-
* @returns {Promise<{running: boolean, port?: number}>}
|
|
50
|
-
*/
|
|
51
|
-
const isRunning = async () => {
|
|
52
|
-
return getManager().isRunning();
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Start LLM Proxy server
|
|
57
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
58
|
-
*/
|
|
59
|
-
const start = async () => {
|
|
60
|
-
return getManager().start();
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Stop LLM Proxy server
|
|
65
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
66
|
-
*/
|
|
67
|
-
const stop = async () => {
|
|
68
|
-
return getManager().stop();
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Set API key for a provider
|
|
73
|
-
* @param {string} providerId - Provider ID (e.g., 'minimax', 'deepseek')
|
|
74
|
-
* @param {string} apiKey - API key
|
|
75
|
-
* @returns {Promise<{success: boolean, error?: string}>}
|
|
76
|
-
*/
|
|
77
|
-
const setApiKey = async (providerId, apiKey) => {
|
|
78
|
-
return getManager().setApiKey(providerId, apiKey);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get API key for a provider
|
|
83
|
-
* @param {string} providerId - Provider ID
|
|
84
|
-
* @returns {string|null}
|
|
85
|
-
*/
|
|
86
|
-
const getApiKey = (providerId) => {
|
|
87
|
-
return getManager().getApiKey(providerId);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Test connection to a provider
|
|
92
|
-
* @param {string} providerId - Provider ID
|
|
93
|
-
* @param {string} modelId - Model ID to test
|
|
94
|
-
* @returns {Promise<{success: boolean, latency?: number, error?: string}>}
|
|
95
|
-
*/
|
|
96
|
-
const testConnection = async (providerId, modelId) => {
|
|
97
|
-
return getManager().testConnection(providerId, modelId);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Make a chat completion request via LLM Proxy
|
|
102
|
-
* @param {string} providerId - Provider ID
|
|
103
|
-
* @param {string} modelId - Model ID
|
|
104
|
-
* @param {Array} messages - Chat messages
|
|
105
|
-
* @param {Object} options - Additional options (temperature, max_tokens, etc.)
|
|
106
|
-
* @returns {Promise<{success: boolean, response?: Object, error?: string}>}
|
|
107
|
-
*/
|
|
108
|
-
const chatCompletion = async (providerId, modelId, messages, options = {}) => {
|
|
109
|
-
return getManager().chatCompletion(providerId, modelId, messages, options);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get LLM Proxy base URL
|
|
114
|
-
* @returns {string}
|
|
115
|
-
*/
|
|
116
|
-
const getBaseUrl = () => {
|
|
117
|
-
return getManager().getBaseUrl();
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get port
|
|
122
|
-
* @returns {number}
|
|
123
|
-
*/
|
|
124
|
-
const getPort = () => {
|
|
125
|
-
return getManager().port;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Provider mapping for LiteLLM model prefixes
|
|
130
|
-
*/
|
|
131
|
-
const PROVIDER_PREFIXES = {
|
|
132
|
-
minimax: 'minimax/',
|
|
133
|
-
deepseek: 'deepseek/',
|
|
134
|
-
groq: 'groq/',
|
|
135
|
-
mistral: 'mistral/',
|
|
136
|
-
xai: 'xai/',
|
|
137
|
-
perplexity: 'perplexity/',
|
|
138
|
-
openrouter: 'openrouter/',
|
|
139
|
-
together: 'together_ai/',
|
|
140
|
-
anyscale: 'anyscale/',
|
|
141
|
-
fireworks: 'fireworks_ai/',
|
|
142
|
-
cohere: 'cohere/',
|
|
143
|
-
ai21: 'ai21/',
|
|
144
|
-
nlp_cloud: 'nlp_cloud/',
|
|
145
|
-
replicate: 'replicate/',
|
|
146
|
-
bedrock: 'bedrock/',
|
|
147
|
-
sagemaker: 'sagemaker/',
|
|
148
|
-
vertex: 'vertex_ai/',
|
|
149
|
-
palm: 'palm/',
|
|
150
|
-
azure: 'azure/',
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
module.exports = {
|
|
154
|
-
isInstalled,
|
|
155
|
-
install,
|
|
156
|
-
isRunning,
|
|
157
|
-
start,
|
|
158
|
-
stop,
|
|
159
|
-
setApiKey,
|
|
160
|
-
getApiKey,
|
|
161
|
-
testConnection,
|
|
162
|
-
chatCompletion,
|
|
163
|
-
getBaseUrl,
|
|
164
|
-
getPort,
|
|
165
|
-
PROVIDER_PREFIXES,
|
|
166
|
-
};
|