mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +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 (apiKeyData && req.account) {
|
||||
if (req.apiKey && req.account) {
|
||||
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' }
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 刷新锁服务
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user