Step 2: API Fallback 自动切换机制完成
This commit is contained in:
parent
70cbd30e7b
commit
5749122cd1
|
|
@ -0,0 +1,2 @@
|
||||||
|
PRIMARY_API_KEY=你的主密钥
|
||||||
|
FALLBACK_API_KEY=你的备用密钥
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
// 模型路由配置(统一走云雾API中转)
|
// 模型路由配置(支持主备API自动切换)
|
||||||
const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions';
|
const PRIMARY_API_KEY = process.env.PRIMARY_API_KEY;
|
||||||
const YUNWU_KEY = process.env.YUNWU_API_KEY;
|
const FALLBACK_API_KEY = process.env.FALLBACK_API_KEY;
|
||||||
|
|
||||||
const MODELS = {
|
const MODELS = {
|
||||||
deepseek: {
|
deepseek: {
|
||||||
name: 'DeepSeek Chat',
|
name: 'DeepSeek Chat',
|
||||||
apiUrl: YUNWU_API_URL,
|
apiUrl: 'https://api.deepseek.com/v1/chat/completions',
|
||||||
apiKey: YUNWU_KEY,
|
primaryApiKey: PRIMARY_API_KEY,
|
||||||
|
fallbackApiKey: FALLBACK_API_KEY,
|
||||||
model: 'deepseek-chat',
|
model: 'deepseek-chat',
|
||||||
maxTokens: 4096
|
maxTokens: 4096
|
||||||
},
|
},
|
||||||
gpt4o_mini: {
|
gpt4o_mini: {
|
||||||
name: 'GPT-4o-mini',
|
name: 'GPT-4o-mini',
|
||||||
apiUrl: YUNWU_API_URL,
|
apiUrl: 'https://api.deepseek.com/v1/chat/completions',
|
||||||
apiKey: YUNWU_KEY,
|
primaryApiKey: PRIMARY_API_KEY,
|
||||||
|
fallbackApiKey: FALLBACK_API_KEY,
|
||||||
model: 'gpt-4o-mini',
|
model: 'gpt-4o-mini',
|
||||||
maxTokens: 4096
|
maxTokens: 4096
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// 模型路由配置(统一走云雾API中转)
|
||||||
|
const YUNWU_API_URL = 'https://api.deepseek.com/v1/chat/completions';
|
||||||
|
const YUNWU_KEY = process.env.YUNWU_API_KEY;
|
||||||
|
|
||||||
|
const MODELS = {
|
||||||
|
deepseek: {
|
||||||
|
name: 'DeepSeek Chat',
|
||||||
|
apiUrl: YUNWU_API_URL,
|
||||||
|
apiKey: YUNWU_KEY,
|
||||||
|
model: 'deepseek-chat',
|
||||||
|
maxTokens: 4096
|
||||||
|
},
|
||||||
|
gpt4o_mini: {
|
||||||
|
name: 'GPT-4o-mini',
|
||||||
|
apiUrl: YUNWU_API_URL,
|
||||||
|
apiKey: YUNWU_KEY,
|
||||||
|
model: 'gpt-4o-mini',
|
||||||
|
maxTokens: 4096
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = 'deepseek';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
MODELS,
|
||||||
|
DEFAULT_MODEL
|
||||||
|
};
|
||||||
|
|
@ -6,12 +6,71 @@ const { MODELS, DEFAULT_MODEL } = require('../config/models');
|
||||||
router.get('/test', (req, res) => {
|
router.get('/test', (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
message: '模型路由正常 ❤️',
|
message: '模型路由正常 ❤️ (支持主备自动切换)',
|
||||||
available_models: Object.keys(MODELS),
|
available_models: Object.keys(MODELS),
|
||||||
default_model: DEFAULT_MODEL
|
default_model: DEFAULT_MODEL
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 带 fallback 的 API 调用函数
|
||||||
|
async function callAPIWithFallback(model, message) {
|
||||||
|
// 第一次尝试:主密钥
|
||||||
|
try {
|
||||||
|
console.log(`[API] 尝试使用主密钥调用 ${model.name}`);
|
||||||
|
const response = await axios.post(
|
||||||
|
model.apiUrl,
|
||||||
|
{
|
||||||
|
model: model.model,
|
||||||
|
messages: [{ role: 'user', content: message }],
|
||||||
|
max_tokens: model.maxTokens
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + model.primaryApiKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { success: true, data: response.data };
|
||||||
|
} catch (primaryErr) {
|
||||||
|
// 判断是否需要 fallback(余额不足/超时/401/403)
|
||||||
|
const shouldFallback =
|
||||||
|
primaryErr.response?.status === 401 ||
|
||||||
|
primaryErr.response?.status === 403 ||
|
||||||
|
primaryErr.response?.data?.error?.type === 'insufficient_quota' ||
|
||||||
|
primaryErr.code === 'ECONNABORTED' ||
|
||||||
|
primaryErr.message.includes('timeout');
|
||||||
|
|
||||||
|
if (!shouldFallback || !model.fallbackApiKey) {
|
||||||
|
// 不需要 fallback 或没有备用密钥,直接抛出错误
|
||||||
|
throw primaryErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要 fallback,尝试备用密钥
|
||||||
|
console.log(`[API Fallback] 从主密钥切换到备用密钥 (${model.name})`);
|
||||||
|
try {
|
||||||
|
const fallbackResponse = await axios.post(
|
||||||
|
model.apiUrl,
|
||||||
|
{
|
||||||
|
model: model.model,
|
||||||
|
messages: [{ role: 'user', content: message }],
|
||||||
|
max_tokens: model.maxTokens
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + model.fallbackApiKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { success: true, data: fallbackResponse.data, fallback: true };
|
||||||
|
} catch (fallbackErr) {
|
||||||
|
// 备用也失败了,抛出备用错误
|
||||||
|
throw fallbackErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.post('/chat', async (req, res) => {
|
router.post('/chat', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { message, model_name } = req.body;
|
const { message, model_name } = req.body;
|
||||||
|
|
@ -21,33 +80,22 @@ router.post('/chat', async (req, res) => {
|
||||||
return res.status(400).json({ error: '未知模型: ' + model_name });
|
return res.status(400).json({ error: '未知模型: ' + model_name });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!model.apiKey) {
|
if (!model.primaryApiKey) {
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
error: '模型 ' + model.name + ' 的 API Key 尚未配置',
|
error: '模型 ' + model.name + ' 的主 API Key 尚未配置',
|
||||||
hint: '请检查 .env 文件中的 YUNWU_API_KEY'
|
hint: '请检查 .env 文件中的 PRIMARY_API_KEY'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.post(
|
const result = await callAPIWithFallback(model, message);
|
||||||
model.apiUrl,
|
|
||||||
{
|
|
||||||
model: model.model,
|
|
||||||
messages: [{ role: 'user', content: message }],
|
|
||||||
max_tokens: model.maxTokens
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer ' + model.apiKey,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
model: model.name,
|
model: model.name,
|
||||||
reply: response.data.choices[0].message.content
|
reply: result.data.choices[0].message.content,
|
||||||
|
fallback_used: result.fallback || false
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('[API Error]', err.message);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: err.message,
|
error: err.message,
|
||||||
detail: err.response ? err.response.data : null
|
detail: err.response ? err.response.data : null
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
const express = require('express');
|
||||||
|
const axios = require('axios');
|
||||||
|
const router = express.Router();
|
||||||
|
const { MODELS, DEFAULT_MODEL } = require('../config/models');
|
||||||
|
|
||||||
|
router.get('/test', (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: 'ok',
|
||||||
|
message: '模型路由正常 ❤️',
|
||||||
|
available_models: Object.keys(MODELS),
|
||||||
|
default_model: DEFAULT_MODEL
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/chat', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message, model_name } = req.body;
|
||||||
|
const model = MODELS[model_name || DEFAULT_MODEL];
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
return res.status(400).json({ error: '未知模型: ' + model_name });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.apiKey) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: '模型 ' + model.name + ' 的 API Key 尚未配置',
|
||||||
|
hint: '请检查 .env 文件中的 YUNWU_API_KEY'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
model.apiUrl,
|
||||||
|
{
|
||||||
|
model: model.model,
|
||||||
|
messages: [{ role: 'user', content: message }],
|
||||||
|
max_tokens: model.maxTokens
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + model.apiKey,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
model: model.name,
|
||||||
|
reply: response.data.choices[0].message.content
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
error: err.message,
|
||||||
|
detail: err.response ? err.response.data : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Loading…
Reference in New Issue