Arag / app /utils /token_counter.py
AuthorBot
Restructure project for HF Spaces deployment
772f852
Raw
History Blame Contribute Delete
2.95 kB
"""Author RAG Chatbot SaaS — Token Counter Utility.
Counts tokens for gpt-4o using tiktoken before sending to OpenAI.
RULE: Always call count_tokens() before assembling the context
to ensure we stay within RAG_MAX_CONTEXT_TOKENS.
"""
import structlog
import tiktoken
logger = structlog.get_logger(__name__)
# gpt-4o uses the same tokenizer as gpt-4
_ENCODING_NAME = "cl100k_base"
_encoding: tiktoken.Encoding | None = None
def _get_encoding() -> tiktoken.Encoding:
"""Lazily load and cache the tiktoken encoding."""
global _encoding
if _encoding is None:
_encoding = tiktoken.get_encoding(_ENCODING_NAME)
return _encoding
def count_tokens(text: str) -> int:
"""Count the number of tokens in a text string.
Args:
text: Input string to tokenize.
Returns:
Integer token count.
"""
return len(_get_encoding().encode(text))
def count_messages_tokens(messages: list[dict]) -> int:
"""Count tokens for a list of OpenAI chat messages.
Accounts for per-message overhead (role + separators).
Args:
messages: List of dicts with 'role' and 'content' keys.
Returns:
Total token count including overhead.
"""
encoding = _get_encoding()
total = 0
for msg in messages:
# Each message has: 4 overhead tokens + role + content
total += 4
total += len(encoding.encode(msg.get("role", "")))
total += len(encoding.encode(msg.get("content", "")))
total += 2 # Reply priming
return total
def trim_messages_to_budget(
messages: list[dict],
max_tokens: int,
preserve_system: bool = True,
) -> list[dict]:
"""Trim conversation history to fit within a token budget.
Removes oldest messages first. System message is always preserved
if preserve_system=True.
Args:
messages: Full list of chat messages (oldest first).
max_tokens: Maximum allowed token count.
preserve_system: If True, never remove the system message.
Returns:
Trimmed list of messages that fits within max_tokens.
"""
if count_messages_tokens(messages) <= max_tokens:
return messages
system_msgs = [m for m in messages if m["role"] == "system"] if preserve_system else []
history_msgs = [m for m in messages if m["role"] != "system"]
while history_msgs and count_messages_tokens(system_msgs + history_msgs) > max_tokens:
history_msgs.pop(0) # Remove oldest non-system message
logger.debug("Trimmed oldest message from context", remaining=len(history_msgs))
return system_msgs + history_msgs
def fits_budget(text: str, budget: int) -> bool:
"""Check whether a text fits within a token budget.
Args:
text: Input string to evaluate.
budget: Maximum allowed token count.
Returns:
True if token count is within budget, False if it exceeds it.
"""
return count_tokens(text) <= budget