feat(token): 增强token加载和保存的日志记录与验证

refactor(claude): 简化token计算逻辑并改进上下文使用率处理
This commit is contained in:
hex2077 2026-01-12 15:49:19 +08:00
parent d1516abc4e
commit 02712afc30
2 changed files with 73 additions and 125 deletions

View file

@ -576,7 +576,7 @@ async initializeAuth(forceRefresh = false) {
if (!this.accessToken) { if (!this.accessToken) {
throw new Error('No access token available after initialization and refresh attempts.'); throw new Error('No access token available after initialization and refresh attempts.');
} }
} }
/** /**
* Extract text content from OpenAI message format * Extract text content from OpenAI message format
@ -1231,7 +1231,7 @@ async initializeAuth(forceRefresh = false) {
async callApi(method, model, body, isRetry = false, retryCount = 0) { async callApi(method, model, body, isRetry = false, retryCount = 0) {
if (!this.isInitialized) await this.initialize(); if (!this.isInitialized) await this.initialize();
const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; const maxRetries = this.config.REQUEST_MAX_RETRIES || 3;
const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; // 1 second base delay
const requestData = this.buildCodewhispererRequest(body.messages, model, body.tools, body.system, body.thinking); const requestData = this.buildCodewhispererRequest(body.messages, model, body.tools, body.system, body.thinking);
@ -1730,7 +1730,6 @@ async initializeAuth(forceRefresh = false) {
let currentToolCall = null; // 用于累积结构化工具调用 let currentToolCall = null; // 用于累积结构化工具调用
const estimatedInputTokens = this.estimateInputTokens(requestBody); const estimatedInputTokens = this.estimateInputTokens(requestBody);
const tokenBreakdown = this._lastTokenBreakdown || {};
// 1. 先发送 message_start 事件 // 1. 先发送 message_start 事件
yield { yield {
@ -1752,23 +1751,9 @@ async initializeAuth(forceRefresh = false) {
// 2. 流式接收并发送每个 content_block_delta // 2. 流式接收并发送每个 content_block_delta
for await (const event of this.streamApiReal('', finalModel, requestBody)) { for await (const event of this.streamApiReal('', finalModel, requestBody)) {
if (event.type === 'contextUsage' && event.percentage) { if (event.type === 'contextUsage' && event.contextUsagePercentage) {
// 捕获上下文使用百分比 // 捕获上下文使用百分比(包含输入和输出的总使用量)
contextUsagePercentage = event.percentage; contextUsagePercentage = event.contextUsagePercentage;
inputTokens = this.calculateInputTokensFromPercentage(contextUsagePercentage);
if (Math.abs(inputTokens - estimatedInputTokens) > estimatedInputTokens * 0.1) {
yield {
type: "message_delta",
delta: {},
usage: {
input_tokens: inputTokens,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
}
};
}
} else if (event.type === 'content' && event.content) { } else if (event.type === 'content' && event.content) {
totalContent += event.content; totalContent += event.content;
@ -1930,11 +1915,6 @@ async initializeAuth(forceRefresh = false) {
yield* pushEvents(stopBlock(streamState.textBlockIndex)); yield* pushEvents(stopBlock(streamState.textBlockIndex));
if (contextUsagePercentage === null) {
console.warn('[Kiro Stream] contextUsagePercentage not received, using estimation');
inputTokens = estimatedInputTokens;
}
// 检查文本内容中的 bracket 格式工具调用 // 检查文本内容中的 bracket 格式工具调用
const bracketToolCalls = parseBracketToolCalls(totalContent); const bracketToolCalls = parseBracketToolCalls(totalContent);
if (bracketToolCalls && bracketToolCalls.length > 0) { if (bracketToolCalls && bracketToolCalls.length > 0) {
@ -1978,6 +1958,7 @@ async initializeAuth(forceRefresh = false) {
} }
} }
// 计算 output tokens
const contentBlocksForCount = thinkingRequested const contentBlocksForCount = thinkingRequested
? this._toClaudeContentBlocksFromKiroText(totalContent) ? this._toClaudeContentBlocksFromKiroText(totalContent)
: [{ type: "text", text: totalContent }]; : [{ type: "text", text: totalContent }];
@ -1990,6 +1971,19 @@ async initializeAuth(forceRefresh = false) {
outputTokens += this.countTextTokens(JSON.stringify(tc.input || {})); outputTokens += this.countTextTokens(JSON.stringify(tc.input || {}));
} }
// 计算 input tokens
// contextUsagePercentage 是包含输入和输出的总使用量百分比
// 总 token = TOTAL_CONTEXT_TOKENS * contextUsagePercentage / 100
// input token = 总 token - output token
if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
const totalTokens = Math.round(KIRO_CONSTANTS.TOTAL_CONTEXT_TOKENS * contextUsagePercentage / 100);
inputTokens = Math.max(0, totalTokens - outputTokens);
console.log(`[Kiro] Token calculation from contextUsagePercentage: total=${totalTokens}, output=${outputTokens}, input=${inputTokens}`);
} else {
console.warn('[Kiro Stream] contextUsagePercentage not received, using estimation');
inputTokens = estimatedInputTokens;
}
// 4. 发送 message_delta 事件 // 4. 发送 message_delta 事件
yield { yield {
type: "message_delta", type: "message_delta",
@ -2011,6 +2005,9 @@ async initializeAuth(forceRefresh = false) {
} }
} }
/**
* Count tokens for a given text using Claude's official tokenizer
*/
countTextTokens(text) { countTextTokens(text) {
if (!text) return 0; if (!text) return 0;
try { try {
@ -2028,117 +2025,50 @@ async initializeAuth(forceRefresh = false) {
estimateInputTokens(requestBody) { estimateInputTokens(requestBody) {
let totalTokens = 0; let totalTokens = 0;
// 定义各类内容的开销乘数
const OVERHEAD_MULTIPLIERS = {
system: 1.0,
message: 1.0,
tools: 1.0,
thinking: 1.0,
tool_result: 1.0,
tool_use_input: 1.0,
image: 1500
};
const breakdown = {
system: 0,
thinking: 0,
text: 0,
tool_result: 0,
tool_use_input: 0,
image: 0,
thinking_content: 0,
tools_def: 0
};
// Count system prompt tokens // Count system prompt tokens
if (requestBody.system) { if (requestBody.system) {
const systemText = this.getContentText(requestBody.system); const systemText = this.getContentText(requestBody.system);
const systemTokens = this.countTextTokens(systemText); totalTokens += this.countTextTokens(systemText);
const counted = Math.ceil(systemTokens * OVERHEAD_MULTIPLIERS.system);
breakdown.system = counted;
totalTokens += counted;
} }
// Count thinking prefix tokens if thinking is enabled
if (requestBody.thinking?.type === 'enabled') { if (requestBody.thinking?.type === 'enabled') {
const budget = this._normalizeThinkingBudgetTokens(requestBody.thinking.budget_tokens); const budget = this._normalizeThinkingBudgetTokens(requestBody.thinking.budget_tokens);
const prefixText = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`; const prefixText = `<thinking_mode>enabled</thinking_mode><max_thinking_length>${budget}</max_thinking_length>`;
const prefixTokens = this.countTextTokens(prefixText); totalTokens += this.countTextTokens(prefixText);
const counted = Math.ceil(prefixTokens * OVERHEAD_MULTIPLIERS.thinking);
breakdown.thinking = counted;
totalTokens += counted;
} }
// Count all messages tokens // Count all messages tokens
if (requestBody.messages && Array.isArray(requestBody.messages)) { if (requestBody.messages && Array.isArray(requestBody.messages)) {
for (const message of requestBody.messages) { for (const message of requestBody.messages) {
if (!message.content) { if (message.content) {
continue; if (Array.isArray(message.content)) {
} for (const part of message.content) {
if (part.type === 'text' && part.text) {
if (Array.isArray(message.content)) { totalTokens += this.countTextTokens(part.text);
for (const part of message.content) { } else if (part.type === 'thinking' && part.thinking) {
if (part.type === 'text' && part.text) { totalTokens += this.countTextTokens(part.thinking);
const counted = Math.ceil(this.countTextTokens(part.text) * OVERHEAD_MULTIPLIERS.message); } else if (part.type === 'tool_result') {
breakdown.text += counted; const resultContent = this.getContentText(part.content);
totalTokens += counted; totalTokens += this.countTextTokens(resultContent);
} } else if (part.type === 'tool_use' && part.input) {
else if (part.type === 'tool_result') { totalTokens += this.countTextTokens(JSON.stringify(part.input));
const toolResultText = this.getContentText(part.content); }
const counted = Math.ceil(this.countTextTokens(toolResultText) * OVERHEAD_MULTIPLIERS.tool_result);
breakdown.tool_result += counted;
totalTokens += counted;
}
else if (part.type === 'tool_use' && part.input) {
const inputJson = JSON.stringify(part.input);
const counted = Math.ceil(this.countTextTokens(inputJson) * OVERHEAD_MULTIPLIERS.tool_use_input);
breakdown.tool_use_input += counted;
totalTokens += counted;
}
else if (part.type === 'image') {
breakdown.image += OVERHEAD_MULTIPLIERS.image;
totalTokens += OVERHEAD_MULTIPLIERS.image;
}
else if (part.type === 'thinking' && part.thinking) {
const counted = Math.ceil(this.countTextTokens(part.thinking) * OVERHEAD_MULTIPLIERS.message);
breakdown.thinking_content += counted;
totalTokens += counted;
} }
} else {
const contentText = this.getContentText(message);
totalTokens += this.countTextTokens(contentText);
} }
} }
else if (typeof message.content === 'string') {
const counted = Math.ceil(this.countTextTokens(message.content) * OVERHEAD_MULTIPLIERS.message);
breakdown.text += counted;
totalTokens += counted;
}
} }
} }
// Count tools definitions tokens if present // Count tools definitions tokens if present
if (requestBody.tools && Array.isArray(requestBody.tools)) { if (requestBody.tools && Array.isArray(requestBody.tools)) {
for (const tool of requestBody.tools) { totalTokens += this.countTextTokens(JSON.stringify(requestBody.tools));
const toolJson = JSON.stringify(tool);
const toolTokens = this.countTextTokens(toolJson);
const counted = Math.ceil(toolTokens * OVERHEAD_MULTIPLIERS.tools);
breakdown.tools_def += counted;
totalTokens += counted;
}
} }
const hasTools = requestBody.tools && requestBody.tools.length > 0;
const toolsDefTokens = breakdown.tools_def || 0;
const isSmallToolsDef = toolsDefTokens > 0 && toolsDefTokens < 21000;
const KIRO_BASE_OVERHEAD = 400;
const KIRO_PERCENTAGE_OVERHEAD = hasTools
? (isSmallToolsDef ? 0.18 : 0.08)
: 0.25;
const baseOverhead = KIRO_BASE_OVERHEAD;
const percentageOverhead = Math.ceil(totalTokens * KIRO_PERCENTAGE_OVERHEAD);
totalTokens += baseOverhead + percentageOverhead;
this._lastTokenBreakdown = breakdown; return totalTokens;
return Math.ceil(totalTokens);
} }
/** /**

View file

@ -105,13 +105,17 @@ class IFlowTokenStorage {
*/ */
async function loadTokenFromFile(filePath) { async function loadTokenFromFile(filePath) {
try { try {
const absolutePath = path.isAbsolute(filePath) const absolutePath = path.isAbsolute(filePath)
? filePath ? filePath
: path.join(process.cwd(), filePath); : path.join(process.cwd(), filePath);
const data = await fs.readFile(absolutePath, 'utf-8'); const data = await fs.readFile(absolutePath, 'utf-8');
const json = JSON.parse(data); const json = JSON.parse(data);
// 记录加载的 token 信息
const refreshToken = json.refreshToken || json.refresh_token || '';
console.log(`[iFlow] Token loaded from: ${filePath} (refresh_token: ${refreshToken ? refreshToken.substring(0, 8) + '...' : 'EMPTY'})`);
return IFlowTokenStorage.fromJSON(json); return IFlowTokenStorage.fromJSON(json);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
@ -129,8 +133,8 @@ async function loadTokenFromFile(filePath) {
*/ */
async function saveTokenToFile(filePath, tokenStorage) { async function saveTokenToFile(filePath, tokenStorage) {
try { try {
const absolutePath = path.isAbsolute(filePath) const absolutePath = path.isAbsolute(filePath)
? filePath ? filePath
: path.join(process.cwd(), filePath); : path.join(process.cwd(), filePath);
// 确保目录存在 // 确保目录存在
@ -139,9 +143,18 @@ async function saveTokenToFile(filePath, tokenStorage) {
// 写入文件 // 写入文件
const json = tokenStorage.toJSON(); const json = tokenStorage.toJSON();
// 验证关键字段是否存在
if (!json.refresh_token || json.refresh_token.trim() === '') {
console.error('[iFlow] WARNING: Attempting to save token file with empty refresh_token!');
}
if (!json.apiKey || json.apiKey.trim() === '') {
console.error('[iFlow] WARNING: Attempting to save token file with empty apiKey!');
}
await fs.writeFile(absolutePath, JSON.stringify(json, null, 2), 'utf-8'); await fs.writeFile(absolutePath, JSON.stringify(json, null, 2), 'utf-8');
console.log(`[iFlow] Token saved to: ${filePath}`); console.log(`[iFlow] Token saved to: ${filePath} (refresh_token: ${json.refresh_token ? json.refresh_token.substring(0, 8) + '...' : 'EMPTY'})`);
} catch (error) { } catch (error) {
throw new Error(`[iFlow] Failed to save token to file: ${error.message}`); throw new Error(`[iFlow] Failed to save token to file: ${error.message}`);
} }
@ -575,12 +588,17 @@ export class IFlowApiService {
} }
// 调用刷新函数 // 调用刷新函数
const tokenData = await refreshOAuthTokens(this.tokenStorage.refreshToken, this.axiosInstance); const oldRefreshToken = this.tokenStorage.refreshToken;
const tokenData = await refreshOAuthTokens(oldRefreshToken, this.axiosInstance);
// 更新 tokenStorage // 更新 tokenStorage - 必须更新 refreshToken因为 OAuth 服务器可能返回新的 refresh_token
this.tokenStorage.accessToken = tokenData.accessToken; this.tokenStorage.accessToken = tokenData.accessToken;
if (tokenData.refreshToken) { // 始终更新 refreshToken即使服务器没有返回新的tokenData.refreshToken 会回退到旧值)
this.tokenStorage.refreshToken = tokenData.refreshToken; this.tokenStorage.refreshToken = tokenData.refreshToken;
// 记录 refresh_token 是否发生变化
if (tokenData.refreshToken !== oldRefreshToken) {
console.log(`[iFlow] refresh_token has been rotated (old: ${this._maskToken(oldRefreshToken)}, new: ${this._maskToken(tokenData.refreshToken)})`);
} }
if (tokenData.apiKey) { if (tokenData.apiKey) {
this.tokenStorage.apiKey = tokenData.apiKey; this.tokenStorage.apiKey = tokenData.apiKey;