/** * Token validation utilities for streaming responses and input validation */ /** * Approximate characters per token (used for token estimation) * Can be overridden by importing from config-utils if needed */ export const APPROX_CHARS_PER_TOKEN = 4; /** * Estimates token count from text using character approximation * @param text - Text to estimate tokens for * @returns Estimated token count */ export function estimateTokenCount(input: string | number): number { if (typeof input === 'number') { if (!isFinite(input) || input <= 0) { return 0; } return Math.ceil(input / APPROX_CHARS_PER_TOKEN); } if (!input || typeof input !== 'string') { return 0; } return Math.ceil(input.length / APPROX_CHARS_PER_TOKEN); } /** * Streaming token counter for monitoring output token limits */ export class StreamingTokenCounter { private tokenCount: number = 0; private charCount: number = 0; private maxTokens: number; private provider: string; constructor(maxTokens: number, provider: string = 'unknown') { this.maxTokens = Math.max(1, Math.floor(maxTokens)); this.provider = provider; } /** * Add text chunk and update token count * @param chunk - Text chunk from streaming response * @returns Object with current count and whether limit exceeded */ addChunk(chunk: string): { tokenCount: number; exceeded: boolean; shouldStop: boolean } { if (!chunk || typeof chunk !== 'string') { return { tokenCount: this.tokenCount, exceeded: false, shouldStop: false }; } this.charCount += chunk.length; this.tokenCount = estimateTokenCount(this.charCount); const exceeded = this.tokenCount > this.maxTokens; const shouldStop = this.tokenCount >= this.maxTokens * 0.95; // Stop at 95% to prevent overflow return { tokenCount: this.tokenCount, exceeded, shouldStop }; } /** * Get current token statistics */ getStats(): { tokenCount: number; charCount: number; maxTokens: number; utilization: number } { return { tokenCount: this.tokenCount, charCount: this.charCount, maxTokens: this.maxTokens, utilization: this.maxTokens > 0 ? this.tokenCount / this.maxTokens : 0 }; } /** * Reset counter for new streaming session */ reset(): void { this.tokenCount = 0; this.charCount = 0; } /** * Check if approaching token limit (80% threshold) */ isApproachingLimit(): boolean { return this.tokenCount >= this.maxTokens * 0.8; } /** * Get warning message if approaching or exceeding limits */ getWarningMessage(): string | null { if (this.tokenCount > this.maxTokens) { return `${this.provider} response exceeded token limit: ${this.tokenCount}/${this.maxTokens} tokens`; } else if (this.isApproachingLimit()) { return `${this.provider} response approaching token limit: ${this.tokenCount}/${this.maxTokens} tokens`; } return null; } } /** * Validates input prompt against token limits * @param prompt - Input prompt to validate * @param maxInputTokens - Maximum allowed input tokens * @param provider - Provider name for error messages * @returns Validation result with token count and any errors */ export function validateInputTokens( prompt: string, maxInputTokens: number, provider: string = 'unknown' ): { valid: boolean; tokenCount: number; error?: string } { if (!prompt || typeof prompt !== 'string') { return { valid: false, tokenCount: 0, error: 'Prompt cannot be empty' }; } const tokenCount = estimateTokenCount(prompt); if (maxInputTokens > 0 && tokenCount > maxInputTokens) { return { valid: false, tokenCount, error: `Prompt too long: ~${tokenCount} tokens (max ${maxInputTokens} for ${provider})` }; } return { valid: true, tokenCount }; } /** * Creates a streaming wrapper that monitors token limits * @param stream - Original async generator * @param maxTokens - Maximum output tokens * @param provider - Provider name for logging * @returns Token-aware streaming generator */ export async function* createTokenAwareStream( stream: AsyncGenerator, maxTokens: number, provider: string = 'unknown' ): AsyncGenerator { const counter = new StreamingTokenCounter(maxTokens, provider); let warningLogged = false; try { for await (const chunk of stream) { // Only count text chunks if (typeof chunk === 'string') { const stats = counter.addChunk(chunk); // Log warning once when approaching limit if (!warningLogged && counter.isApproachingLimit()) { console.warn(counter.getWarningMessage()); warningLogged = true; } // Stop streaming if exceeded limit if (stats.shouldStop) { console.warn(`Stopping ${provider} stream: token limit reached (${stats.tokenCount}/${maxTokens})`); break; } } yield chunk; } } finally { // Log final statistics const finalStats = counter.getStats(); if (finalStats.tokenCount > 0) { console.info(`${provider} stream completed: ${finalStats.tokenCount} tokens (${(finalStats.utilization * 100).toFixed(1)}% of limit)`); } } }