Merge pull request #180 from tickernelz/fix-kiro-token-count

refactor(claude-kiro): improve token counting accuracy using API contextUsagePercentage
This commit is contained in:
何夕2077 2026-01-08 12:54:59 +08:00 committed by GitHub
commit d4e5a25c9d

View file

@ -10,6 +10,7 @@ import { getProviderModels } from '../provider-models.js';
import { countTokens } from '@anthropic-ai/tokenizer'; import { countTokens } from '@anthropic-ai/tokenizer';
import { configureAxiosProxy } from '../proxy-utils.js'; import { configureAxiosProxy } from '../proxy-utils.js';
import { isRetryableNetworkError } from '../common.js'; import { isRetryableNetworkError } from '../common.js';
import { CLAUDE_DEFAULT_MAX_TOKENS } from '../converters/utils.js';
const KIRO_CONSTANTS = { const KIRO_CONSTANTS = {
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken', REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
@ -33,9 +34,9 @@ const KIRO_MODELS = getProviderModels('claude-kiro-oauth');
// 完整的模型映射表 // 完整的模型映射表
const FULL_MODEL_MAPPING = { const FULL_MODEL_MAPPING = {
"claude-opus-4-5":"claude-opus-4.5", "claude-opus-4-5": "claude-opus-4.5",
"claude-opus-4-5-20251101":"claude-opus-4.5", "claude-opus-4-5-20251101": "claude-opus-4.5",
"claude-haiku-4-5":"claude-haiku-4.5", "claude-haiku-4-5": "claude-haiku-4.5",
"claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0",
"claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0", "claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0",
"claude-sonnet-4-20250514": "CLAUDE_SONNET_4_20250514_V1_0", "claude-sonnet-4-20250514": "CLAUDE_SONNET_4_20250514_V1_0",
@ -370,197 +371,197 @@ export class KiroApiService {
this.isInitialized = true; this.isInitialized = true;
} }
async initializeAuth(forceRefresh = false) { async initializeAuth(forceRefresh = false) {
if (this.accessToken && !forceRefresh) { if (this.accessToken && !forceRefresh) {
console.debug('[Kiro Auth] Access token already available and not forced refresh.'); console.debug('[Kiro Auth] Access token already available and not forced refresh.');
return; return;
}
// Helper to load credentials from a file
const loadCredentialsFromFile = async (filePath) => {
try {
const fileContent = await fs.readFile(filePath, 'utf8');
return JSON.parse(fileContent);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
} else if (error instanceof SyntaxError) {
console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`);
} else {
console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
}
return null;
} }
};
// Helper to save credentials to a file // Helper to load credentials from a file
const saveCredentialsToFile = async (filePath, newData) => { const loadCredentialsFromFile = async (filePath) => {
try {
let existingData = {};
try { try {
const fileContent = await fs.readFile(filePath, 'utf8'); const fileContent = await fs.readFile(filePath, 'utf8');
existingData = JSON.parse(fileContent); return JSON.parse(fileContent);
} catch (readError) { } catch (error) {
if (readError.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`); console.debug(`[Kiro Auth] Credential file not found: ${filePath}`);
} else if (error instanceof SyntaxError) {
console.warn(`[Kiro Auth] Failed to parse JSON from ${filePath}: ${error.message}`);
} else { } else {
console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`); console.warn(`[Kiro Auth] Failed to read credential file ${filePath}: ${error.message}`);
} }
return null;
} }
const mergedData = { ...existingData, ...newData }; };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8');
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
} catch (error) {
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
}
};
try { // Helper to save credentials to a file
let mergedCredentials = {}; const saveCredentialsToFile = async (filePath, newData) => {
try {
// Priority 1: Load from Base64 credentials if available let existingData = {};
if (this.base64Creds) { try {
Object.assign(mergedCredentials, this.base64Creds); const fileContent = await fs.readFile(filePath, 'utf8');
console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).'); existingData = JSON.parse(fileContent);
// Clear base64Creds after use to prevent re-processing } catch (readError) {
this.base64Creds = null; if (readError.code === 'ENOENT') {
} console.debug(`[Kiro Auth] Token file not found, creating new one: ${filePath}`);
} else {
// Priority 2 & 3 合并: 从指定文件路径或目录加载凭证 console.warn(`[Kiro Auth] Could not read existing token file ${filePath}: ${readError.message}`);
// 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件)
const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
const dirPath = path.dirname(targetFilePath);
const targetFileName = path.basename(targetFilePath);
console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`);
try {
// 首先尝试读取目标文件
const targetCredentials = await loadCredentialsFromFile(targetFilePath);
if (targetCredentials) {
Object.assign(mergedCredentials, targetCredentials);
console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`);
}
// 然后读取目录下的其他 JSON 文件(排除目标文件本身)
const files = await fs.readdir(dirPath);
for (const file of files) {
if (file.endsWith('.json') && file !== targetFileName) {
const filePath = path.join(dirPath, file);
const credentials = await loadCredentialsFromFile(filePath);
if (credentials) {
// 保留已有的 expiresAt,避免被覆盖
credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`);
} }
} }
const mergedData = { ...existingData, ...newData };
await fs.writeFile(filePath, JSON.stringify(mergedData, null, 2), 'utf8');
console.info(`[Kiro Auth] Updated token file: ${filePath}`);
} catch (error) {
console.error(`[Kiro Auth] Failed to write token to file ${filePath}: ${error.message}`);
} }
} catch (error) { };
console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`);
}
// console.log('[Kiro Auth] Merged credentials:', mergedCredentials);
// Apply loaded credentials, prioritizing existing values if they are not null/undefined
this.accessToken = this.accessToken || mergedCredentials.accessToken;
this.refreshToken = this.refreshToken || mergedCredentials.refreshToken;
this.clientId = this.clientId || mergedCredentials.clientId;
this.clientSecret = this.clientSecret || mergedCredentials.clientSecret;
this.authMethod = this.authMethod || mergedCredentials.authMethod;
this.expiresAt = this.expiresAt || mergedCredentials.expiresAt;
this.profileArn = this.profileArn || mergedCredentials.profileArn;
this.region = this.region || mergedCredentials.region;
// Ensure region is set before using it in URLs
if (!this.region) {
console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.');
this.region = 'us-east-1'; // Set default region
}
this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region);
this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region);
this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region);
this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region);
} catch (error) {
console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`);
}
// Refresh token if forced or if access token is missing but refresh token is available
if (forceRefresh || (!this.accessToken && this.refreshToken)) {
if (!this.refreshToken) {
throw new Error('No refresh token available to refresh access token.');
}
try { try {
const requestBody = { let mergedCredentials = {};
refreshToken: this.refreshToken,
};
let refreshUrl = this.refreshUrl; // Priority 1: Load from Base64 credentials if available
if (this.authMethod !== KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { if (this.base64Creds) {
refreshUrl = this.refreshIDCUrl; Object.assign(mergedCredentials, this.base64Creds);
requestBody.clientId = this.clientId; console.info('[Kiro Auth] Successfully loaded credentials from Base64 (constructor).');
requestBody.clientSecret = this.clientSecret; // Clear base64Creds after use to prevent re-processing
requestBody.grantType = 'refresh_token'; this.base64Creds = null;
} }
let response = null; // Priority 2 & 3 合并: 从指定文件路径或目录加载凭证
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) { // 读取指定的 credPath 文件以及目录下的其他 JSON 文件(排除当前文件)
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody); const targetFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
console.log('[Kiro Auth] Token refresh social response: ok'); const dirPath = path.dirname(targetFilePath);
}else{ const targetFileName = path.basename(targetFilePath);
response = await this.axiosInstance.post(refreshUrl, requestBody);
console.log('[Kiro Auth] Token refresh idc response: ok');
}
if (response.data && response.data.accessToken) { console.debug(`[Kiro Auth] Attempting to load credentials from directory: ${dirPath}`);
this.accessToken = response.data.accessToken;
this.refreshToken = response.data.refreshToken;
this.profileArn = response.data.profileArn;
const expiresIn = response.data.expiresIn;
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
this.expiresAt = expiresAt;
console.info('[Kiro Auth] Access token refreshed successfully');
// Update the token file - use specified path if configured, otherwise use default try {
const tokenFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE); // 首先尝试读取目标文件
const updatedTokenData = { const targetCredentials = await loadCredentialsFromFile(targetFilePath);
accessToken: this.accessToken, if (targetCredentials) {
refreshToken: this.refreshToken, Object.assign(mergedCredentials, targetCredentials);
expiresAt: expiresAt, console.info(`[Kiro Auth] Successfully loaded OAuth credentials from ${targetFilePath}`);
};
if(this.profileArn){
updatedTokenData.profileArn = this.profileArn;
} }
await saveCredentialsToFile(tokenFilePath, updatedTokenData);
} else {
throw new Error('Invalid refresh response: Missing accessToken');
}
} catch (error) {
console.error('[Kiro Auth] Token refresh failed:', error.message);
throw new Error(`Token refresh failed: ${error.message}`);
}
}
if (!this.accessToken) { // 然后读取目录下的其他 JSON 文件(排除目标文件本身)
throw new Error('No access token available after initialization and refresh attempts.'); const files = await fs.readdir(dirPath);
} for (const file of files) {
if (file.endsWith('.json') && file !== targetFileName) {
const filePath = path.join(dirPath, file);
const credentials = await loadCredentialsFromFile(filePath);
if (credentials) {
// 保留已有的 expiresAt,避免被覆盖
credentials.expiresAt = mergedCredentials.expiresAt;
Object.assign(mergedCredentials, credentials);
console.debug(`[Kiro Auth] Loaded Client credentials from ${file}`);
}
}
}
} catch (error) {
console.warn(`[Kiro Auth] Error loading credentials from directory ${dirPath}: ${error.message}`);
}
// console.log('[Kiro Auth] Merged credentials:', mergedCredentials);
// Apply loaded credentials, prioritizing existing values if they are not null/undefined
this.accessToken = this.accessToken || mergedCredentials.accessToken;
this.refreshToken = this.refreshToken || mergedCredentials.refreshToken;
this.clientId = this.clientId || mergedCredentials.clientId;
this.clientSecret = this.clientSecret || mergedCredentials.clientSecret;
this.authMethod = this.authMethod || mergedCredentials.authMethod;
this.expiresAt = this.expiresAt || mergedCredentials.expiresAt;
this.profileArn = this.profileArn || mergedCredentials.profileArn;
this.region = this.region || mergedCredentials.region;
// Ensure region is set before using it in URLs
if (!this.region) {
console.warn('[Kiro Auth] Region not found in credentials. Using default region us-east-1 for URLs.');
this.region = 'us-east-1'; // Set default region
}
this.refreshUrl = (this.config.KIRO_REFRESH_URL || KIRO_CONSTANTS.REFRESH_URL).replace("{{region}}", this.region);
this.refreshIDCUrl = (this.config.KIRO_REFRESH_IDC_URL || KIRO_CONSTANTS.REFRESH_IDC_URL).replace("{{region}}", this.region);
this.baseUrl = (this.config.KIRO_BASE_URL || KIRO_CONSTANTS.BASE_URL).replace("{{region}}", this.region);
this.amazonQUrl = (KIRO_CONSTANTS.AMAZON_Q_URL).replace("{{region}}", this.region);
} catch (error) {
console.warn(`[Kiro Auth] Error during credential loading: ${error.message}`);
}
// Refresh token if forced or if access token is missing but refresh token is available
if (forceRefresh || (!this.accessToken && this.refreshToken)) {
if (!this.refreshToken) {
throw new Error('No refresh token available to refresh access token.');
}
try {
const requestBody = {
refreshToken: this.refreshToken,
};
let refreshUrl = this.refreshUrl;
if (this.authMethod !== KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
refreshUrl = this.refreshIDCUrl;
requestBody.clientId = this.clientId;
requestBody.clientSecret = this.clientSecret;
requestBody.grantType = 'refresh_token';
}
let response = null;
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL) {
response = await this.axiosSocialRefreshInstance.post(refreshUrl, requestBody);
console.log('[Kiro Auth] Token refresh social response: ok');
} else {
response = await this.axiosInstance.post(refreshUrl, requestBody);
console.log('[Kiro Auth] Token refresh idc response: ok');
}
if (response.data && response.data.accessToken) {
this.accessToken = response.data.accessToken;
this.refreshToken = response.data.refreshToken;
this.profileArn = response.data.profileArn;
const expiresIn = response.data.expiresIn;
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
this.expiresAt = expiresAt;
console.info('[Kiro Auth] Access token refreshed successfully');
// Update the token file - use specified path if configured, otherwise use default
const tokenFilePath = this.credsFilePath || path.join(this.credPath, KIRO_AUTH_TOKEN_FILE);
const updatedTokenData = {
accessToken: this.accessToken,
refreshToken: this.refreshToken,
expiresAt: expiresAt,
};
if (this.profileArn) {
updatedTokenData.profileArn = this.profileArn;
}
await saveCredentialsToFile(tokenFilePath, updatedTokenData);
} else {
throw new Error('Invalid refresh response: Missing accessToken');
}
} catch (error) {
console.error('[Kiro Auth] Token refresh failed:', error.message);
throw new Error(`Token refresh failed: ${error.message}`);
}
}
if (!this.accessToken) {
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
*/ */
getContentText(message) { getContentText(message) {
if(message==null){ if (message == null) {
return ""; return "";
} }
if (Array.isArray(message) ) { if (Array.isArray(message)) {
return message return message
.filter(part => part.type === 'text' && part.text) .filter(part => part.type === 'text' && part.text)
.map(part => part.text) .map(part => part.text)
.join(''); .join('');
} else if (typeof message.content === 'string') { } else if (typeof message.content === 'string') {
return message.content; return message.content;
} else if (Array.isArray(message.content) ) { } else if (Array.isArray(message.content)) {
return message.content return message.content
.filter(part => part.type === 'text' && part.text) .filter(part => part.type === 'text' && part.text)
.map(part => part.text) .map(part => part.text)
@ -1102,7 +1103,6 @@ async initializeAuth(forceRefresh = false) {
_processApiResponse(response) { _processApiResponse(response) {
const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data); const rawResponseText = Buffer.isBuffer(response.data) ? response.data.toString('utf8') : String(response.data);
//console.log(`[Kiro] Raw response length: ${rawResponseText.length}`);
if (rawResponseText.includes("[Called")) { if (rawResponseText.includes("[Called")) {
console.log("[Kiro] Raw response contains [Called marker."); console.log("[Kiro] Raw response contains [Called marker.");
} }
@ -1154,13 +1154,22 @@ async initializeAuth(forceRefresh = false) {
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; const finalModel = MODEL_MAPPING[model] ? model : this.modelName;
console.log(`[Kiro] Calling generateContent with model: ${finalModel}`); console.log(`[Kiro] Calling generateContent with model: ${finalModel}`);
// Estimate input tokens before making the API call
const inputTokens = this.estimateInputTokens(requestBody);
const response = await this.callApi('', finalModel, requestBody); const response = await this.callApi('', finalModel, requestBody);
try { try {
const { responseText, toolCalls } = this._processApiResponse(response); const { responseText, toolCalls } = this._processApiResponse(response);
let inputTokens = 0;
const rawResponseText = Buffer.isBuffer(response.data)
? response.data.toString('utf8')
: String(response.data);
const contextUsageMatch = rawResponseText.match(/"contextUsagePercentage":\s*([\d.]+)/);
if (contextUsageMatch) {
const percentage = parseFloat(contextUsageMatch[1]);
inputTokens = this.calculateInputTokensFromPercentage(percentage);
}
return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens); return this.buildClaudeResponse(responseText, false, 'assistant', model, toolCalls, inputTokens);
} catch (error) { } catch (error) {
console.error('[Kiro] Error in generateContent:', error); console.error('[Kiro] Error in generateContent:', error);
@ -1192,9 +1201,10 @@ async initializeAuth(forceRefresh = false) {
const followupStart = remaining.indexOf('{"followupPrompt":', searchStart); const followupStart = remaining.indexOf('{"followupPrompt":', searchStart);
const inputStart = remaining.indexOf('{"input":', searchStart); const inputStart = remaining.indexOf('{"input":', searchStart);
const stopStart = remaining.indexOf('{"stop":', searchStart); const stopStart = remaining.indexOf('{"stop":', searchStart);
const contextUsageStart = remaining.indexOf('{"contextUsagePercentage":', searchStart);
// 找到最早出现的有效 JSON 模式 // 找到最早出现的有效 JSON 模式
const candidates = [contentStart, nameStart, followupStart, inputStart, stopStart].filter(pos => pos >= 0); const candidates = [contentStart, nameStart, followupStart, inputStart, stopStart, contextUsageStart].filter(pos => pos >= 0);
if (candidates.length === 0) break; if (candidates.length === 0) break;
const jsonStart = Math.min(...candidates); const jsonStart = Math.min(...candidates);
@ -1284,6 +1294,15 @@ async initializeAuth(forceRefresh = false) {
} }
}); });
} }
// 处理 context usage percentage 事件
else if (parsed.contextUsagePercentage !== undefined) {
events.push({
type: 'contextUsage',
data: {
percentage: parsed.contextUsagePercentage
}
});
}
} catch (e) { } catch (e) {
// JSON 解析失败,跳过这个位置继续搜索 // JSON 解析失败,跳过这个位置继续搜索
} }
@ -1330,7 +1349,7 @@ async initializeAuth(forceRefresh = false) {
stream = response.data; stream = response.data;
let buffer = ''; let buffer = '';
let lastContentEvent = null; // 用于检测连续重复的 content 事件 let lastContentEvent = null;
for await (const chunk of stream) { for await (const chunk of stream) {
buffer += chunk.toString(); buffer += chunk.toString();
@ -1355,6 +1374,8 @@ async initializeAuth(forceRefresh = false) {
yield { type: 'toolUseInput', input: event.data.input }; yield { type: 'toolUseInput', input: event.data.input };
} else if (event.type === 'toolUseStop') { } else if (event.type === 'toolUseStop') {
yield { type: 'toolUseStop', stop: event.data.stop }; yield { type: 'toolUseStop', stop: event.data.stop };
} else if (event.type === 'contextUsage') {
yield { type: 'contextUsage', percentage: event.data.percentage };
} }
} }
} }
@ -1438,46 +1459,69 @@ async initializeAuth(forceRefresh = false) {
const finalModel = MODEL_MAPPING[model] ? model : this.modelName; const finalModel = MODEL_MAPPING[model] ? model : this.modelName;
console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`); console.log(`[Kiro] Calling generateContentStream with model: ${finalModel} (real streaming)`);
const inputTokens = this.estimateInputTokens(requestBody); let inputTokens = 0;
let contextUsagePercentage = null;
const messageId = `${uuidv4()}`; const messageId = `${uuidv4()}`;
let messageStartSent = false;
const bufferedEvents = [];
try { try {
// 1. 先发送 message_start 事件
yield {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
model: model,
usage: { input_tokens: inputTokens, output_tokens: 0 },
content: []
}
};
// 2. 发送 content_block_start 事件
yield {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" }
};
let totalContent = ''; let totalContent = '';
let outputTokens = 0; let outputTokens = 0;
const toolCalls = []; const toolCalls = [];
let currentToolCall = null; // 用于累积结构化工具调用 let currentToolCall = null;
// 3. 流式接收并发送每个 content_block_delta
for await (const event of this.streamApiReal('', finalModel, requestBody)) { for await (const event of this.streamApiReal('', finalModel, requestBody)) {
if (event.type === 'content' && event.content) { if (event.type === 'contextUsage' && event.percentage) {
totalContent += event.content; contextUsagePercentage = event.percentage;
// 不再每个 chunk 都计算 token改为最后统一计算避免阻塞事件循环 inputTokens = this.calculateInputTokensFromPercentage(contextUsagePercentage);
yield { if (!messageStartSent) {
yield {
type: "message_start",
message: {
id: messageId,
type: "message",
role: "assistant",
model: model,
usage: {
input_tokens: inputTokens,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
},
content: []
}
};
yield {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" }
};
messageStartSent = true;
for (const buffered of bufferedEvents) {
yield buffered;
}
bufferedEvents.length = 0;
}
} else if (event.type === 'content' && event.content) {
totalContent += event.content;
const contentEvent = {
type: "content_block_delta", type: "content_block_delta",
index: 0, index: 0,
delta: { type: "text_delta", text: event.content } delta: { type: "text_delta", text: event.content }
}; };
if (messageStartSent) {
yield contentEvent;
} else {
bufferedEvents.push(contentEvent);
}
} else if (event.type === 'toolUse') { } else if (event.type === 'toolUse') {
const tc = event.toolUse; const tc = event.toolUse;
// 工具调用事件(包含 name 和 toolUseId // 工具调用事件(包含 name 和 toolUseId
@ -1508,7 +1552,7 @@ async initializeAuth(forceRefresh = false) {
if (tc.stop) { if (tc.stop) {
try { try {
currentToolCall.input = JSON.parse(currentToolCall.input); currentToolCall.input = JSON.parse(currentToolCall.input);
} catch (e) {} } catch (e) { }
toolCalls.push(currentToolCall); toolCalls.push(currentToolCall);
currentToolCall = null; currentToolCall = null;
} }
@ -1536,11 +1580,17 @@ async initializeAuth(forceRefresh = false) {
if (currentToolCall) { if (currentToolCall) {
try { try {
currentToolCall.input = JSON.parse(currentToolCall.input); currentToolCall.input = JSON.parse(currentToolCall.input);
} catch (e) {} } catch (e) { }
toolCalls.push(currentToolCall); toolCalls.push(currentToolCall);
currentToolCall = null; currentToolCall = null;
} }
// Fallback: 如果 contextUsagePercentage 没有收到,抛出错误
if (!messageStartSent) {
console.error('[Kiro Stream] contextUsagePercentage not received from API - cannot calculate accurate input tokens');
throw new Error('Failed to receive contextUsagePercentage from Kiro API. Input token calculation requires this data.');
}
// 检查文本内容中的 bracket 格式工具调用 // 检查文本内容中的 bracket 格式工具调用
const bracketToolCalls = parseBracketToolCalls(totalContent); const bracketToolCalls = parseBracketToolCalls(totalContent);
if (bracketToolCalls && bracketToolCalls.length > 0) { if (bracketToolCalls && bracketToolCalls.length > 0) {
@ -1595,8 +1645,16 @@ async initializeAuth(forceRefresh = false) {
yield { yield {
type: "message_delta", type: "message_delta",
delta: { stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn" }, delta: {
usage: { output_tokens: outputTokens } stop_reason: toolCalls.length > 0 ? "tool_use" : "end_turn",
stop_sequence: null
},
usage: {
input_tokens: inputTokens,
output_tokens: outputTokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
}
}; };
// 7. 发送 message_stop 事件 // 7. 发送 message_stop 事件
@ -1623,9 +1681,27 @@ async initializeAuth(forceRefresh = false) {
} }
/** /**
* Convert context usage percentage to actual input tokens
* @param {number} percentage - Context usage percentage (0-100)
* @returns {number} Actual input tokens
*/
calculateInputTokensFromPercentage(percentage) {
if (!percentage || percentage <= 0) {
return 0;
}
const contextWindow = CLAUDE_DEFAULT_MAX_TOKENS;
const inputTokens = Math.round((percentage / 100) * contextWindow);
return inputTokens;
}
/**
* @deprecated Use contextUsagePercentage from API response instead
* Calculate input tokens from request body using Claude's official tokenizer * Calculate input tokens from request body using Claude's official tokenizer
*/ */
estimateInputTokens(requestBody) { estimateInputTokens(requestBody) {
console.warn('[Kiro] estimateInputTokens() is deprecated. Use contextUsagePercentage from API response instead.');
let totalTokens = 0; let totalTokens = 0;
// Count system prompt tokens // Count system prompt tokens
@ -1824,7 +1900,9 @@ async initializeAuth(forceRefresh = false) {
stop_sequence: null, stop_sequence: null,
usage: { usage: {
input_tokens: inputTokens, input_tokens: inputTokens,
output_tokens: outputTokens output_tokens: outputTokens,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0
}, },
content: contentArray content: contentArray
}; };
@ -1950,7 +2028,7 @@ async initializeAuth(forceRefresh = false) {
origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR, origin: KIRO_CONSTANTS.ORIGIN_AI_EDITOR,
resourceType: resourceType resourceType: resourceType
}); });
if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) { if (this.authMethod === KIRO_CONSTANTS.AUTH_METHOD_SOCIAL && this.profileArn) {
params.append('profileArn', this.profileArn); params.append('profileArn', this.profileArn);
} }
const fullUrl = `${usageLimitsUrl}?${params.toString()}`; const fullUrl = `${usageLimitsUrl}?${params.toString()}`;