guanghulab/glada/model-router.js
2026-05-10 13:12:44 +08:00

534 lines
17 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* GLADA · 模型自动路由器 · model-router.js
*
* 核心职责:
* 1. 启动时自动发现第三方代理上的所有可用模型
* 2. 按能力分级:推理型 / 代码型 / 通用型 / 经济型
* 3. 任务步骤自动匹配最佳模型
* 4. 模型不可用时自动降级
* 5. 定期刷新模型列表默认10分钟
*
* 设计原则:
* - 绝不硬编码模型名称——代理是第三方的,模型池随时变化
* - 系统自动检测,需要哪个用哪个
* - API 格式统一走 OpenAI-compatible不写死
*
* 版权:国作登字-2026-A-00037559
* 签发:铸渊 · ICE-GL-ZY001
*/
'use strict';
const https = require('https');
const http = require('http');
// ── 模型能力分类规则 ────────────────────────────
// 根据模型 ID 中的关键词自动分类
// 顺序即优先级(同类中靠前的优先选用)
const CAPABILITY_PATTERNS = {
// 推理型:复杂分析、架构设计、多步推理
reasoning: [
{ pattern: /o1/i, priority: 10 },
{ pattern: /o3/i, priority: 9 },
{ pattern: /o4/i, priority: 9 },
{ pattern: /reasoner/i, priority: 8 },
{ pattern: /thinking/i, priority: 7 },
{ pattern: /claude.*opus/i, priority: 6 },
{ pattern: /claude.*sonnet/i, priority: 5 },
{ pattern: /gpt-?4o/i, priority: 5 },
{ pattern: /gpt-?4\.?5/i, priority: 5 },
{ pattern: /gpt-?5/i, priority: 6 },
{ pattern: /deepseek.*r1/i, priority: 7 },
{ pattern: /deepseek.*reason/i, priority: 7 },
{ pattern: /qwq/i, priority: 6 },
],
// 代码型:代码生成、修改、调试
coding: [
{ pattern: /codex/i, priority: 10 },
{ pattern: /code.*llama/i, priority: 7 },
{ pattern: /coder/i, priority: 8 },
{ pattern: /deepseek.*coder/i, priority: 9 },
{ pattern: /starcoder/i, priority: 7 },
{ pattern: /codestral/i, priority: 8 },
{ pattern: /claude.*sonnet/i, priority: 6 },
{ pattern: /gpt-?4o/i, priority: 6 },
{ pattern: /gpt-?4\.?5/i, priority: 6 },
{ pattern: /gpt-?5/i, priority: 7 },
{ pattern: /deepseek.*chat/i, priority: 5 },
{ pattern: /deepseek.*v3/i, priority: 5 },
],
// 通用型:一般任务
general: [
{ pattern: /gpt-?4o/i, priority: 8 },
{ pattern: /gpt-?4\.?5/i, priority: 8 },
{ pattern: /gpt-?5/i, priority: 9 },
{ pattern: /claude.*sonnet/i, priority: 8 },
{ pattern: /claude.*haiku/i, priority: 6 },
{ pattern: /deepseek.*chat/i, priority: 7 },
{ pattern: /deepseek.*v3/i, priority: 7 },
{ pattern: /qwen/i, priority: 6 },
{ pattern: /gemini.*pro/i, priority: 7 },
{ pattern: /gemini.*flash/i, priority: 5 },
{ pattern: /llama/i, priority: 4 },
{ pattern: /mistral/i, priority: 5 },
{ pattern: /glm/i, priority: 4 },
{ pattern: /moonshot/i, priority: 5 },
],
// 经济型:简单任务、日志分析、格式化
economy: [
{ pattern: /mini/i, priority: 8 },
{ pattern: /flash/i, priority: 7 },
{ pattern: /lite/i, priority: 7 },
{ pattern: /turbo/i, priority: 6 },
{ pattern: /haiku/i, priority: 6 },
{ pattern: /glm.*flash/i, priority: 5 },
{ pattern: /qwen.*turbo/i, priority: 5 },
{ pattern: /deepseek.*chat/i, priority: 4 },
],
};
// ── 任务步骤 → 模型能力映射 ──────────────────────
const TASK_TYPE_PATTERNS = [
// 推理型任务
{ type: 'reasoning', patterns: [/架构/i, /设计/i, /分析/i, /推理/i, /复杂/i, /安全/i, /审核/i, /重构/i, /architecture/i, /design/i, /analyz/i, /reason/i, /complex/i, /security/i, /refactor/i] },
// 代码型任务
{ type: 'coding', patterns: [/代码/i, /编写/i, /实现/i, /创建.*文件/i, /修改.*文件/i, /新增.*路由/i, /接口/i, /函数/i, /模块/i, /组件/i, /code/i, /implement/i, /create.*file/i, /modify/i, /route/i, /function/i, /module/i, /component/i, /bug.*fix/i, /修复/i] },
// 经济型任务
{ type: 'economy', patterns: [/格式化/i, /注释/i, /日志/i, /README/i, /文档/i, /配置/i, /format/i, /comment/i, /log/i, /doc/i, /config/i, /rename/i, /重命名/i] },
];
// ── 模型路由器状态 ─────────────────────────────
let cachedModels = null; // { models: [...], classified: {...}, discoveredAt: Date }
let refreshTimer = null;
let discoveryInProgress = false;
/**
* HTTP GET 请求
*/
function httpGet(url, headers, timeout) {
return new Promise((resolve, reject) => {
let parsed;
try {
parsed = new URL(url);
} catch {
return reject(new Error(`无效的 URL: ${url}`));
}
const isHttps = parsed.protocol === 'https:';
const mod = isHttps ? https : http;
const opts = {
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'GET',
headers: headers || {},
timeout: timeout || 15000,
};
const req = mod.request(opts, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
resolve({ status: res.statusCode, body: data });
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
/**
* 尝试从指定 URL 获取模型列表
* @param {string} url - 完整的模型列表 API URL
* @param {string} apiKey - API 密钥
* @returns {Promise<string[]|null>} 模型 ID 列表,或 null 表示该端点不可用
*/
async function tryFetchModels(url, apiKey) {
const response = await httpGet(url, {
'Authorization': `Bearer ${apiKey}`,
'Accept': 'application/json'
}, 15000);
if (response.status !== 200) {
console.log(`[GLADA-Router] ${url} 返回 HTTP ${response.status},跳过`);
return null;
}
const body = String(response.body || '').trim();
// 前置检测:如果响应是 HTML 而非 JSON直接跳过不报错
// 某些 API 代理商不支持 /models 端点,会返回 HTML 页面
if (body.startsWith('<') || body.startsWith('<!')) {
console.log(`[GLADA-Router] ${url} 返回了 HTML 页面(该代理商可能不支持模型列表端点),跳过`);
return null;
}
// 空响应
if (!body || body === '{}' || body === '[]') {
console.log(`[GLADA-Router] ${url} 返回空响应,跳过`);
return null;
}
let result;
try {
result = JSON.parse(body);
} catch (parseErr) {
console.log(`[GLADA-Router] ${url} 响应非 JSON 格式: ${parseErr.message},跳过`);
return null;
}
// OpenAI-compatible: { data: [{ id: "model-name", ... }] }
// 也兼容直接返回数组的格式
let models;
if (Array.isArray(result.data)) {
models = result.data.map(m => m.id || m.name || m).filter(Boolean);
} else if (Array.isArray(result)) {
models = result.map(m => (typeof m === 'string') ? m : (m.id || m.name)).filter(Boolean);
} else if (result.models && Array.isArray(result.models)) {
models = result.models.map(m => (typeof m === 'string') ? m : (m.id || m.name)).filter(Boolean);
} else {
console.log('[GLADA-Router] 响应 JSON 格式无法识别为模型列表,跳过');
return null;
}
return models;
}
/**
* 从代理 API 发现可用模型
* 依次尝试多种端点路径(兼容不同 API 代理商):
* 1. ${baseUrl}/models (标准 OpenAI-compatible
* 2. ${baseUrl}/v1/models (某些代理商需要 /v1 前缀)
*
* 如果所有端点均不可用(常见于 DeepSeek 等仅提供 chat 端点的代理商),
* 系统会静默降级使用环境变量中配置的默认模型,不影响正常对话功能。
*
* @returns {Promise<string[]>} 可用模型 ID 列表
*/
async function discoverModels() {
const apiKey = process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY || '';
const baseUrl = (process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || '').replace(/\/+$/, '');
if (!apiKey || !baseUrl) {
console.warn('[GLADA-Router] ⚠️ LLM API 未配置,无法发现模型');
return [];
}
// 构建候选端点列表(按优先级排序)
const candidateUrls = [`${baseUrl}/models`];
// 如果 baseUrl 不含 /v1额外尝试 /v1/models
if (!baseUrl.endsWith('/v1') && !baseUrl.endsWith('/v1/')) {
candidateUrls.push(`${baseUrl}/v1/models`);
}
for (const url of candidateUrls) {
try {
const models = await tryFetchModels(url, apiKey);
if (models && models.length > 0) {
console.log(`[GLADA-Router] 🔍 发现 ${models.length} 个可用模型: ${models.slice(0, 10).join(', ')}${models.length > 10 ? '...' : ''}`);
return models;
}
} catch (err) {
// JSON 解析失败或网络错误,尝试下一个端点
console.log(`[GLADA-Router] ${url} 请求异常: ${err.message},尝试下一个端点`);
}
}
// 所有端点均不可用——这是正常情况(很多代理商不提供 /models 端点)
// 不使用 ⚠️ 警告,因为这不影响核心对话功能
const fallbackModel = process.env.GLADA_MODEL || 'deepseek-chat';
console.log(`[GLADA-Router] 模型列表端点不可用(代理商可能不支持),将使用默认模型: ${fallbackModel}`);
console.log(`[GLADA-Router] 这不影响对话功能——chat/completions 端点正常即可工作`);
return [];
}
/**
* 对模型列表按能力分类
*
* @param {string[]} modelIds - 模型 ID 列表
* @returns {Object} 分类结果 { reasoning: [...], coding: [...], general: [...], economy: [...] }
*/
function classifyModels(modelIds) {
const classified = {
reasoning: [],
coding: [],
general: [],
economy: [],
};
for (const category of Object.keys(CAPABILITY_PATTERNS)) {
const patterns = CAPABILITY_PATTERNS[category];
const matches = [];
for (const modelId of modelIds) {
for (const { pattern, priority } of patterns) {
if (pattern.test(modelId)) {
matches.push({ id: modelId, priority });
break; // 同一模型在同一类别中只匹配一次
}
}
}
// 按优先级降序排列
matches.sort((a, b) => b.priority - a.priority);
classified[category] = matches.map(m => m.id);
}
return classified;
}
/**
* 刷新模型缓存
* @returns {Promise<Object>} 缓存对象
*/
async function refreshModelCache() {
if (discoveryInProgress) {
// 等待上一次发现完成
return cachedModels;
}
discoveryInProgress = true;
try {
const models = await discoverModels();
if (models.length > 0) {
const classified = classifyModels(models);
cachedModels = {
models,
classified,
discoveredAt: new Date().toISOString(),
count: models.length
};
console.log(`[GLADA-Router] 📊 模型分类完成:`);
console.log(` 推理型: ${classified.reasoning.slice(0, 3).join(', ') || '无'}`);
console.log(` 代码型: ${classified.coding.slice(0, 3).join(', ') || '无'}`);
console.log(` 通用型: ${classified.general.slice(0, 3).join(', ') || '无'}`);
console.log(` 经济型: ${classified.economy.slice(0, 3).join(', ') || '无'}`);
} else if (!cachedModels) {
// 模型列表不可用(代理商不支持 /models 端点),用环境变量中配置的模型
// 注意GLADA_MODEL 是用户在部署时显式配置的默认模型,不是硬编码
const fallbackModel = process.env.GLADA_MODEL || process.env.LLM_MODEL || 'deepseek-chat';
cachedModels = {
models: [fallbackModel],
classified: {
reasoning: [fallbackModel],
coding: [fallbackModel],
general: [fallbackModel],
economy: [fallbackModel],
},
discoveredAt: new Date().toISOString(),
count: 1,
fallback: true
};
console.log(`[GLADA-Router] ✅ 使用配置的默认模型: ${fallbackModel}(模型列表端点不可用,不影响对话功能)`);
}
return cachedModels;
} finally {
discoveryInProgress = false;
}
}
/**
* 初始化模型路由器(启动时调用一次)
*
* @param {Object} [options]
* @param {number} [options.refreshIntervalMs=600000] - 刷新间隔毫秒默认10分钟
* @returns {Promise<Object>} 初始缓存
*/
async function initialize(options = {}) {
const refreshMs = options.refreshIntervalMs || 600000; // 10 分钟
console.log(`[GLADA-Router] 🚀 模型路由器初始化...`);
const cache = await refreshModelCache();
// 定期刷新
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(() => {
refreshModelCache().catch(err => {
console.warn(`[GLADA-Router] ⚠️ 定期刷新失败: ${err.message}`);
});
}, refreshMs);
refreshTimer.unref(); // Don't block process exit — allow PM2 graceful shutdown
return cache;
}
/**
* 根据步骤描述判断任务类型
*
* @param {string} stepDescription - 步骤描述
* @returns {string} 'reasoning' | 'coding' | 'general' | 'economy'
*/
function detectTaskType(stepDescription) {
if (!stepDescription) return 'general';
for (const { type, patterns } of TASK_TYPE_PATTERNS) {
for (const pattern of patterns) {
if (pattern.test(stepDescription)) {
return type;
}
}
}
// 默认为 codingGLADA 主要做代码开发)
return 'coding';
}
/**
* 为指定步骤选择最佳模型
*
* 选择逻辑:
* 1. 如果 options.model 指定了具体模型 → 直接使用
* 2. 如果 GLADA_MODEL_PREFERENCE 指定了偏好 → 在可用时优先使用
* 3. 自动检测步骤类型 → 从对应能力池中选最优
* 4. 如果对应池为空 → 降级到通用池 → 再降级到任意可用模型
*
* @param {string} stepDescription - 步骤描述
* @param {Object} [options] - 选项
* @param {string} [options.model] - 强制使用的模型
* @param {string} [options.taskType] - 强制指定任务类型
* @returns {Promise<{model: string, taskType: string, source: string}>}
*/
async function selectModel(stepDescription, options = {}) {
// 确保缓存已加载
if (!cachedModels) {
await refreshModelCache();
}
// 1. 显式指定模型
if (options.model) {
return {
model: options.model,
taskType: 'explicit',
source: 'options.model'
};
}
// 2. 环境变量偏好
const modelPreference = process.env.GLADA_MODEL_PREFERENCE || '';
// 3. 自动检测任务类型
const taskType = options.taskType || detectTaskType(stepDescription);
// 4. 从能力池中选择
const classified = cachedModels?.classified || {};
const pool = classified[taskType] || [];
// 如果有偏好模型且在能力池中
if (modelPreference && pool.includes(modelPreference)) {
return {
model: modelPreference,
taskType,
source: `preference(${taskType})`
};
}
// 如果有偏好模型且在可用模型列表中(不在对应池中但可用)
if (modelPreference && cachedModels?.models?.includes(modelPreference)) {
return {
model: modelPreference,
taskType,
source: `preference(available)`
};
}
// 从能力池中取第一个(最高优先级)
if (pool.length > 0) {
return {
model: pool[0],
taskType,
source: `auto(${taskType})`
};
}
// 降级:从通用池取
if (classified.general?.length > 0 && taskType !== 'general') {
return {
model: classified.general[0],
taskType,
source: `fallback(general)`
};
}
// 降级:取任意可用模型
if (cachedModels?.models?.length > 0) {
return {
model: cachedModels.models[0],
taskType,
source: `fallback(any)`
};
}
// 终极降级:用环境变量默认值(用户在部署时显式配置)
const defaultModel = process.env.GLADA_MODEL || process.env.LLM_MODEL || 'deepseek-chat';
return {
model: defaultModel,
taskType,
source: 'env_default'
};
}
/**
* 获取当前模型缓存状态(供 API 端点和诊断使用)
*
* @returns {Object} 缓存状态
*/
function getStatus() {
if (!cachedModels) {
return {
initialized: false,
models: [],
classified: {},
message: '模型路由器未初始化'
};
}
return {
initialized: true,
discoveredAt: cachedModels.discoveredAt,
totalModels: cachedModels.count,
models: cachedModels.models,
classified: {
reasoning: cachedModels.classified.reasoning || [],
coding: cachedModels.classified.coding || [],
general: cachedModels.classified.general || [],
economy: cachedModels.classified.economy || [],
},
fallback: !!cachedModels.fallback,
preference: process.env.GLADA_MODEL_PREFERENCE || null,
defaultModel: process.env.GLADA_MODEL || 'deepseek-chat'
};
}
/**
* 关闭路由器(清除定时器)
*/
function shutdown() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
console.log('[GLADA-Router] 🔌 模型路由器已关闭');
}
module.exports = {
initialize,
selectModel,
detectTaskType,
classifyModels,
discoverModels,
refreshModelCache,
getStatus,
shutdown
};