Merge pull request #204 from shinegod/feature/claude-code-client-support

feat: 全面增强 Claude Code 客户端支持与错误处理
This commit is contained in:
Wesley Liddick
2025-08-06 23:21:51 +08:00
committed by GitHub
3 changed files with 136 additions and 23 deletions

View File

@@ -253,9 +253,10 @@ async function handleMessagesRequest(req, res) {
res.status(response.statusCode);
// 设置响应头
// 设置响应头,避免 Content-Length 和 Transfer-Encoding 冲突
const skipHeaders = ['content-encoding', 'transfer-encoding', 'content-length'];
Object.keys(response.headers).forEach(key => {
if (key.toLowerCase() !== 'content-encoding') {
if (!skipHeaders.includes(key.toLowerCase())) {
res.setHeader(key, response.headers[key]);
}
});
@@ -355,6 +356,51 @@ router.post('/v1/messages', authenticateApiKey, handleMessagesRequest);
// 🚀 Claude API messages 端点 - /claude/v1/messages (别名)
router.post('/claude/v1/messages', authenticateApiKey, handleMessagesRequest);
// 📋 模型列表端点 - Claude Code 客户端需要
router.get('/v1/models', authenticateApiKey, async (req, res) => {
try {
// 返回支持的模型列表
const models = [
{
id: 'claude-3-5-sonnet-20241022',
object: 'model',
created: 1669599635,
owned_by: 'anthropic'
},
{
id: 'claude-3-5-haiku-20241022',
object: 'model',
created: 1669599635,
owned_by: 'anthropic'
},
{
id: 'claude-3-opus-20240229',
object: 'model',
created: 1669599635,
owned_by: 'anthropic'
},
{
id: 'claude-sonnet-4-20250514',
object: 'model',
created: 1669599635,
owned_by: 'anthropic'
}
];
res.json({
object: 'list',
data: models
});
} catch (error) {
logger.error('❌ Models list error:', error);
res.status(500).json({
error: 'Failed to get models list',
message: error.message
});
}
});
// 🏥 健康检查端点
router.get('/health', async (req, res) => {
try {
@@ -422,4 +468,46 @@ router.get('/v1/usage', authenticateApiKey, async (req, res) => {
}
});
// 👤 用户信息端点 - Claude Code 客户端需要
router.get('/v1/me', authenticateApiKey, async (req, res) => {
try {
// 返回基础用户信息
res.json({
id: 'user_' + req.apiKey.id,
type: 'user',
display_name: req.apiKey.name || 'API User',
created_at: new Date().toISOString()
});
} catch (error) {
logger.error('❌ User info error:', error);
res.status(500).json({
error: 'Failed to get user info',
message: error.message
});
}
});
// 💰 余额/限制端点 - Claude Code 客户端需要
router.get('/v1/organizations/:org_id/usage', authenticateApiKey, async (req, res) => {
try {
const usage = await apiKeyService.getUsageStats(req.apiKey.id);
res.json({
object: 'usage',
data: [
{
type: 'credit_balance',
credit_balance: req.apiKey.tokenLimit - (usage.totalTokens || 0)
}
]
});
} catch (error) {
logger.error('❌ Organization usage error:', error);
res.status(500).json({
error: 'Failed to get usage info',
message: error.message
});
}
});
module.exports = router;

View File

@@ -86,11 +86,8 @@ class ClaudeConsoleRelayService {
data: modifiedRequestBody,
headers: {
'Content-Type': 'application/json',
'x-api-key': account.apiKey,
'anthropic-version': '2023-06-01',
'User-Agent': account.userAgent || this.defaultUserAgent,
'anthropic-dangerous-direct-browser-access': true,
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
...filteredHeaders
},
httpsAgent: proxyAgent,
@@ -99,6 +96,17 @@ class ClaudeConsoleRelayService {
validateStatus: () => true // 接受所有状态码
};
// 根据 API Key 格式选择认证方式
if (account.apiKey && account.apiKey.startsWith('sk-ant-')) {
// Anthropic 官方 API Key 使用 x-api-key
requestConfig.headers['x-api-key'] = account.apiKey;
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key');
} else {
// 其他 API Key 使用 Authorization Bearer
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`;
logger.debug('[DEBUG] Using Authorization Bearer authentication');
}
logger.debug(`[DEBUG] Initial headers before beta: ${JSON.stringify(requestConfig.headers, null, 2)}`);
// 添加beta header如果需要
@@ -230,18 +238,27 @@ class ClaudeConsoleRelayService {
data: body,
headers: {
'Content-Type': 'application/json',
'x-api-key': account.apiKey,
'anthropic-version': '2023-06-01',
'User-Agent': account.userAgent || this.defaultUserAgent,
'anthropic-dangerous-direct-browser-access': true,
'anthropic-beta': 'fine-grained-tool-streaming-2025-05-14',
...filteredHeaders
},
httpsAgent: proxyAgent,
timeout: config.proxy.timeout || 60000,
responseType: 'stream'
responseType: 'stream',
validateStatus: () => true // 接受所有状态码
};
// 根据 API Key 格式选择认证方式
if (account.apiKey && account.apiKey.startsWith('sk-ant-')) {
// Anthropic 官方 API Key 使用 x-api-key
requestConfig.headers['x-api-key'] = account.apiKey;
logger.debug('[DEBUG] Using x-api-key authentication for sk-ant-* API key');
} else {
// 其他 API Key 使用 Authorization Bearer
requestConfig.headers['Authorization'] = `Bearer ${account.apiKey}`;
logger.debug('[DEBUG] Using Authorization Bearer authentication');
}
// 添加beta header如果需要
if (requestOptions.betaHeader) {
requestConfig.headers['anthropic-beta'] = requestOptions.betaHeader;
@@ -261,24 +278,31 @@ class ClaudeConsoleRelayService {
claudeConsoleAccountService.markAccountRateLimited(accountId);
}
// 收集错误数据
let errorData = '';
// 设置错误响应的状态码和响应头
if (!responseStream.headersSent) {
const errorHeaders = {
'Content-Type': response.headers['content-type'] || 'application/json',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
};
// 避免 Transfer-Encoding 冲突,让 Express 自动处理
delete errorHeaders['Transfer-Encoding'];
delete errorHeaders['Content-Length'];
responseStream.writeHead(response.status, errorHeaders);
}
// 直接透传错误数据,不进行包装
response.data.on('data', chunk => {
errorData += chunk.toString();
if (!responseStream.destroyed) {
responseStream.write(chunk);
}
});
response.data.on('end', () => {
if (!responseStream.destroyed) {
responseStream.write('event: error\n');
responseStream.write(`data: ${JSON.stringify({
error: 'Claude Console API error',
status: response.status,
details: errorData,
timestamp: new Date().toISOString()
})}\n\n`);
responseStream.end();
}
reject(new Error(`Claude Console API error: ${response.status}`));
resolve(); // 不抛出异常,正常完成流处理
});
return;
}
@@ -460,14 +484,15 @@ class ClaudeConsoleRelayService {
const sensitiveHeaders = [
'content-type',
'user-agent',
'x-api-key',
'authorization',
'x-api-key',
'host',
'content-length',
'connection',
'proxy-authorization',
'content-encoding',
'transfer-encoding'
'transfer-encoding',
'anthropic-version'
];
const filteredHeaders = {};

View File

@@ -314,7 +314,7 @@ class PricingService {
// 记录初始的修改时间
let lastMtime = fs.statSync(this.pricingFile).mtimeMs;
fs.watchFile(this.pricingFile, watchOptions, (curr, prev) => {
fs.watchFile(this.pricingFile, watchOptions, (curr, _prev) => {
// 检查文件是否真的被修改了(不仅仅是访问)
if (curr.mtimeMs !== lastMtime) {
lastMtime = curr.mtimeMs;