protocol-proxy 2.8.0 → 2.8.2
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/lib/converters/anthropic-to-openai.js +14 -5
- package/lib/proxy-server.js +90 -4
- package/lib/request-log.js +27 -0
- package/lib/stats-store.js +26 -2
- package/lib/ws-server.js +46 -0
- package/package.json +4 -3
- package/public/app.js +1590 -1876
- package/public/index.html +603 -393
- package/public/style.css +1786 -1906
- package/server.js +118 -2
package/server.js
CHANGED
|
@@ -456,7 +456,7 @@ async function init() {
|
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
458
|
if (protocol === 'anthropic') {
|
|
459
|
-
const testModel = req.body.model || 'claude-3-haiku-20240307';
|
|
459
|
+
const testModel = req.body.model || (provider.models && provider.models[0]) || 'claude-3-haiku-20240307';
|
|
460
460
|
return {
|
|
461
461
|
url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
|
|
462
462
|
opts: {
|
|
@@ -501,6 +501,103 @@ async function init() {
|
|
|
501
501
|
res.json({ ok: failed === 0, passed, failed, total: results.length, results });
|
|
502
502
|
});
|
|
503
503
|
|
|
504
|
+
app.post('/api/test-connection', async (req, res) => {
|
|
505
|
+
const { url, protocol, apiKeys, models, azureDeployment, azureApiVersion } = req.body || {};
|
|
506
|
+
if (!url || !protocol) return res.json({ ok: false, message: '缺少 url 或 protocol', results: [] });
|
|
507
|
+
if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
|
|
508
|
+
return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
|
|
509
|
+
}
|
|
510
|
+
const keys = apiKeys.filter(k => k && k.key);
|
|
511
|
+
if (keys.length === 0) return res.json({ ok: false, message: '没有可用的 API Key', results: [] });
|
|
512
|
+
const base = url.replace(/\/$/, '');
|
|
513
|
+
const hasV1Suffix = base.endsWith('/v1');
|
|
514
|
+
const isAzure = protocol === 'openai' && !!azureDeployment;
|
|
515
|
+
|
|
516
|
+
function buildTestOpts(key) {
|
|
517
|
+
if (protocol === 'openai') {
|
|
518
|
+
if (isAzure) {
|
|
519
|
+
const ver = azureApiVersion || '2024-02-01';
|
|
520
|
+
return { url: `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`, opts: { headers: { 'api-key': key } } };
|
|
521
|
+
}
|
|
522
|
+
return { url: hasV1Suffix ? `${base}/models` : `${base}/v1/models`, opts: { headers: { 'Authorization': `Bearer ${key}` } } };
|
|
523
|
+
}
|
|
524
|
+
if (protocol === 'anthropic') {
|
|
525
|
+
const testModel = (Array.isArray(models) && models[0]) || 'claude-3-haiku-20240307';
|
|
526
|
+
return {
|
|
527
|
+
url: hasV1Suffix ? `${base}/messages` : `${base}/v1/messages`,
|
|
528
|
+
opts: { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: testModel, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }) },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (protocol === 'gemini') return { url: `${base}/v1beta/models?key=${key}`, opts: {} };
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const results = await Promise.all(keys.map(async (k) => {
|
|
536
|
+
const built = buildTestOpts(k.key);
|
|
537
|
+
if (!built) return { ok: false, alias: k.alias || '', message: '不支持的协议' };
|
|
538
|
+
try {
|
|
539
|
+
const started = Date.now();
|
|
540
|
+
const fetchRes = await fetch(built.url, { ...built.opts, signal: AbortSignal.timeout(15000) });
|
|
541
|
+
const latency = Date.now() - started;
|
|
542
|
+
if (!fetchRes.ok) {
|
|
543
|
+
const hint = fetchRes.status === 401 || fetchRes.status === 403 ? 'API Key 无效或无权限' : `HTTP ${fetchRes.status}`;
|
|
544
|
+
return { ok: false, alias: k.alias || '', message: hint, latency };
|
|
545
|
+
}
|
|
546
|
+
return { ok: true, alias: k.alias || '', latency };
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return { ok: false, alias: k.alias || '', message: err.name === 'TimeoutError' ? '连接超时' : err.message };
|
|
549
|
+
}
|
|
550
|
+
}));
|
|
551
|
+
|
|
552
|
+
const passed = results.filter(r => r.ok).length;
|
|
553
|
+
res.json({ ok: passed === keys.length, passed, failed: keys.length - passed, results });
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
app.post('/api/providers/available-models', async (req, res) => {
|
|
557
|
+
const { url, protocol, apiKey, azureDeployment, azureApiVersion } = req.body || {};
|
|
558
|
+
if (!url || !protocol) return res.json({ models: [], message: '缺少 url 或 protocol 参数' });
|
|
559
|
+
const key = apiKey || '';
|
|
560
|
+
const base = url.replace(/\/$/, '');
|
|
561
|
+
const hasV1Suffix = base.endsWith('/v1');
|
|
562
|
+
const isAzure = protocol === 'openai' && !!azureDeployment;
|
|
563
|
+
try {
|
|
564
|
+
let fetchUrl, fetchOpts;
|
|
565
|
+
if (protocol === 'openai') {
|
|
566
|
+
if (isAzure) {
|
|
567
|
+
const ver = azureApiVersion || '2024-02-01';
|
|
568
|
+
fetchUrl = `${base}/openai/deployments/${azureDeployment}/models?api-version=${ver}`;
|
|
569
|
+
fetchOpts = { headers: { 'api-key': key } };
|
|
570
|
+
} else {
|
|
571
|
+
fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
|
|
572
|
+
fetchOpts = key ? { headers: { 'Authorization': `Bearer ${key}` } } : {};
|
|
573
|
+
}
|
|
574
|
+
} else if (protocol === 'gemini') {
|
|
575
|
+
fetchUrl = `${base}/v1beta/models?key=${key}`;
|
|
576
|
+
fetchOpts = {};
|
|
577
|
+
} else if (protocol === 'anthropic') {
|
|
578
|
+
fetchUrl = hasV1Suffix ? `${base}/models` : `${base}/v1/models`;
|
|
579
|
+
fetchOpts = key ? { headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' } } : {};
|
|
580
|
+
} else {
|
|
581
|
+
return res.json({ models: [], message: `不支持的协议: ${protocol}` });
|
|
582
|
+
}
|
|
583
|
+
const fetchRes = await fetch(fetchUrl, { ...fetchOpts, signal: AbortSignal.timeout(15000) });
|
|
584
|
+
if (!fetchRes.ok) {
|
|
585
|
+
const hint = fetchRes.status === 404 ? '该供应商不支持模型列表接口' : `获取失败: HTTP ${fetchRes.status}`;
|
|
586
|
+
return res.json({ models: [], message: hint });
|
|
587
|
+
}
|
|
588
|
+
const data = await fetchRes.json().catch(() => null);
|
|
589
|
+
let models = [];
|
|
590
|
+
if (Array.isArray(data?.data)) {
|
|
591
|
+
models = data.data.map(m => m.id || m.name).filter(Boolean).sort();
|
|
592
|
+
} else if (Array.isArray(data?.models)) {
|
|
593
|
+
models = data.models.map(m => (m.name || m.id)?.replace('models/', '')).filter(Boolean).sort();
|
|
594
|
+
}
|
|
595
|
+
res.json({ models });
|
|
596
|
+
} catch (err) {
|
|
597
|
+
res.json({ models: [], message: `获取失败: ${err.message}` });
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
504
601
|
app.post('/api/providers/:id/available-models', async (req, res) => {
|
|
505
602
|
const provider = configStore.getProviderById(req.params.id);
|
|
506
603
|
if (!provider) return res.status(404).json({ error: 'Provider not found' });
|
|
@@ -867,6 +964,13 @@ async function init() {
|
|
|
867
964
|
}
|
|
868
965
|
});
|
|
869
966
|
|
|
967
|
+
// 实时请求日志
|
|
968
|
+
const requestLog = require('./lib/request-log');
|
|
969
|
+
app.get('/api/request-logs', (req, res) => {
|
|
970
|
+
const limit = Math.min(parseInt(req.query.limit) || 200, 2000);
|
|
971
|
+
res.json({ entries: requestLog.getAll(limit), total: requestLog.getCount() });
|
|
972
|
+
});
|
|
973
|
+
|
|
870
974
|
// ==================== 配置导入/导出 ====================
|
|
871
975
|
|
|
872
976
|
app.get('/api/config/export', (req, res) => {
|
|
@@ -1036,11 +1140,19 @@ async function init() {
|
|
|
1036
1140
|
}
|
|
1037
1141
|
}));
|
|
1038
1142
|
|
|
1039
|
-
|
|
1143
|
+
const http = require('http');
|
|
1144
|
+
const server = app.listen(PORT, () => {
|
|
1040
1145
|
const adminUrl = `http://localhost:${PORT}`;
|
|
1041
1146
|
logger.log(`[Admin] Management server running on ${adminUrl}`);
|
|
1042
1147
|
logger.log(`[Admin] ${proxies.length} proxy config(s) loaded`);
|
|
1043
1148
|
logger.log(`[Admin] 日志文件: ${logger.LOG_FILE}`);
|
|
1149
|
+
|
|
1150
|
+
// 初始化 WebSocket 实时日志
|
|
1151
|
+
const wsServer = require('./lib/ws-server');
|
|
1152
|
+
wsServer.init(server);
|
|
1153
|
+
requestLog.onEntry((entry) => wsServer.broadcast(entry));
|
|
1154
|
+
logger.log(`[Admin] WebSocket 已附加 (ws://localhost:${PORT})`);
|
|
1155
|
+
|
|
1044
1156
|
openBrowser(adminUrl);
|
|
1045
1157
|
});
|
|
1046
1158
|
}
|
|
@@ -1050,6 +1162,8 @@ process.on('SIGINT', async () => {
|
|
|
1050
1162
|
logger.log('[Shutdown] Shutting down...');
|
|
1051
1163
|
removePid();
|
|
1052
1164
|
try {
|
|
1165
|
+
const wsServer = require('./lib/ws-server');
|
|
1166
|
+
wsServer.close();
|
|
1053
1167
|
const proxyManager = require('./lib/proxy-manager');
|
|
1054
1168
|
const statsStore = require('./lib/stats-store');
|
|
1055
1169
|
statsStore.flush();
|
|
@@ -1063,6 +1177,8 @@ process.on('SIGINT', async () => {
|
|
|
1063
1177
|
process.on('SIGTERM', async () => {
|
|
1064
1178
|
removePid();
|
|
1065
1179
|
try {
|
|
1180
|
+
const wsServer = require('./lib/ws-server');
|
|
1181
|
+
wsServer.close();
|
|
1066
1182
|
const proxyManager = require('./lib/proxy-manager');
|
|
1067
1183
|
const statsStore = require('./lib/stats-store');
|
|
1068
1184
|
statsStore.flush();
|