protocol-proxy 2.8.1 → 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 +15 -3
- package/package.json +1 -1
- package/public/app.js +1590 -2027
- package/public/index.html +603 -440
- package/public/style.css +1786 -1996
- package/server.js +98 -1
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' });
|