@earendil-works/pi-ai
Version:
Unified LLM API with automatic model discovery and provider configuration
95 lines • 3.75 kB
JavaScript
import { getSystemMessageText } from "./text.js";
const CHARS_PER_TOKEN = 3.5;
const ESTIMATED_IMAGE_CHARS = 4800;
export function calculateContextTokens(usage) {
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
}
function safeJsonStringify(value) {
try {
return JSON.stringify(value) ?? "undefined";
}
catch {
return "[unserializable]";
}
}
function estimateTextAndImageContentChars(content) {
if (typeof content === "string")
return content.length;
let chars = 0;
for (const block of content)
chars += block.type === "text" ? block.text.length : ESTIMATED_IMAGE_CHARS;
return chars;
}
export function estimateTextTokens(text) {
return Math.ceil(text.length / CHARS_PER_TOKEN);
}
export function estimateTextAndImageContentTokens(content) {
return Math.ceil(estimateTextAndImageContentChars(content) / CHARS_PER_TOKEN);
}
export function estimateMessageTokens(message) {
let chars = 0;
if (message.role === "system") {
return (estimateTextTokens(getSystemMessageText(message)) +
estimateToolsTokens(message.toolsAdded) +
estimateToolsTokens(message.toolsRemoved));
}
if (message.role === "user")
return estimateTextAndImageContentTokens(message.content);
if (message.role === "toolResult")
return estimateTextAndImageContentTokens(message.content);
for (const block of message.content) {
if (block.type === "text") {
chars += block.text.length;
}
else if (block.type === "thinking") {
chars += block.thinking.length;
}
else {
chars += block.name.length + safeJsonStringify(block.arguments).length;
}
}
return Math.ceil(chars / CHARS_PER_TOKEN);
}
function getLastAssistantUsageInfo(messages) {
let latestPrefixTimestamp = Number.NEGATIVE_INFINITY;
let usageInfo;
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
if (message.role === "assistant") {
const assistant = message;
// A newer prefix message was inserted after this response (for example, a
// compaction summary), so its usage cannot describe the current prefix.
const usageAppliesToPrefix = assistant.timestamp >= latestPrefixTimestamp;
if (usageAppliesToPrefix &&
assistant.stopReason !== "aborted" &&
assistant.stopReason !== "error" &&
calculateContextTokens(assistant.usage) > 0) {
usageInfo = { usage: assistant.usage, index: i };
}
}
latestPrefixTimestamp = Math.max(latestPrefixTimestamp, message.timestamp);
}
return usageInfo;
}
export function estimateContextTokens(context) {
const messages = "messages" in context ? context.messages : context;
const usageInfo = getLastAssistantUsageInfo(messages);
if (usageInfo) {
const usageTokens = calculateContextTokens(usageInfo.usage);
let trailingTokens = 0;
for (let i = usageInfo.index + 1; i < messages.length; i++) {
trailingTokens += estimateMessageTokens(messages[i]);
}
return { tokens: usageTokens + trailingTokens, usageTokens, trailingTokens, lastUsageIndex: usageInfo.index };
}
let tokens = 0;
for (const message of messages)
tokens += estimateMessageTokens(message);
return { tokens, usageTokens: 0, trailingTokens: tokens, lastUsageIndex: null };
}
function estimateToolsTokens(tools) {
if (!tools || tools.length === 0)
return 0;
return estimateTextTokens(safeJsonStringify(tools));
}
//# sourceMappingURL=estimate.js.map