534 lines
17 KiB
JavaScript
534 lines
17 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 默认为 coding(GLADA 主要做代码开发)
|
|||
|
|
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
|
|||
|
|
};
|