feat: 集成阿里云日志服务(SLS)
- 添加 aliyun-log SDK 依赖 - 新增 sls-logger.js 模块,支持批量日志上报、静默降级 - 在四个 API 处理函数中集成请求日志记录 - 更新 .env.example 添加 SLS 配置示例
This commit is contained in:
@@ -5,3 +5,10 @@ FACTORY_API_KEY=your_factory_api_key_here
|
||||
|
||||
# 方式2:使用refresh token自动刷新(次优先级)
|
||||
DROID_REFRESH_KEY=your_refresh_token_here
|
||||
|
||||
# 阿里云日志服务配置
|
||||
ALIYUN_ACCESS_KEY_ID=your_access_key_id
|
||||
ALIYUN_ACCESS_KEY_SECRET=your_access_key_secret
|
||||
ALIYUN_SLS_ENDPOINT=cn-hangzhou.log.aliyuncs.com
|
||||
ALIYUN_SLS_PROJECT=your_project_name
|
||||
ALIYUN_SLS_LOGSTORE=your_logstore_name
|
||||
|
||||
173
package-lock.json
generated
173
package-lock.json
generated
@@ -1,19 +1,99 @@
|
||||
{
|
||||
"name": "droid2api",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "droid2api",
|
||||
"version": "1.3.5",
|
||||
"version": "1.3.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk",
|
||||
"express": "^4.18.2",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz",
|
||||
"integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -36,6 +116,18 @@
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/aliyun-log": {
|
||||
"name": "@alicloud/log",
|
||||
"version": "1.2.6",
|
||||
"resolved": "git+ssh://git@github.com/aliyun/aliyun-log-nodejs-sdk.git#f5c2ab9cf5e0c7d3edd2fa1a15fc7f0a9946cd05",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8",
|
||||
"httpx": "^2.1.2",
|
||||
"kitx": "^1.2.1",
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -502,6 +594,39 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/httpx": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz",
|
||||
"integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^20",
|
||||
"debug": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/httpx/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/httpx/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
@@ -529,6 +654,18 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/kitx": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/kitx/-/kitx-1.3.0.tgz",
|
||||
"integrity": "sha512-fhBqFlXd0GkKTB+8ayLfpzPUw+LHxZlPAukPNBD1Om7JMeInT+/PxCAf1yLagvD+VKoyWhXtJR68xQkX/a0wOQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -690,6 +827,32 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "6.11.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
|
||||
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "bin/pbjs",
|
||||
"pbts": "bin/pbts"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -931,6 +1094,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
11
package.json
11
package.json
@@ -8,12 +8,17 @@
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js"
|
||||
},
|
||||
"keywords": ["openai", "api", "proxy"],
|
||||
"keywords": [
|
||||
"openai",
|
||||
"api",
|
||||
"proxy"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"node-fetch": "^3.3.2"
|
||||
"node-fetch": "^3.3.2",
|
||||
"aliyun-log": "github:aliyun/aliyun-log-nodejs-sdk"
|
||||
}
|
||||
}
|
||||
}
|
||||
71
routes.js
71
routes.js
@@ -9,6 +9,7 @@ import { AnthropicResponseTransformer } from './transformers/response-anthropic.
|
||||
import { OpenAIResponseTransformer } from './transformers/response-openai.js';
|
||||
import { getApiKey } from './auth.js';
|
||||
import { getNextProxyAgent } from './proxy-manager.js';
|
||||
import { logRequest as slsLogRequest } from './sls-logger.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -52,7 +53,7 @@ function convertResponseToChatCompletion(resp) {
|
||||
|
||||
router.get('/v1/models', (req, res) => {
|
||||
logInfo('GET /v1/models');
|
||||
|
||||
|
||||
try {
|
||||
const config = getConfig();
|
||||
const models = config.models.map(model => ({
|
||||
@@ -81,6 +82,7 @@ router.get('/v1/models', (req, res) => {
|
||||
// 标准 OpenAI 聊天补全处理函数(带格式转换)
|
||||
async function handleChatCompletions(req, res) {
|
||||
logInfo('POST /v1/chat/completions');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const openaiRequest = req.body;
|
||||
@@ -108,7 +110,7 @@ async function handleChatCompletions(req, res) {
|
||||
authHeader = await getApiKey(req.headers.authorization);
|
||||
} catch (error) {
|
||||
logError('Failed to get API key', error);
|
||||
return res.status(500).json({
|
||||
return res.status(500).json({
|
||||
error: 'API key not available',
|
||||
message: 'Failed to get or refresh API key. Please check server logs.'
|
||||
});
|
||||
@@ -166,9 +168,9 @@ async function handleChatCompletions(req, res) {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logError(`Endpoint error: ${response.status}`, new Error(errorText));
|
||||
return res.status(response.status).json({
|
||||
return res.status(response.status).json({
|
||||
error: `Endpoint returned ${response.status}`,
|
||||
details: errorText
|
||||
details: errorText
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,6 +189,7 @@ async function handleChatCompletions(req, res) {
|
||||
}
|
||||
res.end();
|
||||
logInfo('Stream forwarded (common type)');
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
} catch (streamError) {
|
||||
logError('Stream error', streamError);
|
||||
res.end();
|
||||
@@ -206,6 +209,7 @@ async function handleChatCompletions(req, res) {
|
||||
}
|
||||
res.end();
|
||||
logInfo('Stream completed');
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
} catch (streamError) {
|
||||
logError('Stream error', streamError);
|
||||
res.end();
|
||||
@@ -228,13 +232,15 @@ async function handleChatCompletions(req, res) {
|
||||
logResponse(200, null, data);
|
||||
res.json(data);
|
||||
}
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Error in /v1/chat/completions', error);
|
||||
res.status(500).json({
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/chat/completions', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -242,6 +248,7 @@ async function handleChatCompletions(req, res) {
|
||||
// 直接转发 OpenAI 请求(不做格式转换)
|
||||
async function handleDirectResponses(req, res) {
|
||||
logInfo('POST /v1/responses');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const openaiRequest = req.body;
|
||||
@@ -258,7 +265,7 @@ async function handleDirectResponses(req, res) {
|
||||
|
||||
// 只允许 openai 类型端点
|
||||
if (model.type !== 'openai') {
|
||||
return res.status(400).json({
|
||||
return res.status(400).json({
|
||||
error: 'Invalid endpoint type',
|
||||
message: `/v1/responses 接口只支持 openai 类型端点,当前模型 ${modelId} 是 ${model.type} 类型`
|
||||
});
|
||||
@@ -280,17 +287,17 @@ async function handleDirectResponses(req, res) {
|
||||
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
|
||||
} catch (error) {
|
||||
logError('Failed to get API key', error);
|
||||
return res.status(500).json({
|
||||
return res.status(500).json({
|
||||
error: 'API key not available',
|
||||
message: 'Failed to get or refresh API key. Please check server logs.'
|
||||
});
|
||||
}
|
||||
|
||||
const clientHeaders = req.headers;
|
||||
|
||||
|
||||
// Get provider from model config
|
||||
const provider = getModelProvider(modelId);
|
||||
|
||||
|
||||
// 获取 headers
|
||||
const headers = getOpenAIHeaders(authHeader, clientHeaders, provider);
|
||||
|
||||
@@ -340,8 +347,8 @@ async function handleDirectResponses(req, res) {
|
||||
if (claudeCodeTools.includes(tool.name)) return false;
|
||||
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
||||
return !tool.name.startsWith('mcp__') &&
|
||||
!tool.name.includes('Mcp') &&
|
||||
!tool.name.includes('MCP');
|
||||
!tool.name.includes('Mcp') &&
|
||||
!tool.name.includes('MCP');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -366,9 +373,9 @@ async function handleDirectResponses(req, res) {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logError(`Endpoint error: ${response.status}`, new Error(errorText));
|
||||
return res.status(response.status).json({
|
||||
return res.status(response.status).json({
|
||||
error: `Endpoint returned ${response.status}`,
|
||||
details: errorText
|
||||
details: errorText
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,6 +394,7 @@ async function handleDirectResponses(req, res) {
|
||||
}
|
||||
res.end();
|
||||
logInfo('Stream forwarded successfully');
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
} catch (streamError) {
|
||||
logError('Stream error', streamError);
|
||||
res.end();
|
||||
@@ -396,13 +404,15 @@ async function handleDirectResponses(req, res) {
|
||||
const data = await response.json();
|
||||
logResponse(200, null, data);
|
||||
res.json(data);
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Error in /v1/responses', error);
|
||||
res.status(500).json({
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/responses', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -410,6 +420,7 @@ async function handleDirectResponses(req, res) {
|
||||
// 直接转发 Anthropic 请求(不做格式转换)
|
||||
async function handleDirectMessages(req, res) {
|
||||
logInfo('POST /v1/messages');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const anthropicRequest = req.body;
|
||||
@@ -426,7 +437,7 @@ async function handleDirectMessages(req, res) {
|
||||
|
||||
// 只允许 anthropic 类型端点
|
||||
if (model.type !== 'anthropic') {
|
||||
return res.status(400).json({
|
||||
return res.status(400).json({
|
||||
error: 'Invalid endpoint type',
|
||||
message: `/v1/messages 接口只支持 anthropic 类型端点,当前模型 ${modelId} 是 ${model.type} 类型`
|
||||
});
|
||||
@@ -448,17 +459,17 @@ async function handleDirectMessages(req, res) {
|
||||
authHeader = await getApiKey(req.headers.authorization || clientAuthFromXApiKey);
|
||||
} catch (error) {
|
||||
logError('Failed to get API key', error);
|
||||
return res.status(500).json({
|
||||
return res.status(500).json({
|
||||
error: 'API key not available',
|
||||
message: 'Failed to get or refresh API key. Please check server logs.'
|
||||
});
|
||||
}
|
||||
|
||||
const clientHeaders = req.headers;
|
||||
|
||||
|
||||
// Get provider from model config
|
||||
const provider = getModelProvider(modelId);
|
||||
|
||||
|
||||
// 获取 headers
|
||||
const isStreaming = anthropicRequest.stream === true;
|
||||
const headers = getAnthropicHeaders(authHeader, clientHeaders, isStreaming, modelId, provider);
|
||||
@@ -519,7 +530,7 @@ async function handleDirectMessages(req, res) {
|
||||
'high': 24576,
|
||||
'xhigh': 40960
|
||||
};
|
||||
|
||||
|
||||
modifiedRequest.thinking = {
|
||||
type: 'enabled',
|
||||
budget_tokens: budgetTokens[reasoningLevel]
|
||||
@@ -575,8 +586,8 @@ async function handleDirectMessages(req, res) {
|
||||
if (claudeCodeTools.includes(tool.name)) return false;
|
||||
// 过滤所有 mcp__ 开头的工具和 MCP 相关工具
|
||||
return !tool.name.startsWith('mcp__') &&
|
||||
!tool.name.includes('Mcp') &&
|
||||
!tool.name.includes('MCP');
|
||||
!tool.name.includes('Mcp') &&
|
||||
!tool.name.includes('MCP');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -601,9 +612,9 @@ async function handleDirectMessages(req, res) {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
logError(`Endpoint error: ${response.status}`, new Error(errorText));
|
||||
return res.status(response.status).json({
|
||||
return res.status(response.status).json({
|
||||
error: `Endpoint returned ${response.status}`,
|
||||
details: errorText
|
||||
details: errorText
|
||||
});
|
||||
}
|
||||
|
||||
@@ -620,6 +631,7 @@ async function handleDirectMessages(req, res) {
|
||||
}
|
||||
res.end();
|
||||
logInfo('Stream forwarded successfully');
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
} catch (streamError) {
|
||||
logError('Stream error', streamError);
|
||||
res.end();
|
||||
@@ -629,10 +641,12 @@ async function handleDirectMessages(req, res) {
|
||||
const data = await response.json();
|
||||
logResponse(200, null, data);
|
||||
res.json(data);
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Error in /v1/messages', error);
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/messages', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
@@ -643,6 +657,7 @@ async function handleDirectMessages(req, res) {
|
||||
// 处理 Anthropic count_tokens 请求
|
||||
async function handleCountTokens(req, res) {
|
||||
logInfo('POST /v1/messages/count_tokens');
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const anthropicRequest = req.body;
|
||||
@@ -686,10 +701,10 @@ async function handleCountTokens(req, res) {
|
||||
}
|
||||
|
||||
const clientHeaders = req.headers;
|
||||
|
||||
|
||||
// Get provider from model config
|
||||
const provider = getModelProvider(modelId);
|
||||
|
||||
|
||||
const headers = getAnthropicHeaders(authHeader, clientHeaders, false, modelId, provider);
|
||||
|
||||
// 构建 count_tokens 端点 URL
|
||||
@@ -728,9 +743,11 @@ async function handleCountTokens(req, res) {
|
||||
const data = await response.json();
|
||||
logResponse(200, null, data);
|
||||
res.json(data);
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: modelId, status: 200, duration_ms: Date.now() - startTime });
|
||||
|
||||
} catch (error) {
|
||||
logError('Error in /v1/messages/count_tokens', error);
|
||||
slsLogRequest({ method: 'POST', endpoint: '/v1/messages/count_tokens', model: req.body?.model, status: 500, duration_ms: Date.now() - startTime, error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
|
||||
149
sls-logger.js
Normal file
149
sls-logger.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 阿里云日志服务(SLS)日志模块
|
||||
*
|
||||
* 功能:
|
||||
* - 将 API 请求/响应日志上报到阿里云 SLS
|
||||
* - 批量上报,减少 API 调用
|
||||
* - 环境变量缺失时静默降级
|
||||
*/
|
||||
|
||||
import aliyunLog from 'aliyun-log';
|
||||
const { Client, PutLogsRequest, LogItem, LogContent } = aliyunLog;
|
||||
|
||||
// SLS 配置
|
||||
const SLS_CONFIG = {
|
||||
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
|
||||
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
|
||||
endpoint: process.env.ALIYUN_SLS_ENDPOINT,
|
||||
project: process.env.ALIYUN_SLS_PROJECT,
|
||||
logstore: process.env.ALIYUN_SLS_LOGSTORE
|
||||
};
|
||||
|
||||
// 检查配置是否完整
|
||||
const isConfigured = Object.values(SLS_CONFIG).every(v => v);
|
||||
|
||||
let client = null;
|
||||
let logQueue = [];
|
||||
const BATCH_SIZE = 10;
|
||||
const FLUSH_INTERVAL_MS = 5000;
|
||||
|
||||
// 初始化 SLS Client
|
||||
function initClient() {
|
||||
if (!isConfigured) {
|
||||
console.warn('[SLS] 阿里云日志服务未配置,日志将仅输出到控制台');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
client = new Client({
|
||||
accessKeyId: SLS_CONFIG.accessKeyId,
|
||||
accessKeySecret: SLS_CONFIG.accessKeySecret,
|
||||
endpoint: SLS_CONFIG.endpoint
|
||||
});
|
||||
console.log('[SLS] 阿里云日志服务客户端初始化成功');
|
||||
return client;
|
||||
} catch (error) {
|
||||
console.error('[SLS] 初始化失败:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日志队列
|
||||
async function flushLogs() {
|
||||
if (!client || logQueue.length === 0) return;
|
||||
|
||||
const logsToSend = logQueue.splice(0, BATCH_SIZE);
|
||||
|
||||
try {
|
||||
const logItems = logsToSend.map(log => {
|
||||
const contents = Object.entries(log).map(([key, value]) => {
|
||||
return new LogContent({
|
||||
key,
|
||||
value: String(value ?? '')
|
||||
});
|
||||
});
|
||||
return new LogItem({
|
||||
time: Math.floor(Date.now() / 1000),
|
||||
contents
|
||||
});
|
||||
});
|
||||
|
||||
const request = new PutLogsRequest({
|
||||
projectName: SLS_CONFIG.project,
|
||||
logStoreName: SLS_CONFIG.logstore,
|
||||
logGroup: {
|
||||
logs: logItems
|
||||
}
|
||||
});
|
||||
|
||||
await client.putLogs(request);
|
||||
console.log(`[SLS] 成功上报 ${logsToSend.length} 条日志`);
|
||||
} catch (error) {
|
||||
console.error('[SLS] 日志上报失败:', error.message);
|
||||
// 失败的日志重新入队(可选:限制重试次数)
|
||||
logQueue.unshift(...logsToSend);
|
||||
}
|
||||
}
|
||||
|
||||
// 定时刷新
|
||||
let flushTimer = null;
|
||||
function startFlushTimer() {
|
||||
if (flushTimer || !isConfigured) return;
|
||||
flushTimer = setInterval(flushLogs, FLUSH_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 API 请求日志
|
||||
* @param {Object} logData - 日志数据
|
||||
* @param {string} logData.method - HTTP 方法
|
||||
* @param {string} logData.endpoint - 请求路径
|
||||
* @param {string} logData.model - 模型 ID
|
||||
* @param {number} logData.status - 响应状态码
|
||||
* @param {number} logData.duration_ms - 请求耗时
|
||||
* @param {number} [logData.input_tokens] - 输入 Token 数
|
||||
* @param {number} [logData.output_tokens] - 输出 Token 数
|
||||
* @param {string} [logData.error] - 错误信息
|
||||
*/
|
||||
export function logRequest(logData) {
|
||||
const enrichedLog = {
|
||||
timestamp: new Date().toISOString(),
|
||||
...logData
|
||||
};
|
||||
|
||||
// 始终输出到控制台
|
||||
console.log('[SLS]', JSON.stringify(enrichedLog));
|
||||
|
||||
if (!isConfigured) return;
|
||||
|
||||
logQueue.push(enrichedLog);
|
||||
|
||||
// 队列满时立即刷新
|
||||
if (logQueue.length >= BATCH_SIZE) {
|
||||
flushLogs();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优雅关闭,刷新剩余日志
|
||||
*/
|
||||
export async function shutdown() {
|
||||
if (flushTimer) {
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = null;
|
||||
}
|
||||
await flushLogs();
|
||||
console.log('[SLS] 已关闭');
|
||||
}
|
||||
|
||||
// 初始化
|
||||
initClient();
|
||||
startFlushTimer();
|
||||
|
||||
// 进程退出时优雅关闭
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
export default {
|
||||
logRequest,
|
||||
shutdown
|
||||
};
|
||||
Reference in New Issue
Block a user