route-claudecode
Version:
Advanced routing and transformation system for Claude Code outputs to multiple AI providers
286 lines • 12.1 kB
JavaScript
"use strict";
/**
* 安全的Max Tokens处理器 - 避免无限重试风险
* Project Owner: Jason Zhang
*
* 改进方案:
* 1. 预处理检查:发送前估算token,超限时主动截断
* 2. 渐进式截断:多级截断策略,确保成功
* 3. 用户控制:返回选项让用户决定是否重试
* 4. 透明化:清晰告知用户发生了什么
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SafeMaxTokensHandler = void 0;
const logger_1 = require("./logger");
const max_tokens_error_handling_module_1 = require("./max-tokens-error-handling-module");
class SafeMaxTokensHandler {
handlingModule;
options;
retryCounters = new Map();
constructor(options = {}) {
this.options = {
enablePreemptiveCheck: true,
maxRetryAttempts: 1, // 只允许1次重试,避免无限循环
tokenSafetyMargin: 500,
userChoiceTimeout: 30000, // 30秒超时
enableProgressiveReduction: true,
...options
};
this.handlingModule = new max_tokens_error_handling_module_1.MaxTokensErrorHandlingModule();
logger_1.logger.info('Safe Max Tokens Handler initialized', {
maxRetryAttempts: this.options.maxRetryAttempts,
tokenSafetyMargin: this.options.tokenSafetyMargin,
preemptiveCheck: this.options.enablePreemptiveCheck
});
}
/**
* 预发送检查 - 在发送请求前检查token使用
*/
async preflightCheck(request, maxTokens, requestId) {
if (!this.options.enablePreemptiveCheck) {
return { action: 'proceed' };
}
const estimatedTokens = this.estimateTokens(request);
const effectiveLimit = maxTokens - this.options.tokenSafetyMargin;
logger_1.logger.debug('🔍 [PREFLIGHT] Token estimation', {
estimatedTokens,
maxTokens,
effectiveLimit,
safetyMargin: this.options.tokenSafetyMargin,
requestId
});
// 安全范围内,直接发送
if (estimatedTokens <= effectiveLimit) {
return { action: 'proceed' };
}
// 接近限制,给出警告但允许发送
if (estimatedTokens <= maxTokens) {
return {
action: 'proceed',
warning: {
type: 'token_limit_approaching',
message: `Request is approaching token limit (${estimatedTokens}/${maxTokens}). Consider simplifying if response is truncated.`,
estimatedTokens,
maxTokens
}
};
}
// 明确超限,需要处理
logger_1.logger.warn('🚨 [PREFLIGHT] Token limit exceeded', {
estimatedTokens,
maxTokens,
overage: estimatedTokens - maxTokens,
requestId
});
// 尝试渐进式截断
const truncationResult = await this.performProgressiveTruncation(request, maxTokens, requestId);
if (truncationResult.success) {
return {
action: 'truncate',
truncatedRequest: truncationResult.truncatedRequest,
warning: {
type: 'truncation_required',
message: `Request exceeded token limit and was automatically truncated (${truncationResult.originalTokens} → ${truncationResult.reducedTokens} tokens).`,
estimatedTokens: truncationResult.reducedTokens,
maxTokens,
truncationDetails: truncationResult
}
};
}
// 无法自动处理,需要用户选择
return {
action: 'user_choice',
warning: {
type: 'context_loss_risk',
message: `Request significantly exceeds token limit (${estimatedTokens}/${maxTokens}). Automatic truncation may affect response quality.`,
estimatedTokens,
maxTokens
},
userChoices: {
autoTruncate: true,
manualEdit: true,
useSimplified: true
}
};
}
/**
* 响应时错误处理 - 收到max_tokens错误后的处理
*/
async handleMaxTokensError(originalRequest, errorResponse, requestId) {
// 检查重试次数
const currentRetries = this.retryCounters.get(requestId) || 0;
if (currentRetries >= this.options.maxRetryAttempts) {
logger_1.logger.warn('🚨 [MAX-TOKENS] Maximum retry attempts reached', {
currentRetries,
maxRetries: this.options.maxRetryAttempts,
requestId
});
return {
action: 'abort',
warning: {
type: 'context_loss_risk',
message: `Maximum retry attempts (${this.options.maxRetryAttempts}) reached. Please manually reduce request size.`,
estimatedTokens: this.estimateTokens(originalRequest),
maxTokens: 0 // Unknown from error response
}
};
}
// 增加重试计数
this.retryCounters.set(requestId, currentRetries + 1);
logger_1.logger.info('🔧 [MAX-TOKENS] Attempting intelligent recovery', {
attempt: currentRetries + 1,
maxAttempts: this.options.maxRetryAttempts,
requestId
});
// 尝试渐进式截断
try {
const truncationResult = await this.performProgressiveTruncation(originalRequest, this.estimateMaxTokensFromError(errorResponse), requestId);
if (truncationResult.success) {
// 确保截断后的请求足够小
const truncatedEstimate = this.estimateTokens(truncationResult.truncatedRequest);
const maxTokens = this.estimateMaxTokensFromError(errorResponse);
if (truncatedEstimate <= maxTokens * 0.8) { // 80% 安全边界
return {
action: 'truncate',
truncatedRequest: truncationResult.truncatedRequest,
warning: {
type: 'truncation_required',
message: `Request was automatically truncated due to token limits (${truncationResult.originalTokens} → ${truncationResult.reducedTokens} tokens).`,
estimatedTokens: truncationResult.reducedTokens,
maxTokens,
truncationDetails: truncationResult
}
};
}
}
}
catch (error) {
logger_1.logger.error('🚨 [MAX-TOKENS] Truncation failed', error, requestId);
}
// 无法安全处理,终止重试
return {
action: 'abort',
warning: {
type: 'context_loss_risk',
message: 'Unable to safely truncate request. Please manually reduce the input size.',
estimatedTokens: this.estimateTokens(originalRequest),
maxTokens: this.estimateMaxTokensFromError(errorResponse)
}
};
}
/**
* 渐进式截断 - 多级截断策略
*/
async performProgressiveTruncation(request, maxTokens, requestId) {
const reductionStrategies = [
{ historyRetention: 80, useSimplified: true }, // 轻度截断
{ historyRetention: 60, useSimplified: true }, // 中度截断
{ historyRetention: 40, useSimplified: true }, // 重度截断
{ historyRetention: 20, useSimplified: true } // 极度截断
];
logger_1.logger.debug('🔧 [PROGRESSIVE] Starting progressive truncation', {
strategies: reductionStrategies.length,
targetTokens: maxTokens * 0.8, // 目标80%使用率
requestId
});
for (let i = 0; i < reductionStrategies.length; i++) {
const strategy = reductionStrategies[i];
// 临时更新配置
this.handlingModule.updateConfig({
strategy: 'rolling_truncation',
rollingTruncation: {
historyRetentionPercent: strategy.historyRetention,
useSimplifiedPrompt: strategy.useSimplified,
simplifiedPromptPath: 'config/simplified-system-prompt.json'
}
});
try {
const result = await this.handlingModule.handleMaxTokensError(request, { maxTokens, provider: 'progressive-truncation' }, requestId);
logger_1.logger.debug(`🔧 [PROGRESSIVE] Strategy ${i + 1} result`, {
historyRetention: strategy.historyRetention,
originalTokens: result.originalTokens,
reducedTokens: result.reducedTokens,
targetTokens: maxTokens * 0.8,
success: result.reducedTokens <= maxTokens * 0.8,
requestId
});
// 检查是否达到目标(80%安全边界)
if (result.success && result.reducedTokens <= maxTokens * 0.8) {
logger_1.logger.info('🎯 [PROGRESSIVE] Truncation successful', {
strategy: i + 1,
reduction: Math.round((1 - result.reducedTokens / result.originalTokens) * 100),
finalTokens: result.reducedTokens,
requestId
});
return result;
}
}
catch (error) {
logger_1.logger.debug(`🔧 [PROGRESSIVE] Strategy ${i + 1} failed`, error, requestId);
}
}
throw new Error('Progressive truncation failed to achieve safe token count');
}
/**
* 估算token数量
*/
estimateTokens(request) {
let totalChars = 0;
// 消息内容
if (request.messages) {
request.messages.forEach(msg => {
if (typeof msg.content === 'string') {
totalChars += msg.content.length;
}
else if (Array.isArray(msg.content)) {
msg.content.forEach((block) => {
if (block.text)
totalChars += block.text.length;
if (block.content)
totalChars += JSON.stringify(block.content).length;
});
}
});
}
// 系统提示词
if (request.metadata?.system) {
totalChars += JSON.stringify(request.metadata.system).length;
}
// 工具定义
if (request.metadata?.tools) {
totalChars += JSON.stringify(request.metadata.tools).length;
}
return Math.ceil(totalChars / 4);
}
/**
* 从错误响应估算最大token数
*/
estimateMaxTokensFromError(errorResponse) {
// 尝试从usage信息推断
if (errorResponse.usage?.total_tokens) {
return errorResponse.usage.total_tokens + 1000; // 增加缓冲
}
// 默认估算
return 4096;
}
/**
* 清理请求的重试计数器
*/
clearRetryCounter(requestId) {
this.retryCounters.delete(requestId);
}
/**
* 获取统计信息
*/
getStats() {
return {
activeRetries: this.retryCounters.size,
totalRequestsHandled: this.retryCounters.size, // 简化统计
averageRetryRate: this.retryCounters.size > 0 ?
Array.from(this.retryCounters.values()).reduce((sum, count) => sum + count, 0) / this.retryCounters.size : 0
};
}
}
exports.SafeMaxTokensHandler = SafeMaxTokensHandler;
exports.default = SafeMaxTokensHandler;
//# sourceMappingURL=safe-max-tokens-handler.js.map