Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

311 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

/**
* persona-studio · 智能模型路由引擎
*
* 功能:
* ① 探测阶段auto-detect→ 用 API 密钥请求平台的 /models 接口
* ② 路由阶段auto-select→ 根据任务类型选择最优模型
* ③ 降级阶段fallback→ 首选模型失败时自动切换
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const CONFIG_PATH = path.join(__dirname, 'model-config.json');
const BENCHMARK_PATH = path.join(__dirname, 'model-benchmark.json');
/**
* 加载路由配置
*/
function loadConfig() {
try {
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
} catch {
return getDefaultConfig();
}
}
/**
* 加载基准测试结果
*/
function loadBenchmark() {
try {
return JSON.parse(fs.readFileSync(BENCHMARK_PATH, 'utf-8'));
} catch {
return null;
}
}
/**
* 保存基准测试结果
*/
function saveBenchmark(data) {
fs.writeFileSync(BENCHMARK_PATH, JSON.stringify(data, null, 2), 'utf-8');
}
/**
* 根据任务类型选择最优模型
* @param {string} taskType - 'chat' | 'code_generation' | 'code_review' | 'quick_reply'
* @returns {{ model: string, baseUrl: string, apiKey: string }}
*/
function selectModel(taskType) {
const config = loadConfig();
const benchmark = loadBenchmark();
const apiKey = process.env.MODEL_API_KEY || '';
const baseUrl = config.base_url || 'https://api.yunwu.ai/v1';
const contextTarget = config.context_window_target || 200000;
// 如果有 benchmark 且有路由表,使用路由表
if (benchmark && benchmark.routing_table && benchmark.routing_table[taskType]) {
// 优先选择 context_window >= contextTarget 的模型
if (benchmark.benchmark) {
const preferred = benchmark.benchmark.find(function (m) {
return m.available &&
m.scores &&
m.scores.context_window >= contextTarget;
});
if (preferred) {
return {
model: preferred.model_id,
baseUrl,
apiKey
};
}
}
return {
model: benchmark.routing_table[taskType],
baseUrl,
apiKey
};
}
// 默认模型映射
const defaults = {
chat: 'deepseek-chat',
code_generation: 'deepseek-chat',
code_review: 'deepseek-chat',
quick_reply: 'deepseek-chat'
};
return {
model: defaults[taskType] || 'deepseek-chat',
baseUrl,
apiKey
};
}
/**
* 调用 AI 模型 API
* @param {object} params
* @param {string} params.model - 模型 ID
* @param {string} params.baseUrl - API 基础 URL
* @param {string} params.apiKey - API 密钥
* @param {Array} params.messages - OpenAI 格式消息列表
* @param {number} [params.maxTokens=2000]
* @param {number} [params.temperature=0.8]
* @returns {Promise<string>} 模型回复文本
*/
async function callModel({ model, baseUrl, apiKey, messages, maxTokens = 2000, temperature = 0.8 }) {
const config = loadConfig();
const fallbackConfig = config.fallback || { max_retries: 3, timeout_ms: 30000 };
const contextWindowTarget = config.context_window_target || 200000;
// 尝试调用,支持降级
const benchmark = loadBenchmark();
const models = [model];
// 如果有 benchmark添加降级模型
if (benchmark && benchmark.benchmark) {
benchmark.benchmark.forEach(function (m) {
if (m.available && m.model_id !== model && models.length < fallbackConfig.max_retries) {
models.push(m.model_id);
}
});
}
let lastError = null;
for (const currentModel of models) {
try {
const result = await _doRequest({
baseUrl,
apiKey,
model: currentModel,
messages,
maxTokens,
temperature,
timeoutMs: fallbackConfig.timeout_ms
});
return result;
} catch (err) {
lastError = err;
console.error(`Model ${currentModel} failed: ${err.message}, trying next...`);
}
}
throw lastError || new Error('All models failed');
}
/**
* 执行 HTTP 请求到 OpenAI 兼容 API
*/
function _doRequest({ baseUrl, apiKey, model, messages, maxTokens, temperature, timeoutMs }) {
return new Promise((resolve, reject) => {
const url = new URL(baseUrl + '/chat/completions');
const isHttps = url.protocol === 'https:';
const mod = isHttps ? https : http;
const body = JSON.stringify({
model,
messages,
max_tokens: maxTokens,
temperature
});
const options = {
hostname: url.hostname,
port: url.port || (isHttps ? 443 : 80),
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
'Content-Length': Buffer.byteLength(body)
},
timeout: timeoutMs
};
const req = mod.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(data);
if (json.choices && json.choices[0] && json.choices[0].message) {
resolve(json.choices[0].message.content);
} else if (json.error) {
reject(new Error(json.error.message || 'API error'));
} else {
reject(new Error('Unexpected API response'));
}
} catch (e) {
reject(new Error('Failed to parse API response: ' + e.message));
}
});
});
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.write(body);
req.end();
});
}
/**
* 自动探测可用模型(定时任务调用)
*/
async function autoDetect() {
const config = loadConfig();
const apiKey = process.env.MODEL_API_KEY || '';
const baseUrl = config.base_url || 'https://api.yunwu.ai/v1';
if (!apiKey) {
console.error('MODEL_API_KEY not set, skipping auto-detect');
return null;
}
try {
// 请求 /models 接口获取可用模型列表
const modelsUrl = new URL(baseUrl + '/models');
const isHttps = modelsUrl.protocol === 'https:';
const mod = isHttps ? https : http;
const modelsList = await new Promise((resolve, reject) => {
const req = mod.get(modelsUrl.href, {
headers: { 'Authorization': 'Bearer ' + apiKey }
}, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json.data || []);
} catch {
resolve([]);
}
});
});
req.on('error', reject);
req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); });
});
// 生成基准测试结果
const contextTarget = config.context_window_target || 200000;
const benchmarkData = {
last_updated: new Date().toISOString(),
models_detected: modelsList.length,
context_window_target: contextTarget,
benchmark: modelsList.slice(0, 10).map(function (m) {
const ctxWindow = m.context_window || m.context_length || 32000;
return {
model_id: m.id,
available: true,
scores: {
chinese_ability: 80,
conversation_quality: 80,
code_quality: 80,
reasoning: 80,
speed_ms: 2000,
context_window: ctxWindow,
cost_per_1k_tokens: 0.002
},
meets_context_target: ctxWindow >= contextTarget,
best_for: ['chat']
};
}),
routing_table: {
chat: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
code_generation: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
code_review: modelsList[0] ? modelsList[0].id : 'deepseek-chat',
quick_reply: modelsList[0] ? modelsList[0].id : 'deepseek-chat'
}
};
saveBenchmark(benchmarkData);
console.log(`Model auto-detect complete: ${modelsList.length} models found`);
return benchmarkData;
} catch (err) {
console.error('Auto-detect failed:', err.message);
return null;
}
}
function getDefaultConfig() {
return {
api_source: 'third_party_combined',
api_key_env: 'MODEL_API_KEY',
base_url: 'https://api.yunwu.ai/v1',
auto_detect: { enabled: true, schedule: 'daily_0300' },
routing_rules: {
chat: { priority: ['chinese_ability', 'conversation_quality', 'speed'], max_latency_ms: 5000 },
code_generation: { priority: ['code_quality', 'context_window', 'reasoning'], max_latency_ms: 30000 },
code_review: { priority: ['reasoning', 'code_quality'], max_latency_ms: 15000 },
quick_reply: { priority: ['speed', 'cost'], max_latency_ms: 2000 }
},
fallback: { max_retries: 3, timeout_ms: 30000, on_all_fail: 'notify_master' }
};
}
module.exports = {
selectModel,
callModel,
autoDetect,
loadConfig,
loadBenchmark
};