feat(token): 增强token加载和保存的日志记录与验证
refactor(claude): 简化token计算逻辑并改进上下文使用率处理
This commit is contained in:
parent
d1516abc4e
commit
02712afc30
2 changed files with 73 additions and 125 deletions
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue