fix 修复openai格式流式响应的结束标记问题

This commit is contained in:
shaw
2025-07-22 18:00:24 +08:00
parent e77945a3e3
commit a431778363
7 changed files with 47 additions and 33 deletions

View File

@@ -137,7 +137,7 @@ router.post('/messages', authenticateApiKey, async (req, res) => {
// 处理速率限制
if (error.status === 429) {
if (apiKeyData && req.account) {
if (req.apiKey && req.account) {
await geminiAccountService.setAccountRateLimited(req.account.id, true);
}
}

View File

@@ -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' }
);

View File

@@ -48,13 +48,7 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
model = 'gemini-2.0-flash-exp',
temperature = 0.7,
max_tokens = 4096,
stream = false,
n = 1,
stop = null,
presence_penalty = 0,
frequency_penalty = 0,
logit_bias = null,
user = null
stream = false
} = req.body;
// 验证必需参数
@@ -159,7 +153,7 @@ router.post('/v1/chat/completions', authenticateApiKey, async (req, res) => {
// 处理速率限制
if (error.status === 429) {
if (apiKeyData && req.account) {
if (req.apiKey && req.account) {
await geminiAccountService.setAccountRateLimited(req.account.id, true);
}
}

View File

@@ -118,7 +118,7 @@ class ClaudeCodeHeadersService {
// 获取当前存储的 headers
const key = `claude_code_headers:${accountId}`;
const currentData = await redis.get(key);
const currentData = await redis.getClient().get(key);
if (currentData) {
const current = JSON.parse(currentData);
@@ -137,7 +137,7 @@ class ClaudeCodeHeadersService {
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}`);
@@ -152,7 +152,7 @@ class ClaudeCodeHeadersService {
async getAccountHeaders(accountId) {
try {
const key = `claude_code_headers:${accountId}`;
const data = await redis.get(key);
const data = await redis.getClient().get(key);
if (data) {
const parsed = JSON.parse(data);
@@ -176,7 +176,7 @@ class ClaudeCodeHeadersService {
async clearAccountHeaders(accountId) {
try {
const key = `claude_code_headers:${accountId}`;
await redis.del(key);
await redis.getClient().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);
@@ -189,12 +189,12 @@ class ClaudeCodeHeadersService {
async getAllAccountHeaders() {
try {
const pattern = 'claude_code_headers:*';
const keys = await redis.keys(pattern);
const keys = await redis.getClient().keys(pattern);
const results = {};
for (const key of keys) {
const accountId = key.replace('claude_code_headers:', '');
const data = await redis.get(key);
const data = await redis.getClient().get(key);
if (data) {
results[accountId] = JSON.parse(data);
}

View File

@@ -99,14 +99,16 @@ class OpenAIToClaudeConverter {
* 转换流式响应的单个数据块
* @param {String} chunk - Claude SSE 数据块
* @param {String} requestModel - 原始请求的模型名
* @param {String} sessionId - 会话ID
* @returns {String} OpenAI 格式的 SSE 数据块
*/
convertStreamChunk(chunk, requestModel) {
convertStreamChunk(chunk, requestModel, sessionId) {
if (!chunk || chunk.trim() === '') return '';
// 解析 SSE 数据
const lines = chunk.split('\n');
let convertedChunks = [];
let hasMessageStop = false;
for (const line of lines) {
if (line.startsWith('data: ')) {
@@ -118,18 +120,27 @@ class OpenAIToClaudeConverter {
try {
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) {
convertedChunks.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
}
} catch (e) {
// 如果不是 JSON原样传递
convertedChunks.push(line + '\n');
// 跳过无法解析的数据不传递非JSON格式的行
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('');
@@ -331,10 +342,10 @@ class OpenAIToClaudeConverter {
/**
* 转换流式事件
*/
_convertStreamEvent(event, requestModel) {
_convertStreamEvent(event, requestModel, sessionId) {
const timestamp = Math.floor(Date.now() / 1000);
const baseChunk = {
id: `chatcmpl-${this._generateId()}`,
id: sessionId,
object: 'chat.completion.chunk',
created: timestamp,
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') {
baseChunk.choices[0].delta.content = event.content_block.text || '';
} else if (event.content_block.type === 'tool_use') {
@@ -381,7 +396,11 @@ class OpenAIToClaudeConverter {
baseChunk.usage = this._convertUsage(event.usage);
}
} else if (event.type === 'message_stop') {
baseChunk.choices[0].finish_reason = 'stop';
// message_stop 事件不需要返回 chunk[DONE] 标记会在 convertStreamChunk 中添加
return null;
} else {
// 忽略其他类型的事件
return null;
}
return baseChunk;

View File

@@ -1,9 +1,6 @@
const redis = require('../models/redis');
const logger = require('../utils/logger');
const { v4: uuidv4 } = require('uuid');
const {
logRefreshSkipped
} = require('../utils/tokenRefreshLogger');
/**
* Token 刷新锁服务

View File

@@ -1,7 +1,7 @@
const winston = require('winston');
const path = require('path');
const fs = require('fs');
const { maskToken, formatTokenRefreshLog } = require('./tokenMask');
const { maskToken } = require('./tokenMask');
// 确保日志目录存在
const logDir = path.join(process.cwd(), 'logs');