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/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' });