token-budget-advisor
Token budget management — context window allocation, system prompt slimming, token counting per message, cost estimation, context pruning, cache optimization, multi-session management.
Token Budget Advisor
Manage token budgets across system prompts, conversation history, retrieval context, and model outputs. Estimate costs, prune context efficiently, and optimize for cache hits.
When to Use
- Planning a multi-step LLM workflow and need to budget context windows.
- Estimating costs before executing a batch of LLM calls.
- Deciding what context to include vs prune when approaching token limits.
- Optimizing prompts for cache hit rates across sessions.
- Managing multiple concurrent sessions with shared token budgets.
- Designing system prompts that stay within caching constraints.
Core Concepts
1. Context Window Allocation
from dataclasses import dataclass
@dataclass
class TokenBudget:
"""Budget allocation for a single LLM conversation."""
total_window: int # e.g., 200_000 for Claude Sonnet 4
system_prompt: int = 0 # Reserved for system instructions
conversation_history: int = 0 # Used by message turns
retrieval_context: int = 0 # Used by retrieved documents
reserved_for_output: int = 0 # Headroom for the model's response
available: int = 0 # Remaining tokens
def __post_init__(self):
self.available = (
self.total_window
- self.system_prompt
- self.conversation_history
- self.retrieval_context
- self.reserved_for_output
)
def can_fit(self, tokens: int) -> bool:
return tokens <= self.available
def allocate(self, category: str, tokens: int) -> bool:
"""Allocate tokens to a category. Returns False if over budget."""
if not self.can_fit(tokens):
return False
if category == "system_prompt":
self.system_prompt += tokens
elif category == "history":
self.conversation_history += tokens
elif category == "retrieval":
self.retrieval_context += tokens
elif category == "output":
self.reserved_for_output += tokens
self.available -= tokens
return True
# Example: Budget for a code review task
budget = TokenBudget(
total_window=200_000,
system_prompt=500, # System prompt
retrieval_context=20_000, # Code files to review
reserved_for_output=4_000, # Expected review output
)
# available = 200_000 - 500 - 0 - 20_000 - 4_000 = 175_500
2. System Prompt Slimming
def slim_system_prompt(prompt: str, target_tokens: int) -> str:
"""Compress a system prompt to fit within a token budget."""
current_tokens = estimate_tokens(prompt)
if current_tokens <= target_tokens:
return prompt
# Step 1: Remove blank lines and excess whitespace
slimmed = "\n".join(line.rstrip() for line in prompt.split("\n") if line.strip())
current_tokens = estimate_tokens(slimmed)
if current_tokens <= target_tokens:
return slimmed
# Step 2: Remove comments and examples (lines starting with # or containing code blocks)
lines = slimmed.split("\n")
essential_lines = []
for line in lines:
stripped = line.strip()
if stripped.startswith("#") and not stripped.startswith("#!"):
continue # Skip comments
if "```" in stripped:
continue # Skip code fences
essential_lines.append(line)
slimmed = "\n".join(essential_lines)
current_tokens = estimate_tokens(slimmed)
if current_tokens <= target_tokens:
return slimmed
# Step 3: Hard truncate (last resort)
words = slimmed.split()
target_words = int(target_tokens * 0.75) # Conservative: ~0.75 tokens per word
return " ".join(words[:target_words]) + "..."
3. Token Counting Per Message
import tiktoken # OpenAI's tokenizer (works as approximation for most models)
def get_tokenizer(model: str = "cl100k_base"):
"""Get the appropriate tokenizer for a model."""
return tiktoken.get_encoding(model)
def count_tokens(text: str, model: str = "cl100k_base") -> int:
"""Count tokens in a text string."""
enc = get_tokenizer(model)
return len(enc.encode(text))
def count_message_tokens(messages: list[dict], model: str = "cl100k_base") -> int:
"""Count tokens across a list of messages."""
enc = get_tokenizer(model)
total = 0
for msg in messages:
# Each message has overhead: role + content
total += 4 # Base message overhead
total += len(enc.encode(msg.get("content", "")))
total += len(enc.encode(msg.get("role", "")))
if "name" in msg:
total += len(enc.encode(msg["name"]))
return total
# Usage
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "Review this file..."},
]
total = count_message_tokens(messages)
print(f"Total tokens: {total}")
4. Cost Estimation Before Execution
@dataclass
class ModelPricing:
input_per_million: float
output_per_million: float
cache_read_per_million: float = 0
cache_create_per_million: float = 0
PRICING = {
"claude-sonnet-4": ModelPricing(3.0, 15.0, 0.30, 3.75),
"claude-haiku": ModelPricing(0.80, 4.0, 0.08, 1.0),
"claude-opus": ModelPricing(15.0, 75.0, 1.50, 18.75),
"gpt-4o": ModelPricing(2.50, 10.0, 0.25, 2.50),
"gpt-4o-mini": ModelPricing(0.15, 0.60, 0.075, 0.15),
}
def estimate_call_cost(
input_tokens: int,
output_tokens: int,
model: str,
cache_read: int = 0,
cache_create: int = 0,
) -> float:
"""Estimate cost for a single LLM call."""
p = PRICING.get(model)
if not p:
raise ValueError(f"Unknown model: {model}")
non_cached_input = input_tokens - cache_read - cache_create
input_cost = (non_cached_input * p.input_per_million / 1_000_000)
cache_read_cost = cache_read * p.cache_read_per_million / 1_000_000
cache_create_cost = cache_create * p.cache_create_per_million / 1_000_000
output_cost = output_tokens * p.output_per_million / 1_000_000
return input_cost + cache_read_cost + cache_create_cost + output_cost
def estimate_workflow_cost(calls: list[dict], model: str) -> float:
"""Estimate total cost for a multi-call workflow."""
total = 0.0
for call in calls:
total += estimate_call_cost(
input_tokens=call["input_tokens"],
output_tokens=call.get("output_tokens", 1000),
model=model,
cache_read=call.get("cache_read", 0),
cache_create=call.get("cache_create", 0),
)
return total
# Example
workflow = [
{"input_tokens": 5000, "output_tokens": 500, "cache_create": 2000},
{"input_tokens": 3000, "output_tokens": 2000, "cache_read": 2000},
{"input_tokens": 2000, "output_tokens": 3000},
]
cost = estimate_workflow_cost(workflow, "claude-sonnet-4")
print(f"Estimated cost: ${cost:.4f}")
5. Context Pruning Strategies
def prune_context(
messages: list[dict],
max_tokens: int,
strategy: str = "recent_first",
) -> list[dict]:
"""Prune messages to fit within token budget."""
current = count_message_tokens(messages)
if current <= max_tokens:
return messages
if strategy == "recent_first":
# Keep most recent messages, drop oldest first
pruned = list(messages)
while count_message_tokens(pruned) > max_tokens and len(pruned) > 1:
# Remove oldest non-system message
for i, msg in enumerate(pruned):
if msg["role"] != "system":
pruned.pop(i)
break
elif strategy == "summarize_old":
# Summarize old messages, keep recent ones
system_msg = messages[0]
old_messages = messages[1:-3]
recent_messages = messages[-3:]
summary = summarize_conversation(old_messages)
pruned = [
system_msg,
{"role": "system", "content": f"Summary of earlier conversation: {summary}"},
*recent_messages,
]
elif strategy == "truncate_by_tokens":
# Remove messages from the middle
system_msg = messages[0]
recent = messages[-5:]
pruned = [system_msg] + recent
else:
pruned = messages
return pruned
def summarize_conversation(messages: list[dict]) -> str:
"""Summarize a list of messages into a compact summary."""
# Use a cheap model for summarization
conversation_text = "\n".join(f"{m['role']}: {m['content'][:200]}" for m in messages)
return f"Conversation covered: {conversation_text[:500]}..."
6. Cache Hit Optimization
class CacheOptimizer:
"""Optimize prompt sequences for maximum cache hits."""
def __init__(self):
self._cache_prefix: str | None = None
self._cache_prefix_tokens: int = 0
def set_stable_prefix(self, system_prompt: str, format_rules: str) -> None:
"""Set the stable prefix that will be cached across calls."""
self._cache_prefix = f"{system_prompt}\n\n{format_rules}"
self._cache_prefix_tokens = count_tokens(self._cache_prefix)
def build_cached_call(self, variable_content: str) -> list[dict]:
"""Build a message list that maximizes cache hits."""
if not self._cache_prefix:
raise ValueError("Set stable prefix first")
return [
{"role": "system", "content": self._cache_prefix},
{"role": "user", "content": variable_content},
]
def cache_efficiency(self, total_calls: int, cache_hits: int) -> float:
return cache_hits / total_calls if total_calls > 0 else 0.0
def report_savings(
self,
total_calls: int,
variable_tokens_per_call: list[int],
model: str,
) -> dict:
"""Report how much caching saves."""
without_cache = sum(
estimate_call_cost(self._cache_prefix_tokens + v, 1000, model)
for v in variable_tokens_per_call
)
with_cache = sum(
estimate_call_cost(
self._cache_prefix_tokens + v, 1000, model,
cache_read=self._cache_prefix_tokens,
)
for v in variable_tokens_per_call
)
return {
"without_cache": f"${without_cache:.4f}",
"with_cache": f"${with_cache:.4f}",
"savings": f"${without_cache - with_cache:.4f}",
"savings_pct": f"{(1 - with_cache / without_cache) * 100:.1f}%",
}
7. Multi-Session Token Management
class MultiSessionBudget:
"""Manage token budgets across concurrent sessions."""
def __init__(self, global_max_tokens: int, max_sessions: int = 5):
self.global_max = global_max_tokens
self.max_sessions = max_sessions
self.sessions: dict[str, TokenBudget] = {}
self.per_session_budget = global_max_tokens // max_sessions
def create_session(self, session_id: str) -> TokenBudget:
if len(self.sessions) >= self.max_sessions:
raise RuntimeError(f"Max sessions ({self.max_sessions}) reached")
budget = TokenBudget(
total_window=self.per_session_budget,
system_prompt=300,
reserved_for_output=2000,
)
self.sessions[session_id] = budget
return budget
def close_session(self, session_id: str) -> None:
self.sessions.pop(session_id, None)
def global_usage(self) -> dict:
total_used = sum(
b.system_prompt + b.conversation_history + b.retrieval_context
for b in self.sessions.values()
)
return {
"total_budget": self.global_max,
"total_used": total_used,
"total_available": self.global_max - total_used,
"active_sessions": len(self.sessions),
"utilization_pct": (total_used / self.global_max) * 100,
}
Anti-Patterns
- No budget planning — sending arbitrarily large context and hitting token limits mid-conversation
- Ignoring cache costs — not designing prompts for cache reuse wastes 70-90% of potential savings
- Over-reserving output tokens — setting max_tokens=8192 when output is usually 500 tokens
- Not counting tokens per message — blind to actual usage; always count before sending
- Pruning system prompts — system prompts should be stable; prune conversation history instead
- Single-session thinking — in multi-agent systems, each agent needs its own budget
Best Practices
- Budget before building — calculate system + retrieval + history + output before any LLM call.
- Slim system prompts to <500 tokens — for efficient caching and lower per-call cost.
- Count tokens per message — use tiktoken or model-specific tokenizers before every call.
- Estimate costs before execution — especially for batch workflows and multi-agent pipelines.
- Prune aggressively at 80% capacity — summarize old messages, drop middle content, keep recent.
- Design for cache hits — stable system prefix + variable content at the end.
- Manage per-session budgets — in multi-agent systems, divide global budget across sessions.
- Use cheap models for counting/summarizing — don't waste Sonnet/Opus tokens on meta-tasks.
- Log token usage per call — track input, output, cache_read, cache_create for every invocation.
- Set conservative output reserves — estimate expected output length + 20% buffer.
Related Skills
claude-api— Token counting and cost estimation specific to Claudeprompt-optimizer— Prompt compression to fit within budgetsagentic-engineering— Context window management across agent delegationscontext-prune— Session-level context pruning and layer persistenceiterative-retrieval— Progressive context loading to stay within budget