fix: 修复 OpenAI 兼容路由的 Claude API 认证和 Function Calling 支持

- 添加必需的系统消息 "You are Claude Code, Anthropic's official CLI for Claude."
- 修改 anthropic-beta header 为 OAuth-only 模式 (oauth-2025-04-20)
- 不再传递客户端 headers,使用固定的 4 个必需 headers
- 增强流式响应的 Function Calling 支持,正确处理 tool_use 事件
- 支持自定义 beta header 参数,允许不同路由使用不同的认证模式

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-07-22 14:38:30 +08:00
parent 6ceed5c3ee
commit b2ad2a4a61
3 changed files with 46 additions and 18 deletions

View File

@@ -221,12 +221,12 @@ async function handleChatCompletion(req, res, apiKeyData) {
} }
}); });
// 使用转换后的响应流 // 使用转换后的响应流 (使用 OAuth-only beta header不传递客户端 headers)
await claudeRelayService.relayStreamRequestWithUsageCapture( await claudeRelayService.relayStreamRequestWithUsageCapture(
claudeRequest, claudeRequest,
apiKeyData, apiKeyData,
res, res,
req.headers, {},
(usage) => { (usage) => {
// 记录使用统计 // 记录使用统计
if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) { if (usage && usage.input_tokens !== undefined && usage.output_tokens !== undefined) {
@@ -251,20 +251,22 @@ async function handleChatCompletion(req, res, apiKeyData) {
// 流转换器 // 流转换器
(chunk) => { (chunk) => {
return openaiToClaude.convertStreamChunk(chunk, req.body.model); return openaiToClaude.convertStreamChunk(chunk, req.body.model);
} },
{ betaHeader: 'oauth-2025-04-20' }
); );
} else { } else {
// 非流式请求 // 非流式请求
logger.info(`📄 Processing OpenAI non-stream request for model: ${req.body.model}`); logger.info(`📄 Processing OpenAI non-stream request for model: ${req.body.model}`);
// 发送请求到 Claude // 发送请求到 Claude (使用 OAuth-only beta header不传递客户端 headers)
const claudeResponse = await claudeRelayService.relayRequest( const claudeResponse = await claudeRelayService.relayRequest(
claudeRequest, claudeRequest,
apiKeyData, apiKeyData,
req, req,
res, res,
req.headers {},
{ betaHeader: 'oauth-2025-04-20' }
); );
// 解析 Claude 响应 // 解析 Claude 响应

View File

@@ -18,7 +18,7 @@ class ClaudeRelayService {
} }
// 🚀 转发请求到Claude API // 🚀 转发请求到Claude API
async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders) { async relayRequest(requestBody, apiKeyData, clientRequest, clientResponse, clientHeaders, options = {}) {
let upstreamRequest = null; let upstreamRequest = null;
try { try {
@@ -89,7 +89,8 @@ class ClaudeRelayService {
accessToken, accessToken,
proxyAgent, proxyAgent,
clientHeaders, clientHeaders,
(req) => { upstreamRequest = req; } (req) => { upstreamRequest = req; },
options
); );
// 移除监听器(请求成功完成) // 移除监听器(请求成功完成)
@@ -325,7 +326,7 @@ class ClaudeRelayService {
} }
// 🔗 发送请求到Claude API // 🔗 发送请求到Claude API
async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest) { async _makeClaudeRequest(body, accessToken, proxyAgent, clientHeaders, onRequest, requestOptions = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl); const url = new URL(this.claudeApiUrl);
@@ -352,8 +353,10 @@ class ClaudeRelayService {
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'; options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
} }
if (this.betaHeader) { // 使用自定义的 betaHeader 或默认值
options.headers['anthropic-beta'] = this.betaHeader; const betaHeader = requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader;
if (betaHeader) {
options.headers['anthropic-beta'] = betaHeader;
} }
const req = https.request(options, (res) => { const req = https.request(options, (res) => {
@@ -445,7 +448,7 @@ class ClaudeRelayService {
} }
// 🌊 处理流式响应带usage数据捕获 // 🌊 处理流式响应带usage数据捕获
async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback, streamTransformer = null) { async relayStreamRequestWithUsageCapture(requestBody, apiKeyData, responseStream, clientHeaders, usageCallback, streamTransformer = null, options = {}) {
try { try {
// 调试日志查看API Key数据流式请求 // 调试日志查看API Key数据流式请求
logger.info('🔍 [Stream] API Key data received:', { logger.info('🔍 [Stream] API Key data received:', {
@@ -495,7 +498,7 @@ class ClaudeRelayService {
const proxyAgent = await this._getProxyAgent(accountId); const proxyAgent = await this._getProxyAgent(accountId);
// 发送流式请求并捕获usage数据 // 发送流式请求并捕获usage数据
return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer); return await this._makeClaudeStreamRequestWithUsageCapture(processedBody, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer, options);
} catch (error) { } catch (error) {
logger.error('❌ Claude stream relay with usage capture failed:', error); logger.error('❌ Claude stream relay with usage capture failed:', error);
throw error; throw error;
@@ -503,7 +506,7 @@ class ClaudeRelayService {
} }
// 🌊 发送流式请求到Claude API带usage数据捕获 // 🌊 发送流式请求到Claude API带usage数据捕获
async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer = null) { async _makeClaudeStreamRequestWithUsageCapture(body, accessToken, proxyAgent, clientHeaders, responseStream, usageCallback, accountId, sessionHash, streamTransformer = null, requestOptions = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl); const url = new URL(this.claudeApiUrl);
@@ -530,8 +533,10 @@ class ClaudeRelayService {
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'; options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
} }
if (this.betaHeader) { // 使用自定义的 betaHeader 或默认值
options.headers['anthropic-beta'] = this.betaHeader; const betaHeader = requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader;
if (betaHeader) {
options.headers['anthropic-beta'] = betaHeader;
} }
const req = https.request(options, (res) => { const req = https.request(options, (res) => {
@@ -736,7 +741,7 @@ class ClaudeRelayService {
} }
// 🌊 发送流式请求到Claude API // 🌊 发送流式请求到Claude API
async _makeClaudeStreamRequest(body, accessToken, proxyAgent, clientHeaders, responseStream) { async _makeClaudeStreamRequest(body, accessToken, proxyAgent, clientHeaders, responseStream, requestOptions = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(this.claudeApiUrl); const url = new URL(this.claudeApiUrl);
@@ -763,8 +768,10 @@ class ClaudeRelayService {
options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)'; options.headers['User-Agent'] = 'claude-cli/1.0.53 (external, cli)';
} }
if (this.betaHeader) { // 使用自定义的 betaHeader 或默认值
options.headers['anthropic-beta'] = this.betaHeader; const betaHeader = requestOptions?.betaHeader !== undefined ? requestOptions.betaHeader : this.betaHeader;
if (betaHeader) {
options.headers['anthropic-beta'] = betaHeader;
} }
const req = https.request(options, (res) => { const req = https.request(options, (res) => {

View File

@@ -358,10 +358,29 @@ class OpenAIToClaudeConverter {
if (event.type === 'content_block_start' && event.content_block) { if (event.type === 'content_block_start' && event.content_block) {
if (event.content_block.type === 'text') { if (event.content_block.type === 'text') {
baseChunk.choices[0].delta.content = event.content_block.text || ''; baseChunk.choices[0].delta.content = event.content_block.text || '';
} else if (event.content_block.type === 'tool_use') {
// 开始工具调用
baseChunk.choices[0].delta.tool_calls = [{
index: event.index || 0,
id: event.content_block.id,
type: 'function',
function: {
name: event.content_block.name,
arguments: ''
}
}];
} }
} else if (event.type === 'content_block_delta' && event.delta) { } else if (event.type === 'content_block_delta' && event.delta) {
if (event.delta.type === 'text_delta') { if (event.delta.type === 'text_delta') {
baseChunk.choices[0].delta.content = event.delta.text || ''; baseChunk.choices[0].delta.content = event.delta.text || '';
} else if (event.delta.type === 'input_json_delta') {
// 工具调用参数的增量更新
baseChunk.choices[0].delta.tool_calls = [{
index: event.index || 0,
function: {
arguments: event.delta.partial_json || ''
}
}];
} }
} else if (event.type === 'message_delta' && event.delta) { } else if (event.type === 'message_delta' && event.delta) {
if (event.delta.stop_reason) { if (event.delta.stop_reason) {