UNPKG

@earendil-works/pi-ai

Version:

Unified LLM API with automatic model discovery and provider configuration

95 lines • 3.75 kB
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