route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
361 lines • 14.5 kB
JavaScript
;
/**
* 统一响应转换器
* 项目所有者: Jason Zhang
*
* 遵循核心原则: 模型发送什么我们就返回什么
* - 不使用fallback机制
* - 不设置默认值
* - 保持响应的原始性
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertOpenAIMessageToAnthropicContent = convertOpenAIMessageToAnthropicContent;
exports.normalizeToAnthropicContent = normalizeToAnthropicContent;
exports.mapFinishReasonStrict = mapFinishReasonStrict;
exports.mapStopReasonStrict = mapStopReasonStrict;
exports.convertOpenAIResponseToAnthropic = convertOpenAIResponseToAnthropic;
exports.validateAndNormalizeAnthropicResponse = validateAndNormalizeAnthropicResponse;
exports.convertToAnthropicResponse = convertToAnthropicResponse;
exports.getSupportedFinishReasons = getSupportedFinishReasons;
exports.getSupportedStopReasons = getSupportedStopReasons;
const logger_1 = require("@/utils/logger");
/**
* 严格的finish reason映射表 - 只映射明确支持的类型
*/
const STRICT_FINISH_REASON_MAPPING = {
// OpenAI -> Anthropic 标准映射
'stop': 'end_turn',
'length': 'max_tokens',
'tool_calls': 'tool_use',
'function_call': 'tool_use',
'content_filter': 'stop_sequence',
// LMStudio/ModelScope异常值支持 - 有些服务可能返回已转换后的值
'end_turn': 'end_turn', // 直接透传已转换的值
'max_tokens': 'max_tokens',
'tool_use': 'tool_use',
'stop_sequence': 'stop_sequence',
};
/**
* 严格的stop reason映射表 - 只映射明确支持的类型
*/
const STRICT_STOP_REASON_MAPPING = {
// Anthropic -> OpenAI 标准映射
'end_turn': 'stop',
'max_tokens': 'length',
'tool_use': 'tool_calls',
'stop_sequence': 'stop',
};
/**
* OpenAI消息内容转换为Anthropic内容格式
*/
function convertOpenAIMessageToAnthropicContent(message) {
const content = [];
// 处理文本内容
if (typeof message.content === 'string' && message.content.trim()) {
content.push({ type: 'text', text: message.content });
}
else if (Array.isArray(message.content)) {
message.content.forEach((block) => {
if (block.type === 'text' && block.text) {
content.push({ type: 'text', text: block.text });
}
else {
content.push(block); // 保持其他类型不变
}
});
}
else if (message.content && typeof message.content !== 'string') {
content.push({ type: 'text', text: String(message.content) });
}
// 处理工具调用
if (message.tool_calls && Array.isArray(message.tool_calls)) {
logger_1.logger.debug('Converting OpenAI tool_calls to Anthropic tool_use format', {
toolCallsCount: message.tool_calls.length,
tools: message.tool_calls.map((tc) => ({ id: tc.id, name: tc.function?.name }))
});
message.tool_calls.forEach((toolCall, index) => {
if (!toolCall.function?.name) {
logger_1.logger.warn('Skipping tool call without function name', { toolCall, index });
return;
}
let parsedInput = {};
if (toolCall.function.arguments) {
try {
parsedInput = JSON.parse(toolCall.function.arguments);
}
catch (error) {
logger_1.logger.warn('Failed to parse tool call arguments', {
toolName: toolCall.function.name,
arguments: toolCall.function.arguments,
error: error instanceof Error ? error.message : String(error)
});
parsedInput = { arguments: toolCall.function.arguments };
}
}
// 标准化工具ID格式
let toolId = toolCall.id;
if (!toolId || !toolId.startsWith('toolu_')) {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 8);
toolId = `toolu_${timestamp}_${random}`;
logger_1.logger.debug('Standardized tool ID format', {
originalId: toolCall.id,
standardizedId: toolId,
toolName: toolCall.function.name
});
}
content.push({
type: 'tool_use',
id: toolId,
name: toolCall.function.name,
input: parsedInput
});
});
}
// 如果没有任何内容,返回空文本块
if (content.length === 0) {
return [{ type: 'text', text: '' }];
}
return content;
}
/**
* 标准化内容块为Anthropic格式
*/
function normalizeToAnthropicContent(content) {
if (!content)
return [];
if (typeof content === 'string') {
return [{ type: 'text', text: content }];
}
if (Array.isArray(content)) {
return content.map(block => normalizeContentBlock(block));
}
if (typeof content === 'object') {
return [normalizeContentBlock(content)];
}
return [{ type: 'text', text: String(content) }];
}
/**
* 标准化单个内容块
*/
function normalizeContentBlock(block) {
if (typeof block === 'string') {
return { type: 'text', text: block };
}
if (!block || typeof block !== 'object') {
return { type: 'text', text: String(block) };
}
// 已经是正确格式
if (block.type && (block.text || block.id || block.content)) {
return block;
}
// 转换常见格式
if (block.content && !block.type) {
return { type: 'text', text: block.content };
}
if (block.message && !block.type) {
return { type: 'text', text: block.message };
}
// 默认转换为文本块
return { type: 'text', text: JSON.stringify(block) };
}
/**
* 严格的finish reason映射 - 不使用fallback
* @param finishReason 原始finish reason
* @returns 映射后的stop reason,如果无法映射则抛出错误
*/
function mapFinishReasonStrict(finishReason) {
if (!finishReason) {
throw new Error('mapFinishReasonStrict: finishReason is required but was undefined/empty - this indicates a potential silent failure upstream');
}
if (finishReason === 'unknown') {
throw new Error(`mapFinishReasonStrict: received 'unknown' finish reason - this indicates a provider connection issue that should be handled upstream`);
}
const mappedReason = STRICT_FINISH_REASON_MAPPING[finishReason];
if (!mappedReason) {
throw new Error(`mapFinishReasonStrict: Unknown finish reason '${finishReason}' - no mapping available. Supported: ${Object.keys(STRICT_FINISH_REASON_MAPPING).join(', ')}`);
}
return mappedReason;
}
/**
* 严格的stop reason映射 - 不使用fallback
* @param stopReason 原始stop reason
* @returns 映射后的finish reason,如果无法映射则抛出错误
*/
function mapStopReasonStrict(stopReason) {
if (!stopReason) {
throw new Error('mapStopReasonStrict: stopReason is required but was undefined/empty - this indicates a potential silent failure upstream');
}
if (stopReason === 'unknown') {
throw new Error(`mapStopReasonStrict: received 'unknown' stop reason - this indicates a provider connection issue that should be handled upstream`);
}
const mappedReason = STRICT_STOP_REASON_MAPPING[stopReason];
if (!mappedReason) {
throw new Error(`mapStopReasonStrict: Unknown stop reason '${stopReason}' - no mapping available. Supported: ${Object.keys(STRICT_STOP_REASON_MAPPING).join(', ')}`);
}
return mappedReason;
}
/**
* 将OpenAI格式响应转换为Anthropic格式
* 严格遵循"模型发送什么就返回什么"原则
*/
function convertOpenAIResponseToAnthropic(response, originalRequest, requestId) {
const choice = response.choices?.[0];
if (!choice) {
throw new Error('No choices in OpenAI response');
}
const content = convertOpenAIMessageToAnthropicContent(choice.message);
let mappedStopReason;
// 尝试严格映射finish reason,如果失败则记录错误
if (choice.finish_reason) {
try {
mappedStopReason = mapFinishReasonStrict(choice.finish_reason);
}
catch (error) {
// 映射失败是严重错误,应该抛出而不是静默忽略
const errorMessage = `Failed to map OpenAI finish_reason '${choice.finish_reason}' to Anthropic stop_reason: ${error instanceof Error ? error.message : String(error)}`;
logger_1.logger.error('Critical: finish_reason mapping failed', {
originalFinishReason: choice.finish_reason,
error: errorMessage,
requestId,
model: originalRequest.model,
provider: originalRequest.metadata?.targetProvider
}, requestId, 'response-converter');
// 抛出错误以防止静默失败
throw new Error(`${errorMessage}. This indicates a system integration issue that must be fixed.`);
}
}
const anthropicResponse = {
content: content,
id: response.id || `msg_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 8)}`,
model: originalRequest.metadata?.originalModel || originalRequest.model,
role: 'assistant',
type: 'message',
stop_sequence: undefined,
usage: {
input_tokens: response.usage?.prompt_tokens || 0,
output_tokens: response.usage?.completion_tokens || 0
}
};
// 只有在成功映射时才设置stop_reason
if (mappedStopReason) {
anthropicResponse.stop_reason = mappedStopReason;
}
logger_1.logger.debug('Converted OpenAI response to Anthropic format', {
originalFinishReason: choice.finish_reason,
mappedStopReason: mappedStopReason || 'undefined',
contentBlocks: content.length,
requestId
}, requestId, 'response-converter');
return anthropicResponse;
}
/**
* 验证并标准化已有的Anthropic格式响应
*/
function validateAndNormalizeAnthropicResponse(response, originalRequest, requestId) {
const content = normalizeToAnthropicContent(response.content);
const normalized = {
content: content,
id: response.id || `msg_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 8)}`,
model: originalRequest.metadata?.originalModel || originalRequest.model,
role: 'assistant',
type: 'message',
stop_sequence: response.stop_sequence || undefined,
usage: {
input_tokens: response.usage?.input_tokens || 0,
output_tokens: response.usage?.output_tokens || 0
}
};
// 保持原有的stop_reason,除非它是'unknown'
if (response.stop_reason && response.stop_reason !== 'unknown') {
normalized.stop_reason = response.stop_reason;
}
logger_1.logger.debug('Validated and normalized Anthropic response', {
id: normalized.id,
contentBlocks: normalized.content.length,
originalStopReason: response.stop_reason,
finalStopReason: normalized.stop_reason || 'undefined',
requestId
}, requestId, 'response-converter');
return normalized;
}
/**
* 从各种格式转换为Anthropic响应
*/
function convertToAnthropicResponse(response, originalRequest, requestId) {
// OpenAI格式
if (response.choices && Array.isArray(response.choices)) {
return convertOpenAIResponseToAnthropic(response, originalRequest, requestId);
}
// 已经是Anthropic格式
if (response && response.role === 'assistant' && Array.isArray(response.content)) {
return validateAndNormalizeAnthropicResponse(response, originalRequest, requestId);
}
// 内容数组格式
if (Array.isArray(response)) {
const content = normalizeToAnthropicContent(response);
return {
id: `msg_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 8)}`,
type: 'message',
role: 'assistant',
model: originalRequest.metadata?.originalModel || originalRequest.model,
content: content,
stop_sequence: undefined,
usage: {
input_tokens: 0,
output_tokens: Math.ceil(JSON.stringify(content).length / 4)
}
};
}
// 简单文本格式
if (typeof response === 'string') {
const content = [{ type: 'text', text: response }];
return {
id: `msg_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 8)}`,
type: 'message',
role: 'assistant',
model: originalRequest.metadata?.originalModel || originalRequest.model,
content: content,
stop_sequence: undefined,
usage: {
input_tokens: 0,
output_tokens: Math.ceil(response.length / 4)
}
};
}
// 结构化响应格式
if (response && typeof response === 'object' && response.content) {
const content = normalizeToAnthropicContent(response.content);
const result = {
id: response.id || `msg_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 8)}`,
type: 'message',
role: 'assistant',
model: originalRequest.metadata?.originalModel || originalRequest.model,
content: content,
stop_sequence: response.stop_sequence || undefined,
usage: {
input_tokens: response.usage?.input_tokens || 0,
output_tokens: response.usage?.output_tokens || Math.ceil(JSON.stringify(content).length / 4)
}
};
// 只有在有效的stop_reason时才设置
if (response.stop_reason && response.stop_reason !== 'unknown') {
result.stop_reason = response.stop_reason;
}
return result;
}
throw new Error(`Unsupported response format for conversion: ${typeof response}`);
}
/**
* 获取支持的finish reasons
*/
function getSupportedFinishReasons() {
return Object.keys(STRICT_FINISH_REASON_MAPPING);
}
/**
* 获取支持的stop reasons
*/
function getSupportedStopReasons() {
return Object.values(STRICT_FINISH_REASON_MAPPING);
}
//# sourceMappingURL=response-converter.js.map