UNPKG

route-claudecode

Version:

Advanced routing and transformation system for Claude Code outputs to multiple AI providers

327 lines 14.1 kB
"use strict"; /** * Finish Reason修正器 * 专门修复OpenAI工具调用的finish_reason映射问题 * 确保工具调用返回正确的finish_reason */ Object.defineProperty(exports, "__esModule", { value: true }); exports.finishReasonCorrector = exports.FinishReasonCorrector = void 0; exports.correctFinishReason = correctFinishReason; exports.needsFinishReasonCorrection = needsFinishReasonCorrection; const logging_1 = require("../logging"); class FinishReasonCorrector { logger; constructor(port) { this.logger = (0, logging_1.getLogger)(port); } /** * 修正finish_reason - 核心方法 */ correctFinishReason(originalReason, responseData, context) { const startTime = Date.now(); try { // 检测响应中是否真的包含工具调用 const toolCallDetection = this.detectToolCallsInResponse(responseData, context); // 确定正确的finish_reason const correctedReason = this.determineCorrectedReason(originalReason, toolCallDetection, context); const wasCorreted = originalReason !== correctedReason; const result = { originalReason, correctedReason, wasCorreted, confidence: toolCallDetection.confidence, detectionMethod: toolCallDetection.method }; if (wasCorreted) { this.logger.warn('Finish reason corrected due to tool call detection', { originalReason, correctedReason, provider: context.provider, model: context.model, hasToolsInRequest: context.hasToolsInRequest, detectionMethod: toolCallDetection.method, confidence: toolCallDetection.confidence, duration: Date.now() - startTime }, context.requestId, 'finish-reason-correction'); } else { this.logger.debug('Finish reason validation passed', { finishReason: originalReason, hasToolCalls: toolCallDetection.hasToolCalls, confidence: toolCallDetection.confidence }, context.requestId, 'finish-reason-validation'); } return result; } catch (error) { this.logger.error('Failed to correct finish_reason', { error: error instanceof Error ? error.message : String(error), originalReason, provider: context.provider }, context.requestId, 'finish-reason-correction'); // 错误时返回原始值 return { originalReason, correctedReason: originalReason, wasCorreted: false, confidence: 0, detectionMethod: 'error_fallback' }; } } /** * 检测响应中的工具调用 */ detectToolCallsInResponse(responseData, context) { let hasToolCalls = false; let confidence = 0; let method = 'none'; let toolCallsCount = 0; if (!responseData) { return { hasToolCalls, confidence, method, toolCallsCount }; } // 方法1: 检查OpenAI格式的tool_calls字段 if (responseData.choices?.[0]?.message?.tool_calls?.length > 0) { hasToolCalls = true; confidence = 1.0; method = 'openai_tool_calls_field'; toolCallsCount = responseData.choices[0].message.tool_calls.length; return { hasToolCalls, confidence, method, toolCallsCount }; } // 方法2: 检查流式响应中的tool_calls if (responseData.choices?.[0]?.delta?.tool_calls?.length > 0) { hasToolCalls = true; confidence = 1.0; method = 'openai_delta_tool_calls'; toolCallsCount = responseData.choices[0].delta.tool_calls.length; return { hasToolCalls, confidence, method, toolCallsCount }; } // 方法3: 检查Anthropic格式的content数组 if (responseData.content && Array.isArray(responseData.content)) { const toolUseBlocks = responseData.content.filter((block) => block.type === 'tool_use'); if (toolUseBlocks.length > 0) { hasToolCalls = true; confidence = 1.0; method = 'anthropic_tool_use_blocks'; toolCallsCount = toolUseBlocks.length; return { hasToolCalls, confidence, method, toolCallsCount }; } } // 方法4: 检查文本内容中的工具调用模式 const textContent = this.extractTextContent(responseData); if (textContent) { const textDetection = this.detectToolCallsInText(textContent); if (textDetection.hasToolCalls) { hasToolCalls = true; confidence = textDetection.confidence; method = textDetection.method; toolCallsCount = textDetection.count; return { hasToolCalls, confidence, method, toolCallsCount }; } } // 方法5: 基于请求上下文的推断 if (context.hasToolsInRequest && this.shouldInferToolCall(responseData, context)) { hasToolCalls = true; confidence = 0.3; method = 'context_inference'; toolCallsCount = 1; return { hasToolCalls, confidence, method, toolCallsCount }; } return { hasToolCalls, confidence, method, toolCallsCount }; } /** * 从响应中提取文本内容 */ extractTextContent(responseData) { let textContent = ''; // OpenAI格式 if (responseData.choices?.[0]?.message?.content) { textContent = responseData.choices[0].message.content; } else if (responseData.choices?.[0]?.delta?.content) { textContent = responseData.choices[0].delta.content; } // Anthropic格式 else if (responseData.content && Array.isArray(responseData.content)) { textContent = responseData.content .filter((block) => block.type === 'text') .map((block) => block.text) .join(' '); } // 直接字符串 else if (typeof responseData === 'string') { textContent = responseData; } return textContent; } /** * 检测文本中的工具调用 */ detectToolCallsInText(text) { if (!text || typeof text !== 'string') { return { hasToolCalls: false, confidence: 0, method: 'no_text', count: 0 }; } // 高置信度模式 const highConfidencePatterns = [ { pattern: /Tool\s+call:\s*\w+\s*\(/gi, name: 'text_tool_call_format', confidence: 0.95 }, { pattern: /\{\s*"type"\s*:\s*"tool_use"/gi, name: 'anthropic_json_tool_use', confidence: 0.95 }, { pattern: /\{\s*"name"\s*:\s*"[^"]+",\s*"input"\s*:\s*\{/gi, name: 'anthropic_name_input', confidence: 0.9 }, { pattern: /工具调用\s*:\s*[\u4e00-\u9fff\w]+\s*\(/gi, name: 'chinese_tool_call', confidence: 0.9 } ]; for (const patternInfo of highConfidencePatterns) { const matches = text.match(patternInfo.pattern); if (matches && matches.length > 0) { return { hasToolCalls: true, confidence: patternInfo.confidence, method: `text_detection_${patternInfo.name}`, count: matches.length }; } } // 中等置信度模式 const mediumConfidencePatterns = [ { pattern: /\w+\s*\(\s*\{[^}]*"[^"]+"\s*:/gi, name: 'function_with_json_args', confidence: 0.7 }, { pattern: /\{\s*"id"\s*:\s*"call_[^"]+"/gi, name: 'openai_call_id', confidence: 0.8 }, { pattern: /\{\s*"function"\s*:\s*\{[^}]*"name"\s*:/gi, name: 'openai_function_object', confidence: 0.8 } ]; for (const patternInfo of mediumConfidencePatterns) { const matches = text.match(patternInfo.pattern); if (matches && matches.length > 0) { return { hasToolCalls: true, confidence: patternInfo.confidence, method: `text_detection_${patternInfo.name}`, count: matches.length }; } } // 低置信度模式 - 仅在有工具定义的上下文中使用 const lowConfidencePatterns = [ { pattern: /function_call/gi, name: 'function_call_keyword', confidence: 0.5 }, { pattern: /tool_calls/gi, name: 'tool_calls_keyword', confidence: 0.5 } ]; for (const patternInfo of lowConfidencePatterns) { const matches = text.match(patternInfo.pattern); if (matches && matches.length > 0) { return { hasToolCalls: true, confidence: patternInfo.confidence, method: `text_detection_${patternInfo.name}`, count: matches.length }; } } return { hasToolCalls: false, confidence: 0, method: 'no_patterns_matched', count: 0 }; } /** * 基于上下文推断是否应该有工具调用 */ shouldInferToolCall(responseData, context) { // 如果请求中有工具定义,但响应很短或为空,可能是工具调用被错误处理 const textContent = this.extractTextContent(responseData); // 响应为空或很短,但请求中有工具 if (context.hasToolsInRequest && (!textContent || textContent.trim().length < 10)) { return true; } // 响应中包含明显的工具调用意图但没有正确格式化 if (textContent) { const intentPatterns = [ /I'll\s+(use|call|invoke)\s+the\s+\w+\s+(tool|function)/i, /Let\s+me\s+(use|call|invoke)\s+the\s+\w+/i, /I\s+need\s+to\s+(use|call|invoke)\s+/i, /我将使用\s*[\u4e00-\u9fff]+\s*工具/i, /让我调用\s*[\u4e00-\u9fff]+/i ]; return intentPatterns.some(pattern => pattern.test(textContent)); } return false; } /** * 确定修正后的finish_reason */ determineCorrectedReason(originalReason, toolCallDetection, context) { // 如果没有检测到工具调用,保持原始reason if (!toolCallDetection.hasToolCalls) { return originalReason; } // 如果检测到工具调用,根据provider确定正确的finish_reason if (toolCallDetection.confidence >= 0.7) { // 高置信度检测,强制修正 if (context.provider === 'openai' || context.provider.includes('openai')) { return 'tool_calls'; } else if (context.provider === 'anthropic' || context.provider.includes('anthropic')) { return 'tool_use'; } else if (context.provider.includes('gemini')) { return 'tool_calls'; // Gemini使用OpenAI兼容格式 } } else if (toolCallDetection.confidence >= 0.3) { // 中等置信度,仅在原始reason明显错误时修正 if (originalReason === 'stop' || originalReason === 'end_turn') { if (context.provider === 'openai' || context.provider.includes('openai')) { return 'tool_calls'; } else if (context.provider === 'anthropic' || context.provider.includes('anthropic')) { return 'tool_use'; } else if (context.provider.includes('gemini')) { return 'tool_calls'; } } } // 低置信度或未知provider,保持原始reason return originalReason; } /** * 批量修正多个响应的finish_reason */ batchCorrectFinishReasons(responses) { return responses.map(({ reason, data, context }) => this.correctFinishReason(reason, data, context)); } /** * 验证finish_reason是否需要修正 */ needsCorrection(finishReason, responseData, context) { const detection = this.detectToolCallsInResponse(responseData, context); // 如果检测到工具调用但finish_reason不正确 if (detection.hasToolCalls && detection.confidence >= 0.7) { const expectedReason = context.provider === 'openai' ? 'tool_calls' : 'tool_use'; return finishReason !== expectedReason; } return false; } /** * 获取修正统计信息 */ getStats() { return { supportedProviders: ['openai', 'anthropic', 'gemini'], detectionMethods: [ 'openai_tool_calls_field', 'openai_delta_tool_calls', 'anthropic_tool_use_blocks', 'text_detection_*', 'context_inference' ], confidenceThresholds: { high: 0.7, medium: 0.3, low: 0.1 } }; } } exports.FinishReasonCorrector = FinishReasonCorrector; // 导出单例实例 exports.finishReasonCorrector = new FinishReasonCorrector(); // 便捷函数 function correctFinishReason(originalReason, responseData, context) { return exports.finishReasonCorrector.correctFinishReason(originalReason, responseData, context); } function needsFinishReasonCorrection(finishReason, responseData, context) { return exports.finishReasonCorrector.needsCorrection(finishReason, responseData, context); } //# sourceMappingURL=finish-reason-corrector.js.map