guanghulab/server/proxy/service/llm-router.js

233 lines
7.4 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/llm-router.js
// 🤖 AI大模型动态路由器
//
// 核心原则 (冰朔指令):
// - 不写死模型,系统自动检测+按需调用
// - 调用失败自动切换下一个
// - 所有模型都失败 → 推送邮件给冰朔
//
// 使用仓库已配置的 ZY_LLM_API_KEY + ZY_LLM_BASE_URL
// secrets-manifest.json 确认此KEY支持多模型动态路由
// ═══════════════════════════════════════════════
'use strict';
const https = require('https');
const http = require('http');
// ── 模型优先级列表(自动降级)──────────────────
// API网关会根据可用性自动路由但我们仍按优先级尝试
const MODEL_PRIORITY = [
'deepseek-chat',
'qwen-turbo',
'moonshot-v1-8k',
'glm-4-flash',
'claude-3-haiku-20240307',
'gpt-4o-mini'
];
// ── Claude模型识别 (需走硅谷中继) ──────────────
const CLAUDE_MODELS = [
'claude-3-haiku-20240307',
'claude-3-sonnet-20240229',
'claude-3-opus-20240229',
'claude-3-5-sonnet-20241022'
];
// 硅谷SSH隧道本地端口 (Claude API中继)
// 新加坡服务器通过SSH隧道将Claude请求从硅谷IP出口
// 配置: server/proxy/config/claude-relay-config.json
const CLAUDE_RELAY_PORT = parseInt(process.env.ZY_CLAUDE_RELAY_PORT || '18443', 10);
const CLAUDE_RELAY_ENABLED = process.env.ZY_CLAUDE_RELAY_ENABLED === 'true';
function isClaudeModel(model) {
return CLAUDE_MODELS.some(cm => model.includes(cm)) || model.includes('claude');
}
// ── 路由器状态 ──────────────────────────────
const routerState = {
total_calls: 0,
successful_calls: 0,
failed_calls: 0,
last_success: null,
last_error: null,
model_stats: {} // 每个模型的调用统计
};
/**
* 调用LLM API自动模型降级
* @param {string} prompt - 用户/系统提示
* @param {Object} options - 可选配置
* @param {string} options.systemPrompt - 系统提示
* @param {number} options.maxTokens - 最大token数
* @param {number} options.timeout - 超时时间(ms)
* @param {string[]} options.preferModels - 优先尝试的模型列表
* @returns {Promise<{content: string, model: string, usage: Object}|null>}
*/
async function callLLM(prompt, options = {}) {
const apiKey = process.env.ZY_LLM_API_KEY || '';
const baseUrl = process.env.ZY_LLM_BASE_URL || '';
if (!apiKey || !baseUrl) {
console.log('[LLM路由器] API未配置 (ZY_LLM_API_KEY / ZY_LLM_BASE_URL)');
return null;
}
const {
systemPrompt = '你是光湖语言世界VPN系统的AI助手。请简洁、准确地回答问题。',
maxTokens = 500,
timeout = 30000,
preferModels = null
} = options;
const modelsToTry = preferModels || MODEL_PRIORITY;
routerState.total_calls++;
for (const model of modelsToTry) {
try {
const result = await _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout);
if (result) {
routerState.successful_calls++;
routerState.last_success = {
model,
time: new Date().toISOString()
};
// 更新模型统计
if (!routerState.model_stats[model]) {
routerState.model_stats[model] = { success: 0, fail: 0 };
}
routerState.model_stats[model].success++;
return result;
}
} catch (err) {
console.log(`[LLM路由器] 模型 ${model} 调用失败: ${err.message},尝试下一个...`);
if (!routerState.model_stats[model]) {
routerState.model_stats[model] = { success: 0, fail: 0 };
}
routerState.model_stats[model].fail++;
}
}
// 所有模型都失败
routerState.failed_calls++;
routerState.last_error = {
time: new Date().toISOString(),
message: `所有模型(${modelsToTry.length}个)均调用失败`
};
console.error(`[LLM路由器] ❌ 所有模型均不可用`);
return null;
}
/**
* 调用单个模型
* Claude模型自动走硅谷中继 ( ZY_CLAUDE_RELAY_ENABLED=true)
*/
function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout) {
return new Promise((resolve, reject) => {
let urlObj;
try {
urlObj = new URL(baseUrl);
} catch {
// 如果baseUrl不含协议加上https
urlObj = new URL(`https://${baseUrl}`);
}
// Claude模型走硅谷中继 (SSH隧道 localhost:18443 → 硅谷 → api.anthropic.com)
if (CLAUDE_RELAY_ENABLED && isClaudeModel(model)) {
console.log(`[LLM路由器] Claude模型 ${model} → 硅谷中继 (localhost:${CLAUDE_RELAY_PORT})`);
urlObj = new URL(`https://localhost:${CLAUDE_RELAY_PORT}`);
}
const postData = JSON.stringify({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
temperature: 0.3
});
// 构建路径如果baseUrl已包含chat/completions路径则使用
// 否则追加 /v1/chat/completions
let apiPath = urlObj.pathname;
if (!apiPath || apiPath === '/') {
apiPath = '/v1/chat/completions';
} else if (!apiPath.includes('/chat/completions')) {
apiPath = apiPath.replace(/\/+$/, '') + '/chat/completions';
}
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (urlObj.protocol === 'http:' ? 80 : 443),
path: apiPath,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
'Content-Length': Buffer.byteLength(postData)
},
timeout
};
const httpModule = urlObj.protocol === 'http:' ? http : https;
const req = httpModule.request(options, (res) => {
let data = '';
res.on('data', (d) => { data += d; });
res.on('end', () => {
try {
if (res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: 请求失败`));
return;
}
const json = JSON.parse(data);
const content = json.choices?.[0]?.message?.content;
if (!content) {
reject(new Error('响应中无content字段'));
return;
}
resolve({
content,
model: json.model || model,
usage: json.usage || null
});
} catch (e) {
reject(new Error(`JSON解析失败: ${e.message}`));
}
});
});
req.on('error', (err) => reject(err));
req.on('timeout', () => {
req.destroy();
reject(new Error(`超时(${timeout}ms)`));
});
req.write(postData);
req.end();
});
}
/**
* 获取路由器状态
*/
function getRouterStatus() {
return {
...routerState,
api_configured: !!(process.env.ZY_LLM_API_KEY && process.env.ZY_LLM_BASE_URL),
models_available: MODEL_PRIORITY
};
}
module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY, CLAUDE_MODELS, isClaudeModel };