mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 实现 Claude Code headers 动态管理功能
- 创建 claudeCodeHeadersService 管理各账号的 Claude Code headers - 自动捕获成功请求的 headers 并按账号存储在 Redis - 智能版本管理,只保留最新版本的 headers - OpenAI 转发时根据账号动态获取对应的 headers - 添加管理端点查看和清除各账号的 headers 信息 - 完整支持 Claude Code 必需的 beta headers 解决了 "This credential is only authorized for use with Claude Code" 错误 避免了固定版本号带来的风控问题 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
109
docs/claude-code-headers.md
Normal file
109
docs/claude-code-headers.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Claude Code Headers 动态管理功能
|
||||
|
||||
## 概述
|
||||
|
||||
该功能自动捕获和管理不同 Claude 账号使用的 Claude Code 客户端 headers,实现版本动态跟踪和避免风控。
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **自动捕获**: 从 `/api` 网关的成功请求中自动捕获 Claude Code headers
|
||||
2. **版本管理**: 根据 user-agent 中的版本号智能更新,只保留最新版本
|
||||
3. **账号隔离**: 每个 Claude 账号独立存储 headers,避免版本混用
|
||||
4. **智能降级**: OpenAI 转发时优先使用捕获的 headers,无数据时使用默认值
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. Headers 捕获(claudeRelayService.js)
|
||||
- 在请求成功(200/201)后自动捕获客户端 headers
|
||||
- 提取 Claude Code 特定的 headers(x-stainless-*, x-app, user-agent 等)
|
||||
- 根据版本号决定是否更新存储
|
||||
|
||||
### 2. Headers 存储(Redis)
|
||||
- Key: `claude_code_headers:{accountId}`
|
||||
- 数据结构:
|
||||
```json
|
||||
{
|
||||
"headers": {
|
||||
"x-stainless-retry-count": "0",
|
||||
"x-stainless-timeout": "60",
|
||||
"x-stainless-lang": "js",
|
||||
"x-stainless-package-version": "0.55.1",
|
||||
"x-stainless-os": "Windows",
|
||||
"x-stainless-arch": "x64",
|
||||
"x-stainless-runtime": "node",
|
||||
"x-stainless-runtime-version": "v20.19.2",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"x-app": "cli",
|
||||
"user-agent": "claude-cli/1.0.57 (external, cli)",
|
||||
"accept-language": "*",
|
||||
"sec-fetch-mode": "cors"
|
||||
},
|
||||
"version": "1.0.57",
|
||||
"updatedAt": "2025-01-22T10:00:00.000Z"
|
||||
}
|
||||
```
|
||||
- TTL: 7天自动过期
|
||||
|
||||
### 3. Headers 使用(openaiClaudeRoutes.js)
|
||||
- OpenAI 格式转发时,根据选定的 Claude 账号获取对应的 headers
|
||||
- 自动添加完整的 beta headers 以支持 Claude Code 功能
|
||||
|
||||
## API 端点
|
||||
|
||||
### 查看所有账号的 headers
|
||||
```
|
||||
GET /admin/claude-code-headers
|
||||
```
|
||||
|
||||
响应示例:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"accountId": "account_123",
|
||||
"accountName": "Claude Account 1",
|
||||
"version": "1.0.57",
|
||||
"userAgent": "claude-cli/1.0.57 (external, cli)",
|
||||
"updatedAt": "2025-01-22T10:00:00.000Z",
|
||||
"headers": { ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 清除账号的 headers
|
||||
```
|
||||
DELETE /admin/claude-code-headers/:accountId
|
||||
```
|
||||
|
||||
## 默认 Headers
|
||||
|
||||
当账号没有捕获到 headers 时,使用以下默认值:
|
||||
- claude-cli/1.0.57 (external, cli)
|
||||
- x-stainless-package-version: 0.55.1
|
||||
- 其他必要的 Claude Code headers
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **版本一致性**: 确保同一账号使用相同版本的 headers,避免触发风控
|
||||
2. **自动更新**: 系统会自动使用更高版本的 headers 更新存储
|
||||
3. **Beta Headers**: OpenAI 转发时自动添加必要的 beta headers:
|
||||
- oauth-2025-04-20
|
||||
- claude-code-20250219
|
||||
- interleaved-thinking-2025-05-14
|
||||
- fine-grained-tool-streaming-2025-05-14
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Headers 未被捕获
|
||||
- 检查请求是否成功(200/201 状态码)
|
||||
- 确认请求包含有效的 user-agent(含 claude-cli)
|
||||
|
||||
### 版本未更新
|
||||
- 系统只接受更高版本的 headers
|
||||
- 检查新版本号是否确实高于当前存储版本
|
||||
|
||||
### OpenAI 转发仍报错
|
||||
- 检查 beta headers 是否正确配置
|
||||
- 确认账号已有存储的 headers 或默认值可用
|
||||
@@ -8,6 +8,7 @@ const logger = require('../utils/logger');
|
||||
const oauthHelper = require('../utils/oauthHelper');
|
||||
const CostCalculator = require('../utils/costCalculator');
|
||||
const pricingService = require('../services/pricingService');
|
||||
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -1558,4 +1559,52 @@ router.get('/usage-costs', authenticateAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 📋 获取所有账号的 Claude Code headers 信息
|
||||
router.get('/claude-code-headers', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const allHeaders = await claudeCodeHeadersService.getAllAccountHeaders();
|
||||
|
||||
// 获取所有 Claude 账号信息
|
||||
const accounts = await claudeAccountService.getAllAccounts();
|
||||
const accountMap = {};
|
||||
accounts.forEach(account => {
|
||||
accountMap[account.id] = account.name;
|
||||
});
|
||||
|
||||
// 格式化输出
|
||||
const formattedData = Object.entries(allHeaders).map(([accountId, data]) => ({
|
||||
accountId,
|
||||
accountName: accountMap[accountId] || 'Unknown',
|
||||
version: data.version,
|
||||
userAgent: data.headers['user-agent'],
|
||||
updatedAt: data.updatedAt,
|
||||
headers: data.headers
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: formattedData
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get Claude Code headers:', error);
|
||||
res.status(500).json({ error: 'Failed to get Claude Code headers', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 🗑️ 清除指定账号的 Claude Code headers
|
||||
router.delete('/claude-code-headers/:accountId', authenticateAdmin, async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
await claudeCodeHeadersService.clearAccountHeaders(accountId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Claude Code headers cleared for account ${accountId}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to clear Claude Code headers:', error);
|
||||
res.status(500).json({ error: 'Failed to clear Claude Code headers', message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -12,6 +12,9 @@ const { authenticateApiKey } = require('../middleware/auth');
|
||||
const claudeRelayService = require('../services/claudeRelayService');
|
||||
const openaiToClaude = require('../services/openaiToClaude');
|
||||
const apiKeyService = require('../services/apiKeyService');
|
||||
const claudeAccountService = require('../services/claudeAccountService');
|
||||
const claudeCodeHeadersService = require('../services/claudeCodeHeadersService');
|
||||
const sessionHelper = require('../utils/sessionHelper');
|
||||
|
||||
// 加载模型定价数据
|
||||
let modelPricingData = {};
|
||||
@@ -199,6 +202,19 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成会话哈希用于sticky会话
|
||||
const sessionHash = sessionHelper.generateSessionHash(claudeRequest);
|
||||
|
||||
// 选择可用的Claude账户
|
||||
const accountId = await claudeAccountService.selectAccountForApiKey(apiKeyData, sessionHash);
|
||||
|
||||
// 获取该账号存储的 Claude Code headers
|
||||
const claudeCodeHeaders = await claudeCodeHeadersService.getAccountHeaders(accountId);
|
||||
|
||||
logger.debug(`📋 Using Claude Code headers for account ${accountId}:`, {
|
||||
userAgent: claudeCodeHeaders['user-agent']
|
||||
});
|
||||
|
||||
// 处理流式请求
|
||||
if (claudeRequest.stream) {
|
||||
logger.info(`🌊 Processing OpenAI stream request for model: ${req.body.model}`);
|
||||
@@ -221,12 +237,12 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
||||
}
|
||||
});
|
||||
|
||||
// 使用转换后的响应流 (使用 OAuth-only beta header,不传递客户端 headers)
|
||||
// 使用转换后的响应流 (使用 OAuth-only beta header,添加 Claude Code 必需的 headers)
|
||||
await claudeRelayService.relayStreamRequestWithUsageCapture(
|
||||
claudeRequest,
|
||||
apiKeyData,
|
||||
res,
|
||||
{},
|
||||
claudeCodeHeaders,
|
||||
(usage) => {
|
||||
// 记录使用统计
|
||||
if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) {
|
||||
@@ -252,20 +268,20 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
||||
(chunk) => {
|
||||
return openaiToClaude.convertStreamChunk(chunk, req.body.model);
|
||||
},
|
||||
{ betaHeader: 'oauth-2025-04-20' }
|
||||
{ betaHeader: 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14' }
|
||||
);
|
||||
|
||||
} else {
|
||||
// 非流式请求
|
||||
logger.info(`📄 Processing OpenAI non-stream request for model: ${req.body.model}`);
|
||||
|
||||
// 发送请求到 Claude (使用 OAuth-only beta header,不传递客户端 headers)
|
||||
// 发送请求到 Claude (使用 OAuth-only beta header,添加 Claude Code 必需的 headers)
|
||||
const claudeResponse = await claudeRelayService.relayRequest(
|
||||
claudeRequest,
|
||||
apiKeyData,
|
||||
req,
|
||||
res,
|
||||
{},
|
||||
claudeCodeHeaders,
|
||||
{ betaHeader: 'oauth-2025-04-20' }
|
||||
);
|
||||
|
||||
|
||||
212
src/services/claudeCodeHeadersService.js
Normal file
212
src/services/claudeCodeHeadersService.js
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Claude Code Headers 管理服务
|
||||
* 负责存储和管理不同账号使用的 Claude Code headers
|
||||
*/
|
||||
|
||||
const redis = require('../models/redis');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
class ClaudeCodeHeadersService {
|
||||
constructor() {
|
||||
this.defaultHeaders = {
|
||||
'x-stainless-retry-count': '0',
|
||||
'x-stainless-timeout': '60',
|
||||
'x-stainless-lang': 'js',
|
||||
'x-stainless-package-version': '0.55.1',
|
||||
'x-stainless-os': 'Windows',
|
||||
'x-stainless-arch': 'x64',
|
||||
'x-stainless-runtime': 'node',
|
||||
'x-stainless-runtime-version': 'v20.19.2',
|
||||
'anthropic-dangerous-direct-browser-access': 'true',
|
||||
'x-app': 'cli',
|
||||
'user-agent': 'claude-cli/1.0.57 (external, cli)',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors'
|
||||
};
|
||||
|
||||
// 需要捕获的 Claude Code 特定 headers
|
||||
this.claudeCodeHeaderKeys = [
|
||||
'x-stainless-retry-count',
|
||||
'x-stainless-timeout',
|
||||
'x-stainless-lang',
|
||||
'x-stainless-package-version',
|
||||
'x-stainless-os',
|
||||
'x-stainless-arch',
|
||||
'x-stainless-runtime',
|
||||
'x-stainless-runtime-version',
|
||||
'anthropic-dangerous-direct-browser-access',
|
||||
'x-app',
|
||||
'user-agent',
|
||||
'accept-language',
|
||||
'sec-fetch-mode',
|
||||
'accept-encoding'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 user-agent 中提取版本号
|
||||
*/
|
||||
extractVersionFromUserAgent(userAgent) {
|
||||
if (!userAgent) return null;
|
||||
const match = userAgent.match(/claude-cli\/(\d+\.\d+\.\d+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较版本号
|
||||
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
||||
*/
|
||||
compareVersions(v1, v2) {
|
||||
if (!v1 || !v2) return 0;
|
||||
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
|
||||
if (p1 > p2) return 1;
|
||||
if (p1 < p2) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从客户端 headers 中提取 Claude Code 相关的 headers
|
||||
*/
|
||||
extractClaudeCodeHeaders(clientHeaders) {
|
||||
const headers = {};
|
||||
|
||||
// 转换所有 header keys 为小写进行比较
|
||||
const lowerCaseHeaders = {};
|
||||
Object.keys(clientHeaders || {}).forEach(key => {
|
||||
lowerCaseHeaders[key.toLowerCase()] = clientHeaders[key];
|
||||
});
|
||||
|
||||
// 提取需要的 headers
|
||||
this.claudeCodeHeaderKeys.forEach(key => {
|
||||
const lowerKey = key.toLowerCase();
|
||||
if (lowerCaseHeaders[lowerKey]) {
|
||||
headers[key] = lowerCaseHeaders[lowerKey];
|
||||
}
|
||||
});
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储账号的 Claude Code headers
|
||||
*/
|
||||
async storeAccountHeaders(accountId, clientHeaders) {
|
||||
try {
|
||||
const extractedHeaders = this.extractClaudeCodeHeaders(clientHeaders);
|
||||
|
||||
// 检查是否有 user-agent
|
||||
const userAgent = extractedHeaders['user-agent'];
|
||||
if (!userAgent || !userAgent.includes('claude-cli')) {
|
||||
// 不是 Claude Code 的请求,不存储
|
||||
return;
|
||||
}
|
||||
|
||||
const version = this.extractVersionFromUserAgent(userAgent);
|
||||
if (!version) {
|
||||
logger.warn(`⚠️ Failed to extract version from user-agent: ${userAgent}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前存储的 headers
|
||||
const key = `claude_code_headers:${accountId}`;
|
||||
const currentData = await redis.get(key);
|
||||
|
||||
if (currentData) {
|
||||
const current = JSON.parse(currentData);
|
||||
const currentVersion = this.extractVersionFromUserAgent(current.headers['user-agent']);
|
||||
|
||||
// 只有新版本更高时才更新
|
||||
if (this.compareVersions(version, currentVersion) <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 存储新的 headers
|
||||
const data = {
|
||||
headers: extractedHeaders,
|
||||
version: version,
|
||||
updatedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await redis.setex(key, 86400 * 7, JSON.stringify(data)); // 7天过期
|
||||
|
||||
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to store Claude Code headers for account ${accountId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号的 Claude Code headers
|
||||
*/
|
||||
async getAccountHeaders(accountId) {
|
||||
try {
|
||||
const key = `claude_code_headers:${accountId}`;
|
||||
const data = await redis.get(key);
|
||||
|
||||
if (data) {
|
||||
const parsed = JSON.parse(data);
|
||||
logger.debug(`📋 Retrieved Claude Code headers for account ${accountId}, version: ${parsed.version}`);
|
||||
return parsed.headers;
|
||||
}
|
||||
|
||||
// 返回默认 headers
|
||||
logger.debug(`📋 Using default Claude Code headers for account ${accountId}`);
|
||||
return this.defaultHeaders;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to get Claude Code headers for account ${accountId}:`, error);
|
||||
return this.defaultHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除账号的 Claude Code headers
|
||||
*/
|
||||
async clearAccountHeaders(accountId) {
|
||||
try {
|
||||
const key = `claude_code_headers:${accountId}`;
|
||||
await redis.del(key);
|
||||
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`);
|
||||
} catch (error) {
|
||||
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有账号的 headers 信息
|
||||
*/
|
||||
async getAllAccountHeaders() {
|
||||
try {
|
||||
const pattern = 'claude_code_headers:*';
|
||||
const keys = await redis.keys(pattern);
|
||||
|
||||
const results = {};
|
||||
for (const key of keys) {
|
||||
const accountId = key.replace('claude_code_headers:', '');
|
||||
const data = await redis.get(key);
|
||||
if (data) {
|
||||
results[accountId] = JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ Failed to get all account headers:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ClaudeCodeHeadersService();
|
||||
@@ -8,6 +8,7 @@ const claudeAccountService = require('./claudeAccountService');
|
||||
const sessionHelper = require('../utils/sessionHelper');
|
||||
const logger = require('../utils/logger');
|
||||
const config = require('../../config/config');
|
||||
const claudeCodeHeadersService = require('./claudeCodeHeadersService');
|
||||
|
||||
class ClaudeRelayService {
|
||||
constructor() {
|
||||
@@ -128,6 +129,11 @@ class ClaudeRelayService {
|
||||
if (isRateLimited) {
|
||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||
}
|
||||
|
||||
// 存储成功请求的 Claude Code headers
|
||||
if (clientHeaders && Object.keys(clientHeaders).length > 0) {
|
||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
// 记录成功的API调用
|
||||
@@ -651,6 +657,11 @@ class ClaudeRelayService {
|
||||
if (isRateLimited) {
|
||||
await claudeAccountService.removeAccountRateLimit(accountId);
|
||||
}
|
||||
|
||||
// 存储成功请求的 Claude Code headers(流式请求)
|
||||
if (clientHeaders && Object.keys(clientHeaders).length > 0) {
|
||||
await claudeCodeHeadersService.storeAccountHeaders(accountId, clientHeaders);
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('🌊 Claude stream response with usage capture completed');
|
||||
|
||||
@@ -241,21 +241,22 @@ const originalError = logger.error;
|
||||
const originalWarn = logger.warn;
|
||||
const originalInfo = logger.info;
|
||||
|
||||
logger.error = function(message, metadata = {}) {
|
||||
logger.error = function(message, ...args) {
|
||||
logger.stats.errors++;
|
||||
return originalError.call(this, message, metadata);
|
||||
return originalError.call(this, message, ...args);
|
||||
};
|
||||
|
||||
logger.warn = function(message, metadata = {}) {
|
||||
logger.warn = function(message, ...args) {
|
||||
logger.stats.warnings++;
|
||||
return originalWarn.call(this, message, metadata);
|
||||
return originalWarn.call(this, message, ...args);
|
||||
};
|
||||
|
||||
logger.info = function(message, metadata = {}) {
|
||||
if (metadata.type === 'request') {
|
||||
logger.info = function(message, ...args) {
|
||||
// 检查是否是请求类型的日志
|
||||
if (args.length > 0 && typeof args[0] === 'object' && args[0].type === 'request') {
|
||||
logger.stats.requests++;
|
||||
}
|
||||
return originalInfo.call(this, message, metadata);
|
||||
return originalInfo.call(this, message, ...args);
|
||||
};
|
||||
|
||||
// 📈 获取日志统计
|
||||
|
||||
Reference in New Issue
Block a user