mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
fix 修复openai格式流式响应的结束标记问题
This commit is contained in:
@@ -137,7 +137,7 @@ router.post('/messages', authenticateApiKey, async (req, res) => {
|
|||||||
|
|
||||||
// 处理速率限制
|
// 处理速率限制
|
||||||
if (error.status === 429) {
|
if (error.status === 429) {
|
||||||
if (apiKeyData && req.account) {
|
if (req.apiKey && req.account) {
|
||||||
await geminiAccountService.setAccountRateLimited(req.account.id, true);
|
await geminiAccountService.setAccountRateLimited(req.account.id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -265,9 +265,13 @@ async function handleChatCompletion(req, res, apiKeyData) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 流转换器
|
// 流转换器
|
||||||
(chunk) => {
|
(() => {
|
||||||
return openaiToClaude.convertStreamChunk(chunk, req.body.model);
|
// 为每个请求创建独立的会话ID
|
||||||
},
|
const sessionId = `chatcmpl-${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
|
||||||
|
return (chunk) => {
|
||||||
|
return openaiToClaude.convertStreamChunk(chunk, req.body.model, sessionId);
|
||||||
|
};
|
||||||
|
})(),
|
||||||
{ betaHeader: 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14' }
|
{ betaHeader: 'oauth-2025-04-20,claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14' }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,13 +48,7 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
|||||||
model = 'gemini-2.0-flash-exp',
|
model = 'gemini-2.0-flash-exp',
|
||||||
temperature = 0.7,
|
temperature = 0.7,
|
||||||
max_tokens = 4096,
|
max_tokens = 4096,
|
||||||
stream = false,
|
stream = false
|
||||||
n = 1,
|
|
||||||
stop = null,
|
|
||||||
presence_penalty = 0,
|
|
||||||
frequency_penalty = 0,
|
|
||||||
logit_bias = null,
|
|
||||||
user = null
|
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// 验证必需参数
|
// 验证必需参数
|
||||||
@@ -159,7 +153,7 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
|
|||||||
|
|
||||||
// 处理速率限制
|
// 处理速率限制
|
||||||
if (error.status === 429) {
|
if (error.status === 429) {
|
||||||
if (apiKeyData && req.account) {
|
if (req.apiKey && req.account) {
|
||||||
await geminiAccountService.setAccountRateLimited(req.account.id, true);
|
await geminiAccountService.setAccountRateLimited(req.account.id, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class ClaudeCodeHeadersService {
|
|||||||
|
|
||||||
// 获取当前存储的 headers
|
// 获取当前存储的 headers
|
||||||
const key = `claude_code_headers:${accountId}`;
|
const key = `claude_code_headers:${accountId}`;
|
||||||
const currentData = await redis.get(key);
|
const currentData = await redis.getClient().get(key);
|
||||||
|
|
||||||
if (currentData) {
|
if (currentData) {
|
||||||
const current = JSON.parse(currentData);
|
const current = JSON.parse(currentData);
|
||||||
@@ -137,7 +137,7 @@ class ClaudeCodeHeadersService {
|
|||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
await redis.setex(key, 86400 * 7, JSON.stringify(data)); // 7天过期
|
await redis.getClient().setex(key, 86400 * 7, JSON.stringify(data)); // 7天过期
|
||||||
|
|
||||||
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`);
|
logger.info(`✅ Stored Claude Code headers for account ${accountId}, version: ${version}`);
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class ClaudeCodeHeadersService {
|
|||||||
async getAccountHeaders(accountId) {
|
async getAccountHeaders(accountId) {
|
||||||
try {
|
try {
|
||||||
const key = `claude_code_headers:${accountId}`;
|
const key = `claude_code_headers:${accountId}`;
|
||||||
const data = await redis.get(key);
|
const data = await redis.getClient().get(key);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
@@ -176,7 +176,7 @@ class ClaudeCodeHeadersService {
|
|||||||
async clearAccountHeaders(accountId) {
|
async clearAccountHeaders(accountId) {
|
||||||
try {
|
try {
|
||||||
const key = `claude_code_headers:${accountId}`;
|
const key = `claude_code_headers:${accountId}`;
|
||||||
await redis.del(key);
|
await redis.getClient().del(key);
|
||||||
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`);
|
logger.info(`🗑️ Cleared Claude Code headers for account ${accountId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error);
|
logger.error(`❌ Failed to clear Claude Code headers for account ${accountId}:`, error);
|
||||||
@@ -189,12 +189,12 @@ class ClaudeCodeHeadersService {
|
|||||||
async getAllAccountHeaders() {
|
async getAllAccountHeaders() {
|
||||||
try {
|
try {
|
||||||
const pattern = 'claude_code_headers:*';
|
const pattern = 'claude_code_headers:*';
|
||||||
const keys = await redis.keys(pattern);
|
const keys = await redis.getClient().keys(pattern);
|
||||||
|
|
||||||
const results = {};
|
const results = {};
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const accountId = key.replace('claude_code_headers:', '');
|
const accountId = key.replace('claude_code_headers:', '');
|
||||||
const data = await redis.get(key);
|
const data = await redis.getClient().get(key);
|
||||||
if (data) {
|
if (data) {
|
||||||
results[accountId] = JSON.parse(data);
|
results[accountId] = JSON.parse(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,14 +99,16 @@ class OpenAIToClaudeConverter {
|
|||||||
* 转换流式响应的单个数据块
|
* 转换流式响应的单个数据块
|
||||||
* @param {String} chunk - Claude SSE 数据块
|
* @param {String} chunk - Claude SSE 数据块
|
||||||
* @param {String} requestModel - 原始请求的模型名
|
* @param {String} requestModel - 原始请求的模型名
|
||||||
|
* @param {String} sessionId - 会话ID
|
||||||
* @returns {String} OpenAI 格式的 SSE 数据块
|
* @returns {String} OpenAI 格式的 SSE 数据块
|
||||||
*/
|
*/
|
||||||
convertStreamChunk(chunk, requestModel) {
|
convertStreamChunk(chunk, requestModel, sessionId) {
|
||||||
if (!chunk || chunk.trim() === '') return '';
|
if (!chunk || chunk.trim() === '') return '';
|
||||||
|
|
||||||
// 解析 SSE 数据
|
// 解析 SSE 数据
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split('\n');
|
||||||
let convertedChunks = [];
|
let convertedChunks = [];
|
||||||
|
let hasMessageStop = false;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
@@ -118,18 +120,27 @@ class OpenAIToClaudeConverter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const claudeEvent = JSON.parse(data);
|
const claudeEvent = JSON.parse(data);
|
||||||
const openaiChunk = this._convertStreamEvent(claudeEvent, requestModel);
|
|
||||||
|
// 检查是否是 message_stop 事件
|
||||||
|
if (claudeEvent.type === 'message_stop') {
|
||||||
|
hasMessageStop = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openaiChunk = this._convertStreamEvent(claudeEvent, requestModel, sessionId);
|
||||||
if (openaiChunk) {
|
if (openaiChunk) {
|
||||||
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
|
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 如果不是 JSON,原样传递
|
// 跳过无法解析的数据,不传递非JSON格式的行
|
||||||
convertedChunks.push(line + '\n');
|
continue;
|
||||||
}
|
}
|
||||||
} else if (line.startsWith('event:') || line === '') {
|
|
||||||
// 保留事件类型行和空行
|
|
||||||
convertedChunks.push(line + '\n');
|
|
||||||
}
|
}
|
||||||
|
// 忽略 event: 行和空行,OpenAI 格式不包含这些
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果收到 message_stop 事件,添加 [DONE] 标记
|
||||||
|
if (hasMessageStop) {
|
||||||
|
convertedChunks.push('data: [DONE]\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertedChunks.join('');
|
return convertedChunks.join('');
|
||||||
@@ -331,10 +342,10 @@ class OpenAIToClaudeConverter {
|
|||||||
/**
|
/**
|
||||||
* 转换流式事件
|
* 转换流式事件
|
||||||
*/
|
*/
|
||||||
_convertStreamEvent(event, requestModel) {
|
_convertStreamEvent(event, requestModel, sessionId) {
|
||||||
const timestamp = Math.floor(Date.now() / 1000);
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
const baseChunk = {
|
const baseChunk = {
|
||||||
id: `chatcmpl-${this._generateId()}`,
|
id: sessionId,
|
||||||
object: 'chat.completion.chunk',
|
object: 'chat.completion.chunk',
|
||||||
created: timestamp,
|
created: timestamp,
|
||||||
model: requestModel || 'gpt-4',
|
model: requestModel || 'gpt-4',
|
||||||
@@ -346,7 +357,11 @@ class OpenAIToClaudeConverter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 根据事件类型处理
|
// 根据事件类型处理
|
||||||
if (event.type === 'content_block_start' && event.content_block) {
|
if (event.type === 'message_start') {
|
||||||
|
// 处理消息开始事件,发送角色信息
|
||||||
|
baseChunk.choices[0].delta.role = 'assistant';
|
||||||
|
return baseChunk;
|
||||||
|
} else 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') {
|
} else if (event.content_block.type === 'tool_use') {
|
||||||
@@ -381,7 +396,11 @@ class OpenAIToClaudeConverter {
|
|||||||
baseChunk.usage = this._convertUsage(event.usage);
|
baseChunk.usage = this._convertUsage(event.usage);
|
||||||
}
|
}
|
||||||
} else if (event.type === 'message_stop') {
|
} else if (event.type === 'message_stop') {
|
||||||
baseChunk.choices[0].finish_reason = 'stop';
|
// message_stop 事件不需要返回 chunk,[DONE] 标记会在 convertStreamChunk 中添加
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
// 忽略其他类型的事件
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseChunk;
|
return baseChunk;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
const redis = require('../models/redis');
|
const redis = require('../models/redis');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const {
|
|
||||||
logRefreshSkipped
|
|
||||||
} = require('../utils/tokenRefreshLogger');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token 刷新锁服务
|
* Token 刷新锁服务
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const winston = require('winston');
|
const winston = require('winston');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { maskToken, formatTokenRefreshLog } = require('./tokenMask');
|
const { maskToken } = require('./tokenMask');
|
||||||
|
|
||||||
// 确保日志目录存在
|
// 确保日志目录存在
|
||||||
const logDir = path.join(process.cwd(), 'logs');
|
const logDir = path.join(process.cwd(), 'logs');
|
||||||
|
|||||||
Reference in New Issue
Block a user