MCPilot / src /utils /token_counter.py
girish-hari's picture
checking-in the project source code
2358888
"""
Improved token counting using tiktoken.
Provides accurate token estimates for different models.
"""
import json
from typing import Any, Dict
class TokenCounter:
"""Count tokens accurately for different models."""
def __init__(self):
self._tiktoken_available = False
self._encoders = {}
try:
import tiktoken
self._tiktoken_available = True
self.tiktoken = tiktoken
except ImportError:
print("Warning: tiktoken not installed. Using fallback token counting.")
print("Install with: pip install tiktoken")
def count_tokens(self, text: str, model: str = 'claude-sonnet-4') -> int:
"""
Count tokens for given text and model.
Args:
text: Text to count tokens for
model: Model name (affects tokenization)
Returns:
Number of tokens
"""
if not self._tiktoken_available:
return self._fallback_count(text)
# Map our model names to tiktoken encodings
encoding_map = {
'claude-sonnet-4': 'cl100k_base', # Claude uses similar to GPT-4
'claude-opus-4': 'cl100k_base',
'claude-haiku-4': 'cl100k_base',
'gpt-4-turbo': 'cl100k_base',
'gpt-4o': 'o200k_base',
'gpt-3.5-turbo': 'cl100k_base',
'gemini-pro': 'cl100k_base', # Approximation
'gemini-flash': 'cl100k_base',
}
encoding_name = encoding_map.get(model, 'cl100k_base')
# Get or create encoder
if encoding_name not in self._encoders:
try:
self._encoders[encoding_name] = self.tiktoken.get_encoding(encoding_name)
except Exception as e:
print(f"Warning: Could not load encoding {encoding_name}: {e}")
return self._fallback_count(text)
encoder = self._encoders[encoding_name]
try:
tokens = encoder.encode(text)
return len(tokens)
except Exception as e:
print(f"Warning: Token counting failed: {e}")
return self._fallback_count(text)
def count_tokens_for_dict(self, data: Dict[str, Any], model: str = 'claude-sonnet-4') -> int:
"""
Count tokens for a dictionary (API response).
Args:
data: Dictionary to count tokens for
model: Model name
Returns:
Number of tokens
"""
# Convert to JSON string for counting
text = json.dumps(data, indent=None)
return self.count_tokens(text, model)
def _fallback_count(self, text: str) -> int:
"""
Fallback token counting when tiktoken unavailable.
Simple heuristic: ~4 characters per token for English text.
"""
return len(text) // 4
def count_tokens_detailed(self, text: str, model: str = 'claude-sonnet-4') -> Dict:
"""
Get detailed token count information.
Returns:
Dictionary with token count and breakdown
"""
total_tokens = self.count_tokens(text, model)
return {
'total_tokens': total_tokens,
'character_count': len(text),
'avg_chars_per_token': len(text) / total_tokens if total_tokens > 0 else 0,
'model': model,
'method': 'tiktoken' if self._tiktoken_available else 'fallback'
}
def compare_models(self, text: str, models: list = None) -> Dict[str, int]:
"""
Compare token counts across different models.
Args:
text: Text to count
models: List of model names to compare
Returns:
Dictionary mapping model names to token counts
"""
if models is None:
models = ['claude-sonnet-4', 'gpt-4-turbo', 'gpt-4o', 'gemini-pro']
results = {}
for model in models:
results[model] = self.count_tokens(text, model)
return results
# Global instance
_token_counter = None
def get_token_counter() -> TokenCounter:
"""Get the global token counter instance."""
global _token_counter
if _token_counter is None:
_token_counter = TokenCounter()
return _token_counter