Skip to main content
Glama
tokenEstimator.ts7.99 kB
/** * Token estimation utility for Claude context window management * * Character-based estimation with 20% safety margin. * Based on research.md R001: 1 token ≈ 4 characters heuristic. */ import { expect } from 'vitest'; /** * Estimate Claude token count from text * * Uses 4 characters per token heuristic with 20% safety margin. * Target performance: <1ms for typical API responses. * * @param text - Text to estimate tokens for * @returns Estimated token count (includes 20% safety margin) * * @example * ```ts * estimateTokens("Hello world") // Returns 4 * estimateTokens("A".repeat(1000)) // Returns 300 * ``` */ export function estimateTokens(text: string): number { const charCount = text.length; const baseEstimate = charCount / 4; const safetyMargin = baseEstimate * 0.20; return Math.ceil(baseEstimate + safetyMargin); } /** * Estimate tokens from a JavaScript object * * Serializes object to JSON and estimates tokens from the string representation. * * @param obj - Object to estimate tokens for * @returns Estimated token count * * @example * ```ts * estimateTokensFromObject({ id: "BK12345", status: "confirmed" }) // Returns ~12 * ``` */ export function estimateTokensFromObject(obj: unknown): number { const jsonStr = JSON.stringify(obj); return estimateTokens(jsonStr); } /** * Estimate tokens from an array of items * * @param items - Array of items to estimate tokens for * @returns Estimated token count for entire array * * @example * ```ts * estimateTokensFromList([{ id: "1" }, { id: "2" }]) // Returns ~7 * ``` */ export function estimateTokensFromList(items: unknown[]): number { const jsonStr = JSON.stringify(items); return estimateTokens(jsonStr); } export interface TokenBudgetResult { /** Estimated token count */ estimated: number; /** Whether the threshold was exceeded */ exceeds: boolean; /** Ratio of budget used (1.0 = 100%) */ ratio: number; } /** * Check if text exceeds token budget threshold * * @param text - Text to check * @param threshold - Token budget threshold (default: 4000) * @returns Token budget analysis * * @example * ```ts * checkTokenBudget("Hello".repeat(1000), 4000) * // Returns { estimated: 1800, exceeds: false, ratio: 0.45 } * ``` */ export function checkTokenBudget( text: string, threshold: number = 4000 ): TokenBudgetResult { const estimated = estimateTokens(text); const exceeds = estimated > threshold; const ratio = threshold > 0 ? estimated / threshold : 0; return { estimated, exceeds, ratio }; } /** * Calculate token reduction needed to meet threshold * * @param currentTokens - Current estimated token count * @param targetThreshold - Target token threshold * @returns Reduction analysis * * @example * ```ts * estimateReductionNeeded(6000, 4000) * // Returns { tokensToReduce: 2000, reductionRatio: 0.33 } * ``` */ export function estimateReductionNeeded( currentTokens: number, targetThreshold: number = 4000 ): { tokensToReduce: number; reductionRatio: number } { if (currentTokens <= targetThreshold) { return { tokensToReduce: 0, reductionRatio: 0 }; } const tokensToReduce = currentTokens - targetThreshold; const reductionRatio = tokensToReduce / currentTokens; return { tokensToReduce, reductionRatio }; } /** * Calculate safe page size given average item token count * * @param avgItemTokens - Average tokens per item in list * @param targetThreshold - Target token threshold * @param overheadTokens - Overhead tokens for metadata/envelope (default: 200) * @returns Recommended page size (minimum 1) * * @example * ```ts * calculateSafePageSize(50, 4000) // Returns 76 * calculateSafePageSize(500, 4000) // Returns 7 * ``` */ export function calculateSafePageSize( avgItemTokens: number, targetThreshold: number = 4000, overheadTokens: number = 200 ): number { const availableTokens = targetThreshold - overheadTokens; if (availableTokens <= 0) { return 1; } const pageSize = Math.floor(availableTokens / avgItemTokens); return Math.max(1, pageSize); } // ============================================================================ // Vitest Assertion Helpers // ============================================================================ /** * Assert that content is within token budget threshold * * @param content - Content to check (string or object) * @param threshold - Token budget threshold * @param message - Optional custom error message * * @example * ```ts * const response = await mcpClient.callTool("list_properties"); * assertWithinTokenBudget(response.content, 50000); * ``` */ export function assertWithinTokenBudget( content: string | unknown, threshold: number, message?: string ): void { const text = typeof content === 'string' ? content : JSON.stringify(content); const result = checkTokenBudget(text, threshold); expect( result.exceeds, message || `Token count ${result.estimated} exceeds threshold ${threshold} (${(result.ratio * 100).toFixed(1)}% of budget)` ).toBe(false); } /** * Assert that content is below hard token cap * * Hard cap violations are CRITICAL - they indicate a bug in overflow safety. * * @param content - Content to check (string or object) * @param hardCap - Hard cap limit (default: from env or 100000) * @param message - Optional custom error message * * @example * ```ts * const response = await mcpClient.callTool("get_financial_reports"); * assertBelowHardCap(response.content); // Uses env MCP_OUTPUT_TOKEN_HARD_CAP * ``` */ export function assertBelowHardCap( content: string | unknown, hardCap?: number, message?: string ): void { const cap = hardCap || parseInt(process.env.MCP_OUTPUT_TOKEN_HARD_CAP || '100000', 10); const text = typeof content === 'string' ? content : JSON.stringify(content); const estimated = estimateTokens(text); expect( estimated, message || `HARD CAP VIOLATION: ${estimated} tokens exceeds hard cap ${cap}` ).toBeLessThanOrEqual(cap); } /** * Assert that preview mode was triggered (for large responses) * * @param response - Response object to check * @param message - Optional custom error message * * @example * ```ts * const response = await mcpClient.callTool("get_financial_reports", { * start_date: "2020-01-01", * end_date: "2024-12-31" * }); * assertPreviewMode(response.content); * ``` */ export function assertPreviewMode( response: unknown, message?: string ): void { const obj = typeof response === 'string' ? JSON.parse(response) : response; const meta = (obj as { meta?: { summary?: { kind?: string } } }).meta; expect( meta?.summary?.kind, message || 'Expected preview mode to be triggered (meta.summary.kind should be "preview")' ).toBe('preview'); } /** * Assert that pagination cursor is present when expected * * @param response - Response object to check * @param shouldHaveCursor - Whether cursor should be present * @param message - Optional custom error message * * @example * ```ts * const response = await mcpClient.callTool("list_properties", { limit: 10 }); * if (response.content.hasMore) { * assertPaginationCursor(response.content, true); * } * ``` */ export function assertPaginationCursor( response: unknown, shouldHaveCursor: boolean, message?: string ): void { const obj = typeof response === 'string' ? JSON.parse(response) : response; const cursor = (obj as { nextCursor?: string }).nextCursor; if (shouldHaveCursor) { expect( cursor, message || 'Expected pagination cursor to be present' ).toBeDefined(); expect( cursor, message || 'Expected pagination cursor to be non-empty' ).not.toBe(''); } else { // Accept both null (from JSON) and undefined (from JavaScript) expect( cursor, message || 'Expected no pagination cursor (last page)' ).toBeFalsy(); // null, undefined, empty string all pass } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/darrentmorgan/hostaway-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server