mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix: 修复非流式响应JSON解析错误和max_tokens参数校验
- 修复gzip压缩响应处理:添加zlib模块支持,正确解压缩Claude API响应 - 优化max_tokens验证机制:从硬编码改为动态读取model_pricing.json文件 - 改进错误处理:增强响应数据处理的容错性 - 修复SSE错误事件格式:统一字符串引号风格 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const zlib = require('zlib');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
const claudeAccountService = require('./claudeAccountService');
|
const claudeAccountService = require('./claudeAccountService');
|
||||||
@@ -91,6 +94,9 @@ class ClaudeRelayService {
|
|||||||
// 深拷贝请求体
|
// 深拷贝请求体
|
||||||
const processedBody = JSON.parse(JSON.stringify(body));
|
const processedBody = JSON.parse(JSON.stringify(body));
|
||||||
|
|
||||||
|
// 验证并限制max_tokens参数
|
||||||
|
this._validateAndLimitMaxTokens(processedBody);
|
||||||
|
|
||||||
// 移除cache_control中的ttl字段
|
// 移除cache_control中的ttl字段
|
||||||
this._stripTtlFromCacheControl(processedBody);
|
this._stripTtlFromCacheControl(processedBody);
|
||||||
|
|
||||||
@@ -133,6 +139,49 @@ class ClaudeRelayService {
|
|||||||
return processedBody;
|
return processedBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔢 验证并限制max_tokens参数
|
||||||
|
_validateAndLimitMaxTokens(body) {
|
||||||
|
if (!body || !body.max_tokens) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 读取模型定价配置文件
|
||||||
|
const pricingFilePath = path.join(__dirname, '../../data/model_pricing.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(pricingFilePath)) {
|
||||||
|
logger.warn('⚠️ Model pricing file not found, skipping max_tokens validation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricingData = JSON.parse(fs.readFileSync(pricingFilePath, 'utf8'));
|
||||||
|
const model = body.model || 'claude-sonnet-4-20250514';
|
||||||
|
|
||||||
|
// 查找对应模型的配置
|
||||||
|
const modelConfig = pricingData[model];
|
||||||
|
|
||||||
|
if (!modelConfig) {
|
||||||
|
logger.debug(`🔍 Model ${model} not found in pricing file, skipping max_tokens validation`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模型的最大token限制
|
||||||
|
const maxLimit = modelConfig.max_tokens || modelConfig.max_output_tokens;
|
||||||
|
|
||||||
|
if (!maxLimit) {
|
||||||
|
logger.debug(`🔍 No max_tokens limit found for model ${model}, skipping validation`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查并调整max_tokens
|
||||||
|
if (body.max_tokens > maxLimit) {
|
||||||
|
logger.warn(`⚠️ max_tokens ${body.max_tokens} exceeds limit ${maxLimit} for model ${model}, adjusting to ${maxLimit}`);
|
||||||
|
body.max_tokens = maxLimit;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to validate max_tokens from pricing file:', error);
|
||||||
|
// 如果文件读取失败,不进行校验,让请求继续处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 🧹 移除TTL字段
|
// 🧹 移除TTL字段
|
||||||
_stripTtlFromCacheControl(body) {
|
_stripTtlFromCacheControl(body) {
|
||||||
if (!body || typeof body !== 'object') return;
|
if (!body || typeof body !== 'object') return;
|
||||||
@@ -251,18 +300,40 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const req = https.request(options, (res) => {
|
const req = https.request(options, (res) => {
|
||||||
let responseData = '';
|
let responseData = Buffer.alloc(0);
|
||||||
|
|
||||||
res.on('data', (chunk) => {
|
res.on('data', (chunk) => {
|
||||||
responseData += chunk;
|
responseData = Buffer.concat([responseData, chunk]);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.on('end', () => {
|
res.on('end', () => {
|
||||||
try {
|
try {
|
||||||
|
let bodyString = '';
|
||||||
|
|
||||||
|
// 根据Content-Encoding处理响应数据
|
||||||
|
const contentEncoding = res.headers['content-encoding'];
|
||||||
|
if (contentEncoding === 'gzip') {
|
||||||
|
try {
|
||||||
|
bodyString = zlib.gunzipSync(responseData).toString('utf8');
|
||||||
|
} catch (unzipError) {
|
||||||
|
logger.error('❌ Failed to decompress gzip response:', unzipError);
|
||||||
|
bodyString = responseData.toString('utf8');
|
||||||
|
}
|
||||||
|
} else if (contentEncoding === 'deflate') {
|
||||||
|
try {
|
||||||
|
bodyString = zlib.inflateSync(responseData).toString('utf8');
|
||||||
|
} catch (unzipError) {
|
||||||
|
logger.error('❌ Failed to decompress deflate response:', unzipError);
|
||||||
|
bodyString = responseData.toString('utf8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bodyString = responseData.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
statusCode: res.statusCode,
|
statusCode: res.statusCode,
|
||||||
headers: res.headers,
|
headers: res.headers,
|
||||||
body: responseData
|
body: bodyString
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug(`🔗 Claude API response: ${res.statusCode}`);
|
logger.debug(`🔗 Claude API response: ${res.statusCode}`);
|
||||||
@@ -505,7 +576,7 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
if (!responseStream.destroyed) {
|
if (!responseStream.destroyed) {
|
||||||
// 发送 SSE 错误事件
|
// 发送 SSE 错误事件
|
||||||
responseStream.write(`event: error\n`);
|
responseStream.write('event: error\n');
|
||||||
responseStream.write(`data: ${JSON.stringify({
|
responseStream.write(`data: ${JSON.stringify({
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
code: error.code,
|
code: error.code,
|
||||||
@@ -528,7 +599,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
if (!responseStream.destroyed) {
|
if (!responseStream.destroyed) {
|
||||||
// 发送 SSE 错误事件
|
// 发送 SSE 错误事件
|
||||||
responseStream.write(`event: error\n`);
|
responseStream.write('event: error\n');
|
||||||
responseStream.write(`data: ${JSON.stringify({
|
responseStream.write(`data: ${JSON.stringify({
|
||||||
error: 'Request timeout',
|
error: 'Request timeout',
|
||||||
code: 'TIMEOUT',
|
code: 'TIMEOUT',
|
||||||
@@ -635,7 +706,7 @@ class ClaudeRelayService {
|
|||||||
|
|
||||||
if (!responseStream.destroyed) {
|
if (!responseStream.destroyed) {
|
||||||
// 发送 SSE 错误事件
|
// 发送 SSE 错误事件
|
||||||
responseStream.write(`event: error\n`);
|
responseStream.write('event: error\n');
|
||||||
responseStream.write(`data: ${JSON.stringify({
|
responseStream.write(`data: ${JSON.stringify({
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
code: error.code,
|
code: error.code,
|
||||||
@@ -658,7 +729,7 @@ class ClaudeRelayService {
|
|||||||
}
|
}
|
||||||
if (!responseStream.destroyed) {
|
if (!responseStream.destroyed) {
|
||||||
// 发送 SSE 错误事件
|
// 发送 SSE 错误事件
|
||||||
responseStream.write(`event: error\n`);
|
responseStream.write('event: error\n');
|
||||||
responseStream.write(`data: ${JSON.stringify({
|
responseStream.write(`data: ${JSON.stringify({
|
||||||
error: 'Request timeout',
|
error: 'Request timeout',
|
||||||
code: 'TIMEOUT',
|
code: 'TIMEOUT',
|
||||||
|
|||||||
Reference in New Issue
Block a user