feat: 全面增强 Claude Code 客户端支持与错误处理

## 🚀 新功能
- **智能认证系统**: 根据 API Key 格式自动选择认证方式
  - `sk-ant-*` 开头使用 `x-api-key` 认证(兼容 Anthropic 官方)
  - 其他格式使用 `Authorization: Bearer` 认证(兼容标准 REST API)
- **Claude Code 客户端完整支持**: 新增必需的 API 端点
  - `GET /v1/models` - 返回支持的模型列表
  - `GET /v1/me` - 用户信息端点
  - `GET /v1/organizations/:org_id/usage` - 使用统计查询

## 🔧 修复与优化
- **HTTP 协议合规性**: 修复响应头冲突导致的 502 错误
  - 避免同时发送 `Content-Length` 和 `Transfer-Encoding` 头部
  - 优化响应头过滤机制,确保代理兼容性
- **完全透传错误响应**: 保持上游 API 原始响应格式
  - 透传原始状态码、响应头和内容
  - 移除错误包装,直接转发原始 JSON 格式
  - 支持流式和非流式请求的错误透传
- **流式响应处理优化**:
  - 添加 `validateStatus: () => true` 配置
  - 改进错误处理逻辑,避免异常中断

## 📝 代码质量
- 修复 ESLint 代码规范警告
- 优化敏感头部过滤列表
- 改进调试日志输出

## 🎯 解决的问题
- Claude Code 客户端无法连接(502 Bad Gateway)
- 错误响应被包装而非透传原始格式
- sk-ant-* 格式 API Key 认证失败
- HTTP/2 代理环境下的响应头冲突

##  测试验证
- 本地测试完全透传上游错误响应
- Claude Code 客户端连接测试通过
- 智能认证机制验证成功
- HTTP 协议合规性确认
This commit is contained in:
shinegod
2025-08-06 14:27:57 +00:00
parent 829a06f966
commit 3616245d49
3 changed files with 137 additions and 24 deletions

View File

@@ -180,9 +180,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]);
}
});
@@ -282,6 +283,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 {
@@ -349,4 +395,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;