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:
commit
d4e5a25c9d
1 changed files with 359 additions and 281 deletions
|
|
@ -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',
|
||||||
|
|
@ -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,11 +1459,25 @@ 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 事件
|
let totalContent = '';
|
||||||
|
let outputTokens = 0;
|
||||||
|
const toolCalls = [];
|
||||||
|
let currentToolCall = null;
|
||||||
|
|
||||||
|
for await (const event of this.streamApiReal('', finalModel, requestBody)) {
|
||||||
|
if (event.type === 'contextUsage' && event.percentage) {
|
||||||
|
contextUsagePercentage = event.percentage;
|
||||||
|
inputTokens = this.calculateInputTokensFromPercentage(contextUsagePercentage);
|
||||||
|
|
||||||
|
if (!messageStartSent) {
|
||||||
yield {
|
yield {
|
||||||
type: "message_start",
|
type: "message_start",
|
||||||
message: {
|
message: {
|
||||||
|
|
@ -1450,34 +1485,43 @@ async initializeAuth(forceRefresh = false) {
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
model: model,
|
model: model,
|
||||||
usage: { input_tokens: inputTokens, output_tokens: 0 },
|
usage: {
|
||||||
|
input_tokens: inputTokens,
|
||||||
|
output_tokens: 0,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0
|
||||||
|
},
|
||||||
content: []
|
content: []
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 发送 content_block_start 事件
|
|
||||||
yield {
|
yield {
|
||||||
type: "content_block_start",
|
type: "content_block_start",
|
||||||
index: 0,
|
index: 0,
|
||||||
content_block: { type: "text", text: "" }
|
content_block: { type: "text", text: "" }
|
||||||
};
|
};
|
||||||
|
|
||||||
let totalContent = '';
|
messageStartSent = true;
|
||||||
let outputTokens = 0;
|
|
||||||
const toolCalls = [];
|
|
||||||
let currentToolCall = null; // 用于累积结构化工具调用
|
|
||||||
|
|
||||||
// 3. 流式接收并发送每个 content_block_delta
|
for (const buffered of bufferedEvents) {
|
||||||
for await (const event of this.streamApiReal('', finalModel, requestBody)) {
|
yield buffered;
|
||||||
if (event.type === 'content' && event.content) {
|
}
|
||||||
|
bufferedEvents.length = 0;
|
||||||
|
}
|
||||||
|
} else if (event.type === 'content' && event.content) {
|
||||||
totalContent += event.content;
|
totalContent += event.content;
|
||||||
// 不再每个 chunk 都计算 token,改为最后统一计算,避免阻塞事件循环
|
|
||||||
|
|
||||||
yield {
|
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)
|
||||||
|
|
@ -1541,6 +1585,12 @@ async initializeAuth(forceRefresh = false) {
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue