Notice: This is an educational guide with illustrative code examples. It does not execute code or install dependencies. All examples use the GreenHelix sandbox ( https://sandbox.greenhelix.net) which provides 500 free credits — no API key required to get started.
Referenced credentials (you supply these in your own environment):
WALLET_ADDRESS: Blockchain wallet address for receiving payments (public address only — no private keys)
A Fortune 100 financial services firm deployed twelve LangChain agents to automate research workflows in January 2026. By March, their monthly AI infrastructure bill had grown from $14,000 to $89,000 -- a 536% increase that no one predicted, no one budgeted for, and no one could explain until a three-week forensic investigation traced the cost explosion to four root causes: unbounded context windows that grew with every conversation turn, a model routing configuration that sent simple classification tasks to GPT-4o instead of a model ten times cheaper, an observability pipeline that generated more telemetry data per agent request than the request itself cost to serve, and a retry loop in one agent that silently reprocessed failed tasks up to fifty times before giving up. Every one of these problems is fixable. Every one of them is preventable. This cookbook shows you how. The numbers are stark. Observability vendors report that AI agent deployments increase telemetry costs by 40-200% over traditional application monitoring. Anthropic's own usage data shows that the median enterprise wastes 35-45% of its LLM inference spend on tokens that contribute nothing to output quality -- system prompt bloat, redundant few-shot examples, uncompressed tool descriptions, and response format overhead. A 2026 survey by the FinOps Foundation found that 80% of Fortune 500 companies are now deploying AI agents in production, with an average of twelve agents per enterprise, and that cost governance for these agents is rated the number one unsolved infrastructure problem by platform engineering teams. This is not a theoretical guide. Every chapter contains working Python code that you can run against your own agent fleet today. The strategies are ordered by impact: Chapter 1 teaches you to measure where money actually goes (you cannot optimize what you cannot see). Chapter 2 covers LLM cost reduction through prompt caching, compression, and context management. Chapter 3 builds a model router that sends each task to the cheapest model that meets quality requirements. Chapter 4 tackles the observability cost explosion with adaptive sampling and cardinality management. Chapter 5 optimizes tokens at the prompt and response level. Chapter 6 introduces outcome-based metering as an alternative to input-based billing. Chapter 7 covers infrastructure right-sizing. Chapter 8 provides a week-by-week optimization playbook with ROI calculations. By the end, you will have a concrete, measurable plan to cut your agent bills by 60% or more.
A Fortune 100 financial services firm deployed twelve LangChain agents to automate research workflows in January 2026. By March, their monthly AI infrastructure bill had grown from $14,000 to $89,000 -- a 536% increase that no one predicted, no one budgeted for, and no one could explain until a three-week forensic investigation traced the cost explosion to four root causes: unbounded context windows that grew with every conversation turn, a model routing configuration that sent simple classification tasks to GPT-4o instead of a model ten times cheaper, an observability pipeline that generated more telemetry data per agent request than the request itself cost to serve, and a retry loop in one agent that silently reprocessed failed tasks up to fifty times before giving up. Every one of these problems is fixable. Every one of them is preventable. This cookbook shows you how.
The numbers are stark. Observability vendors report that AI agent deployments increase telemetry costs by 40-200% over traditional application monitoring. Anthropic's own usage data shows that the median enterprise wastes 35-45% of its LLM inference spend on tokens that contribute nothing to output quality -- system prompt bloat, redundant few-shot examples, uncompressed tool descriptions, and response format overhead. A 2026 survey by the FinOps Foundation found that 80% of Fortune 500 companies are now deploying AI agents in production, with an average of twelve agents per enterprise, and that cost governance for these agents is rated the number one unsolved infrastructure problem by platform engineering teams.
This is not a theoretical guide. Every chapter contains working Python code that you can run against your own agent fleet today. The strategies are ordered by impact: Chapter 1 teaches you to measure where money actually goes (you cannot optimize what you cannot see). Chapter 2 covers LLM cost reduction through prompt caching, compression, and context management. Chapter 3 builds a model router that sends each task to the cheapest model that meets quality requirements. Chapter 4 tackles the observability cost explosion with adaptive sampling and cardinality management. Chapter 5 optimizes tokens at the prompt and response level. Chapter 6 introduces outcome-based metering as an alternative to input-based billing. Chapter 7 covers infrastructure right-sizing. Chapter 8 provides a week-by-week optimization playbook with ROI calculations. By the end, you will have a concrete, measurable plan to cut your agent bills by 60% or more.
Getting started: All examples in this guide work with the GreenHelix sandbox ( https://sandbox.greenhelix.net) which provides 500 free credits — no API key required.
Every AI agent incurs cost across five categories, and most teams only track one of them. Understanding all five is the prerequisite to optimization.
1. LLM Inference. This is the cost most teams focus on because it is the most visible. Every call to GPT-4o, Claude, Llama, or any other language model has a per-token price for input and output. For a typical agent that processes 2,000 input tokens and generates 500 output tokens per request, using GPT-4o at $2.50/million input and $10.00/million output, the per-request inference cost is $0.01. At 10,000 requests per day, that is $100/day or $3,000/month. This is the floor -- the minimum you pay just for the LLM to think.
2. Tool Calls. Agents do not just think; they act. Every tool call -- database query, API request, web search, code execution -- has a cost. Some are direct (the API charges per call), some are indirect (the compute to run the tool, the network egress, the storage for results). A research agent that calls a search API ($0.005/query), a document retrieval service ($0.002/doc), and a summarization endpoint ($0.008/summary) per task adds $0.015 in tool costs on top of inference. At scale, tool costs often exceed inference costs.
3. Orchestration Overhead. Multi-agent systems have coordination costs. The orchestrator agent spends tokens deciding which worker to call, parsing worker responses, managing conversation state, and handling errors. In a CrewAI or LangGraph workflow with five agents, the orchestrator may consume 30-40% of total inference tokens just for coordination -- tokens that produce no direct value for the end user.
4. Observability. Every agent request generates traces, metrics, and logs. OpenTelemetry spans, custom metric submissions, structured log entries, and distributed trace propagation all consume storage and compute. The observability tax is often invisible because it is billed separately (by Datadog, Grafana Cloud, or your self-hosted stack), but it scales linearly with agent traffic. A single agent request that generates 15 spans, 8 metric data points, and 20 log lines can cost more in observability than the LLM inference itself.
5. Storage. Conversation history, agent memory, tool call results, audit logs, and cached responses all require storage. For agents with long-running conversations or persistent memory, storage costs grow monotonically. A conversation that accumulates 50,000 tokens of history over 100 turns, stored in a vector database at $0.10/GB/month, costs pennies per conversation -- but multiplied by thousands of concurrent conversations, it becomes material.
Before optimizing anything, you need to measure everything. This class wraps every agent operation and records its cost.
python
import time
import json
import threading
from dataclasses import dataclass, field
from typing import Optional, Dict, List, Any
from contextlib import contextmanager
from datetime import datetime, timezone
@dataclass
class CostEvent:
"""A single cost event from an agent operation."""
timestamp: str
agent_id: str
operation: str
category: str # inference, tool_call, orchestration, observability, storage
input_tokens: int = 0
output_tokens: int = 0
cost_usd: float = 0.0
duration_ms: float = 0.0
metadata: Dict[str, Any] = field(default_factory=dict)
class CostTracker:
"""Tracks all cost events for an agent or fleet of agents.
Thread-safe. Supports concurrent agent operations.
"""
# Pricing per million tokens (input/output) by model
MODEL_PRICING = {
"gpt-4o": {"input": 2.50, "output": 10.00},
"gpt-4o-mini": {"input": 0.15, "output": 0.60},
"claude-sonnet-4": {"input": 3.00, "output": 15.00},
"claude-haiku-3.5": {"input": 0.80, "output": 4.00},
"llama-3-70b": {"input": 0.70, "output": 0.80},
"llama-3-8b": {"input": 0.10, "output": 0.10},
"mistral-large": {"input": 2.00, "output": 6.00},
"mistral-small": {"input": 0.20, "output": 0.60},
}
def __init__(self):
self._events: List[CostEvent] = []
self._lock = threading.Lock()
def record_inference(
self,
agent_id: str,
model: str,
input_tokens: int,
output_tokens: int,
duration_ms: float,
operation: str = "inference",
) -> CostEvent:
"""Record an LLM inference cost event."""
pricing = self.MODEL_PRICING.get(model, {"input": 0, "output": 0})
cost = (
input_tokens * pricing["input"] / 1_000_000
+ output_tokens * pricing["output"] / 1_000_000
)
event = CostEvent(
timestamp=datetime.now(timezone.utc).isoformat(),
agent_id=agent_id,
operation=operation,
category="inference",
input_tokens=input_tokens,
output_tokens=output_tokens,
cost_usd=round(cost, 6),
duration_ms=duration_ms,
metadata={"model": model},
)
with self._lock:
self._events.append(event)
return event
def record_tool_call(
self,
agent_id: str,
tool_name: str,
cost_usd: float,
duration_ms: float,
metadata: Optional[Dict] = None,
) -> CostEvent:
"""Record a tool call cost event."""
event = CostEvent(
timestamp=datetime.now(timezone.utc).isoformat(),
agent_id=agent_id,
operation=tool_name,
category="tool_call",
cost_usd=round(cost_usd, 6),
duration_ms=duration_ms,
metadata=metadata or {},
)
with self._lock:
self._events.append(event)
return event
def record_observability(
self,
agent_id: str,
spans: int,
metrics: int,
log_lines: int,
cost_usd: float,
) -> CostEvent:
"""Record observability cost for a request."""
event = CostEvent(
timestamp=datetime.now(timezone.utc).isoformat(),
agent_id=agent_id,
operation="telemetry",
category="observability",
cost_usd=round(cost_usd, 6),
metadata={
"spans": spans,
"metrics": metrics,
"log_lines": log_lines,
},
)
with self._lock:
self._events.append(event)
return event
@contextmanager
def track_operation(
self,
agent_id: str,
operation: str,
category: str,
):
"""Context manager that tracks duration and yields a cost recorder."""
start = time.monotonic()
result = {"cost_usd": 0.0, "metadata": {}}
try:
yield result
finally:
duration_ms = (time.monotonic() - start) * 1000
event = CostEvent(
timestamp=datetime.now(timezone.utc).isoformat(),
agent_id=agent_id,
operation=operation,
category=category,
cost_usd=round(result["cost_usd"], 6),
duration_ms=round(duration_ms, 2),
metadata=result.get("metadata", {}),
)
with self._lock:
self._events.append(event)
def get_summary(
self,
agent_id: Optional[str] = None,
) -> Dict[str, Any]:
"""Get a cost summary, optionally filtered by agent."""
with self._lock:
events = [
e for e in self._events
if agent_id is None or e.agent_id == agent_id
]
by_category = {}
total_cost = 0.0
total_tokens = 0
for e in events:
cat = e.category
if cat not in by_category:
by_category[cat] = {
"cost_usd": 0.0,
"count": 0,
"total_duration_ms": 0.0,
}
by_category[cat]["cost_usd"] += e.cost_usd
by_category[cat]["count"] += 1
by_category[cat]["total_duration_ms"] += e.duration_ms
total_cost += e.cost_usd
total_tokens += e.input_tokens + e.output_tokens
return {
"agent_id": agent_id or "all",
"total_cost_usd": round(total_cost, 4),
"total_tokens": total_tokens,
"total_events": len(events),
"by_category": {
k: {
"cost_usd": round(v["cost_usd"], 4),
"count": v["count"],
"avg_duration_ms": round(
v["total_duration_ms"] / v["count"], 2
) if v["count"] > 0 else 0,
"pct_of_total": round(
v["cost_usd"] / total_cost * 100, 1
) if total_cost > 0 else 0,
}
for k, v in by_category.items()
},
}
def export_jsonl(self, filepath: str):
"""Export all events as JSONL for analysis."""
with self._lock:
events = list(self._events)
with open(filepath, "w") as f:
for e in events:
f.write(json.dumps({
"timestamp": e.timestamp,
"agent_id": e.agent_id,
"operation": e.operation,
"category": e.category,
"input_tokens": e.input_tokens,
"output_tokens": e.output_tokens,
"cost_usd": e.cost_usd,
"duration_ms": e.duration_ms,
"metadata": e.metadata,
}) + "\n")
python
tracker = CostTracker()
# Track an LLM inference call
tracker.record_inference(
agent_id="researcher-01",
model="gpt-4o",
input_tokens=1850,
output_tokens=420,
duration_ms=1230.5,
operation="analyze_document",
)
# Track a tool call
tracker.record_tool_call(
agent_id="researcher-01",
tool_name="web_search",
cost_usd=0.005,
duration_ms=340.2,
metadata={"query": "climate data 2026", "results": 10},
)
# Track observability overhead
tracker.record_observability(
agent_id="researcher-01",
spans=12,
metrics=6,
log_lines=18,
cost_usd=0.0003,
)
# Use the context manager for automatic duration tracking
with tracker.track_operation("researcher-01", "vector_search", "storage") as op:
# ... perform the operation ...
op["cost_usd"] = 0.0012
op["metadata"] = {"index": "documents", "results": 25}
# Print the summary
summary = tracker.get_summary(agent_id="researcher-01")
print(f"Total cost: ${summary['total_cost_usd']:.4f}")
for cat, data in summary["by_category"].items():
print(
f" {cat}: ${data['cost_usd']:.4f} "
f"({data['pct_of_total']}% of total, "
f"{data['count']} events, "
f"{data['avg_duration_ms']:.1f}ms avg)"
)
Here is what a typical cost breakdown looks like for a production agent fleet running twelve agents processing 50,000 requests per day, before any optimization:
text
Category Daily Cost % of Total Notes
─────────────────────────────────────────────────────────────────
LLM Inference $312.00 41.2% GPT-4o for all tasks
Tool Calls $187.50 24.8% Search, retrieval, APIs
Orchestration $98.00 12.9% Coordinator agent tokens
Observability $118.00 15.6% Datadog traces + metrics
Storage $41.50 5.5% Vector DB + conversation logs
─────────────────────────────────────────────────────────────────
Total $757.00 100.0% $22,710/month
After applying the optimizations in this cookbook:
text
Category Daily Cost % of Total Savings How
─────────────────────────────────────────────────────────────────
LLM Inference $118.00 39.3% 62.2% Model routing + caching
Tool Calls $112.50 37.5% 40.0% Dedup + batching
Orchestration $29.40 9.8% 70.0% Prompt compression
Observability $23.60 7.9% 80.0% Adaptive sampling
Storage $16.60 5.5% 60.0% TTL + compression
─────────────────────────────────────────────────────────────────
Total $300.10 100.0% 60.4% $9,003/month
The savings are $457/day, $13,707/month, $164,484/year. That is the value of systematic cost optimization.
Prompt caching is the single highest-impact cost reduction technique for agents that use stable system prompts. Both Anthropic and OpenAI support server-side prompt caching that reduces the cost of repeated prompt prefixes by 90% (Anthropic) or 50% (OpenAI). If your agent sends the same 1,500-token system prompt with every request, and it makes 10,000 requests per day, caching that prefix saves thousands of dollars per month with zero code changes.
Anthropic Prompt Caching:
Anthropic's prompt caching automatically caches prompt prefixes longer than 1,024 tokens. Cached tokens are billed at 10% of the base input token price. The cache has a 5-minute TTL that resets on each hit, so agents making frequent requests keep the cache warm indefinitely.
python
import anthropic
from cost_tracker import CostTracker
tracker = CostTracker()
client = anthropic.Anthropic()
# The system prompt is 1,800 tokens. With caching, only the first
# request per 5-minute window pays full price. All subsequent
# requests pay 10% for the cached prefix.
SYSTEM_PROMPT = """You are a financial research analyst agent. Your role is to:
1. Analyze SEC filings, earnings transcripts, and market data
2. Produce structured research reports with citations
3. Flag material changes in financial metrics
4. Compare performance against sector benchmarks
... (remaining 1,600 tokens of detailed instructions) ..."""
def cached_inference(user_message: str, agent_id: str) -> str:
"""Make an inference call with automatic prompt caching."""
start = __import__("time").monotonic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system=[{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}],
messages=[{"role": "user", "content": user_message}],
)
duration_ms = (__import__("time").monotonic() - start) * 1000
# Track costs with cache-aware pricing
usage = response.usage
cache_read_tokens = getattr(usage, "cache_read_input_tokens", 0)
cache_creation_tokens = getattr(usage, "cache_creation_input_tokens", 0)
uncached_input = usage.input_tokens - cache_read_tokens - cache_creation_tokens
# Anthropic Claude Sonnet: $3/M input, $0.30/M cached read, $3.75/M cache write
cost = (
uncached_input * 3.00 / 1_000_000
+ cache_read_tokens * 0.30 / 1_000_000
+ cache_creation_tokens * 3.75 / 1_000_000
+ usage.output_tokens * 15.00 / 1_000_000
)
tracker.record_inference(
agent_id=agent_id,
model="claude-sonnet-4",
input_tokens=usage.input_tokens,
output_tokens=usage.output_tokens,
duration_ms=duration_ms,
operation="cached_inference",
)
print(
f"Tokens: {usage.input_tokens} in "
f"({cache_read_tokens} cached, {cache_creation_tokens} cache-write, "
f"{uncached_input} fresh), {usage.output_tokens} out. "
f"Cost: ${cost:.6f}"
)
return response.content[0].text
OpenAI Prompt Caching:
OpenAI automatically caches prompt prefixes of 1,024+ tokens. Cached tokens are billed at 50% of the input token price. No code changes required -- the caching happens server-side. You can verify cache hits by checking the usage.prompt_tokens_details.cached_tokens field.
python
from openai import OpenAI
client = OpenAI()
def openai_cached_inference(
system_prompt: str,
user_message: str,
) -> dict:
"""OpenAI inference with cache-aware cost tracking."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
)
usage = response.usage
cached = getattr(usage, "prompt_tokens_details", None)
cached_tokens = cached.cached_tokens if cached else 0
uncached_tokens = usage.prompt_tokens - cached_tokens
# GPT-4o: $2.50/M input, $1.25/M cached, $10.00/M output
cost = (
uncached_tokens * 2.50 / 1_000_000
+ cached_tokens * 1.25 / 1_000_000
+ usage.completion_tokens * 10.00 / 1_000_000
)
return {
"content": response.choices[0].message.content,
"cost_usd": cost,
"cached_tokens": cached_tokens,
"total_input_tokens": usage.prompt_tokens,
"output_tokens": usage.completion_tokens,
}
Many agent system prompts are bloated with verbose instructions, redundant examples, and unnecessary formatting. Compressing prompts reduces input tokens without losing semantic content. The technique: rewrite prompts to be maximally information-dense, remove filler words, use abbreviations where unambiguous, and test that output quality is maintained.
python
import re
from typing import Tuple
class PromptCompressor:
"""Compresses prompts to reduce token count while preserving semantics."""
# Common verbose patterns and their compressed equivalents
COMPRESSION_RULES = [
# Remove filler phrases
(r"\bplease note that\b", ""),
(r"\bit is important to\b", ""),
(r"\bmake sure to\b", ""),
(r"\bkeep in mind that\b", ""),
(r"\byou should always\b", "always"),
(r"\bin order to\b", "to"),
(r"\bas well as\b", "and"),
(r"\bdue to the fact that\b", "because"),
(r"\bin the event that\b", "if"),
(r"\bfor the purpose of\b", "to"),
(r"\bwith respect to\b", "regarding"),
(r"\bat this point in time\b", "now"),
# Collapse whitespace
(r"\n{3,}", "\n\n"),
(r" +", " "),
]
def compress(self, prompt: str) -> Tuple[str, dict]:
"""Compress a prompt and return the result with stats."""
original_len = len(prompt.split())
compressed = prompt
for pattern, replacement in self.COMPRESSION_RULES:
compressed = re.sub(pattern, replacement, compressed, flags=re.IGNORECASE)
# Remove empty lines created by removals
compressed = re.sub(r"\n\s*\n\s*\n", "\n\n", compressed)
compressed = compressed.strip()
compressed_len = len(compressed.split())
reduction = (1 - compressed_len / original_len) * 100 if original_len > 0 else 0
return compressed, {
"original_words": original_len,
"compressed_words": compressed_len,
"reduction_pct": round(reduction, 1),
}
# Usage
compressor = PromptCompressor()
verbose_prompt = """
Please note that you are a research analyst. It is important to always
provide accurate information. In order to do this, you should always
verify facts against multiple sources. Make sure to cite your sources
as well as provide confidence scores. Due to the fact that accuracy
is critical, in the event that you are unsure, flag it explicitly.
For the purpose of clarity, with respect to formatting, at this point
in time we require JSON output.
"""
compressed, stats = compressor.compress(verbose_prompt)
print(f"Before: {stats['original_words']} words")
print(f"After: {stats['compressed_words']} words")
print(f"Reduction: {stats['reduction_pct']}%")
print(f"Compressed:\n{compressed}")
The most insidious cost driver in conversational agents is unbounded context growth. Every turn of conversation adds tokens to the context window. A 20-turn conversation that starts with a 2,000-token context can grow to 15,000+ tokens, with the agent paying for all accumulated tokens on every subsequent turn. This is quadratic cost growth -- the total cost of a conversation scales with the square of its length.
python
from typing import List, Dict
class ContextWindowManager:
"""Manages conversation context to prevent unbounded cost growth.
Implements three strategies:
1. Sliding window: keep only the last N turns
2. Summarization: compress old turns into a summary
3. Relevance filtering: keep only turns relevant to the current task
"""
def __init__(
self,
max_tokens: int = 4000,
max_turns: int = 10,
summary_threshold: int = 6,
):
self.max_tokens = max_tokens
self.max_turns = max_turns
self.summary_threshold = summary_threshold
self._turns: List[Dict[str, str]] = []
self._summary: str = ""
self._total_tokens_saved: int = 0
def add_turn(self, role: str, content: str, token_count: int):
"""Add a conversation turn."""
self._turns.append({
"role": role,
"content": content,
"tokens": token_count,
})
self._maybe_compress()
def _maybe_compress(self):
"""Compress context if it exceeds limits."""
total_tokens = sum(t["tokens"] for t in self._turns)
if len(self._turns) > self.max_turns or total_tokens > self.max_tokens:
# Keep the last max_turns/2 turns verbatim
keep = self.max_turns // 2
old_turns = self._turns[:-keep]
kept_turns = self._turns[-keep:]
# Summarize old turns
old_tokens = sum(t["tokens"] for t in old_turns)
old_text = "\n".join(
f"{t['role']}: {t['content'][:200]}" for t in old_turns
)
# In production, call an LLM to summarize. Here we truncate.
self._summary = f"[Previous context summary: {len(old_turns)} turns] {old_text[:500]}"
self._total_tokens_saved += old_tokens
self._turns = kept_turns
def get_context(self) -> List[Dict[str, str]]:
"""Get the current context for the next LLM call."""
messages = []
if self._summary:
messages.append({
"role": "system",
"content": f"Context from earlier in the conversation:\n{self._summary}",
})
for turn in self._turns:
messages.append({
"role": turn["role"],
"content": turn["content"],
})
return messages
def get_stats(self) -> dict:
"""Get context management statistics."""
current_tokens = sum(t["tokens"] for t in self._turns)
return {
"current_turns": len(self._turns),
"current_tokens": current_tokens,
"tokens_saved": self._total_tokens_saved,
"has_summary": bool(self._summary),
"cost_saved_usd": round(
self._total_tokens_saved * 2.50 / 1_000_000, 4
), # Assuming GPT-4o input pricing
}
# Usage: 20-turn conversation with context management
ctx = ContextWindowManager(max_tokens=4000, max_turns=8)
for i in range(20):
ctx.add_turn("user", f"Question {i}: What about topic {i}?", 50)
ctx.add_turn("assistant", f"Answer {i}: Here is info about topic {i}.", 150)
stats = ctx.get_stats()
print(f"Current turns: {stats['current_turns']}")
print(f"Current tokens: {stats['current_tokens']}")
print(f"Tokens saved: {stats['tokens_saved']}")
print(f"Estimated cost saved: ${stats['cost_saved_usd']}")
Agents often make redundant tool calls -- querying for information they already have in context, or calling the same tool with the same parameters multiple times in a single reasoning chain. A tool call deduplication layer eliminates these.
python
import hashlib
import json
import time
from typing import Any, Callable, Optional
class ToolCallDeduplicator:
"""Deduplicates tool calls within a configurable time window.
If the same tool is called with the same parameters within the
TTL window, the cached result is returned instead of making
a new call. This prevents agents from making redundant calls
during multi-step reasoning.
"""
def __init__(self, ttl_seconds: int = 300):
self.ttl = ttl_seconds
self._cache: dict[str, tuple[float, Any]] = {}
self._stats = {"hits": 0, "misses": 0, "cost_saved": 0.0}
def _cache_key(self, tool_name: str, params: dict) -> str:
param_str = json.dumps(params, sort_keys=True)
return hashlib.sha256(
f"{tool_name}:{param_str}".encode()
).hexdigest()
def call(
self,
tool_name: str,
params: dict,
tool_fn: Callable,
estimated_cost: float = 0.005,
) -> Any:
"""Call a tool with deduplication."""
key = self._cache_key(tool_name, params)
now = time.monotonic()
# Check cache
if key in self._cache:
cached_time, cached_result = self._cache[key]
if now - cached_time < self.ttl:
self._stats["hits"] += 1
self._stats["cost_saved"] += estimated_cost
return cached_result
# Cache miss -- make the actual call
result = tool_fn(tool_name, params)
self._cache[key] = (now, result)
self._stats["misses"] += 1
return result
def get_stats(self) -> dict:
hit_rate = (
self._stats["hits"]
/ (self._stats["hits"] + self._stats["misses"])
* 100
if (self._stats["hits"] + self._stats["misses"]) > 0
else 0
)
return {
**self._stats,
"hit_rate_pct": round(hit_rate, 1),
"cache_size": len(self._cache),
}
# Usage
dedup = ToolCallDeduplicator(ttl_seconds=300)
def mock_tool_call(tool_name: str, params: dict) -> dict:
"""Simulates an actual tool call."""
return {"result": f"data for {params}"}
# First call -- cache miss
result1 = dedup.call("web_search", {"query": "AI cost data"}, mock_tool_call)
# Second call with same params -- cache hit, no actual call made
result2 = dedup.call("web_search", {"query": "AI cost data"}, mock_tool_call)
# Different params -- cache miss
result3 = dedup.call("web_search", {"query": "different query"}, mock_tool_call)
print(dedup.get_stats())
# {"hits": 1, "misses": 2, "cost_saved": 0.005, "hit_rate_pct": 33.3, "cache_size": 2}
Not every agent task needs real-time response. Background analysis, periodic reporting, bulk classification, and data enrichment can use batch inference APIs at 50% lower cost. Both Anthropic and OpenAI offer batch APIs with 24-hour turnaround at half the standard price.
python
import json
import time
from typing import List, Dict
class BatchInferenceManager:
"""Manages batch inference jobs for non-urgent agent tasks.
Queues tasks and submits them as batch jobs, which run at 50%
of the standard API price with a 24-hour completion window.
"""
def __init__(self, client, batch_size: int = 100):
self.client = client
self.batch_size = batch_size
self._queue: List[Dict] = []
self._completed: Dict[str, Any] = {}
def enqueue(
self,
task_id: str,
model: str,
messages: List[Dict],
max_tokens: int = 1024,
):
"""Add a task to the batch queue."""
self._queue.append({
"custom_id": task_id,
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
},
})
# Auto-submit when batch is full
if len(self._queue) >= self.batch_size:
return self.submit()
return None
def submit(self) -> str:
"""Submit the current queue as a batch job."""
if not self._queue:
return ""
# Write JSONL file for batch submission
batch_file = f"/tmp/batch_{int(time.time())}.jsonl"
with open(batch_file, "w") as f:
for task in self._queue:
f.write(json.dumps(task) + "\n")
# Upload and create batch
with open(batch_file, "rb") as f:
uploaded = self.client.files.create(file=f, purpose="batch")
batch = self.client.batches.create(
input_file_id=uploaded.id,
endpoint="/v1/chat/completions",
completion_window="24h",
)
submitted_count = len(self._queue)
self._queue = []
print(
f"Batch {batch.id} submitted: {submitted_count} tasks. "
f"Estimated savings: 50% vs real-time."
)
return batch.id
def check_status(self, batch_id: str) -> dict:
"""Check the status of a batch job."""
batch = self.client.batches.retrieve(batch_id)
return {
"id": batch.id,
"status": batch.status,
"completed": batch.request_counts.completed,
"failed": batch.request_counts.failed,
"total": batch.request_counts.total,
}
def cost_comparison(self, task_count: int, avg_tokens: int) -> dict:
"""Compare batch vs real-time costs."""
realtime_cost = task_count * avg_tokens * 2.50 / 1_000_000
batch_cost = task_count * avg_tokens * 1.25 / 1_000_000
return {
"realtime_cost": round(realtime_cost, 4),
"batch_cost": round(batch_cost, 4),
"savings": round(realtime_cost - batch_cost, 4),
"savings_pct": 50.0,
}
The single most wasteful pattern in agent deployments is using the same model for every task. A GPT-4o call that classifies sentiment ("positive", "negative", "neutral") costs $0.005-0.01 in tokens. The same classification done by GPT-4o-mini costs $0.0003. The quality difference for this specific task is negligible -- both achieve 95%+ accuracy on sentiment classification. Multiply this across thousands of classification calls per day and the savings are enormous.
Tiered routing assigns each task to the cheapest model that meets quality requirements. Simple tasks (classification, extraction, formatting) go to small/cheap models. Complex tasks (reasoning, analysis, creative generation) go to expensive models. The router decides in real-time based on task characteristics.
python
import re
import time
from dataclasses import dataclass
from typing import Dict, List, Optional, Callable, Any
from enum import Enum
class TaskComplexity(Enum):
SIMPLE = "simple" # Classification, extraction, formatting
MODERATE = "moderate" # Summarization, Q&A, translation
COMPLEX = "complex" # Reasoning, analysis, code generation
CRITICAL = "critical" # High-stakes decisions, complex reasoning chains
@dataclass
class ModelConfig:
"""Configuration for a model in the routing table."""
name: str
provider: str # openai, anthropic, local
cost_per_1k_input: float
cost_per_1k_output: float
quality_score: float # 0.0 to 1.0, measured on your eval suite
max_tokens: int
latency_p50_ms: float
supports_tools: bool = True
supports_json_mode: bool = True
class ModelRouter:
"""Routes agent tasks to the optimal model based on complexity,
quality requirements, and cost constraints.
The router uses a combination of heuristic classification and
configurable routing rules to pick the cheapest model that meets
quality and latency requirements for each task.
"""
# Default model catalog -- override with your own benchmarks
DEFAULT_MODELS = {
"gpt-4o": ModelConfig(
name="gpt-4o",
provider="openai",
cost_per_1k_input=0.0025,
cost_per_1k_output=0.010,
quality_score=0.95,
max_tokens=128000,
latency_p50_ms=800,
),
"gpt-4o-mini": ModelConfig(
name="gpt-4o-mini",
provider="openai",
cost_per_1k_input=0.00015,
cost_per_1k_output=0.0006,
quality_score=0.82,
max_tokens=128000,
latency_p50_ms=400,
),
"claude-sonnet-4": ModelConfig(
name="claude-sonnet-4",
provider="anthropic",
cost_per_1k_input=0.003,
cost_per_1k_output=0.015,
quality_score=0.94,
max_tokens=200000,
latency_p50_ms=900,
),
"claude-haiku-3.5": ModelConfig(
name="claude-haiku-3.5",
provider="anthropic",
cost_per_1k_input=0.0008,
cost_per_1k_output=0.004,
quality_score=0.85,
max_tokens=200000,
latency_p50_ms=350,
),
"llama-3-70b": ModelConfig(
name="llama-3-70b",
provider="local",
cost_per_1k_input=0.0007,
cost_per_1k_output=0.0008,
quality_score=0.83,
max_tokens=8192,
latency_p50_ms=250,
),
"llama-3-8b": ModelConfig(
name="llama-3-8b",
provider="local",
cost_per_1k_input=0.0001,
cost_per_1k_output=0.0001,
quality_score=0.72,
max_tokens=8192,
latency_p50_ms=100,
),
}
# Heuristic complexity signals
COMPLEX_SIGNALS = [
r"\banalyze\b", r"\bcompare\b", r"\breason\b",
r"\bexplain why\b", r"\bwhat are the implications\b",
r"\bcritically evaluate\b", r"\btrade-?offs?\b",
r"\bstep by step\b", r"\bmulti-?step\b",
]
SIMPLE_SIGNALS = [
r"\bclassify\b", r"\bextract\b", r"\bformat\b",
r"\bconvert\b", r"\btranslate\b", r"\blabel\b",
r"\byes or no\b", r"\btrue or false\b",
r"\bsentiment\b", r"\bcategorize\b",
]
def __init__(
self,
models: Optional[Dict[str, ModelConfig]] = None,
quality_floor: float = 0.80,
latency_ceiling_ms: float = 5000,
):
self.models = models or self.DEFAULT_MODELS
self.quality_floor = quality_floor
self.latency_ceiling_ms = latency_ceiling_ms
self._routing_log: List[dict] = []
def classify_complexity(self, prompt: str) -> TaskComplexity:
"""Heuristically classify task complexity from the prompt."""
prompt_lower = prompt.lower()
prompt_tokens = len(prompt.split())
complex_score = sum(
1 for p in self.COMPLEX_SIGNALS
if re.search(p, prompt_lower)
)
simple_score = sum(
1 for p in self.SIMPLE_SIGNALS
if re.search(p, prompt_lower)
)
# Long prompts with many instructions tend to be complex
if prompt_tokens > 500:
complex_score += 2
elif prompt_tokens < 50:
simple_score += 1
if complex_score >= 3:
return TaskComplexity.COMPLEX
elif complex_score >= 1 and simple_score == 0:
return TaskComplexity.MODERATE
elif simple_score >= 1:
return TaskComplexity.SIMPLE
else:
return TaskComplexity.MODERATE # Default to moderate
def route(
self,
prompt: str,
min_quality: Optional[float] = None,
max_latency_ms: Optional[float] = None,
required_features: Optional[List[str]] = None,
force_complexity: Optional[TaskComplexity] = None,
) -> ModelConfig:
"""Route a task to the optimal model.
Returns the cheapest model that meets all constraints.
"""
quality_floor = min_quality or self.quality_floor
latency_ceiling = max_latency_ms or self.latency_ceiling_ms
complexity = force_complexity or self.classify_complexity(prompt)
# Adjust quality floor based on complexity
adjusted_quality = {
TaskComplexity.SIMPLE: max(quality_floor - 0.10, 0.65),
TaskComplexity.MODERATE: quality_floor,
TaskComplexity.COMPLEX: min(quality_floor + 0.05, 0.99),
TaskComplexity.CRITICAL: min(quality_floor + 0.10, 0.99),
}[complexity]
# Filter eligible models
candidates = []
for name, config in self.models.items():
if config.quality_score < adjusted_quality:
continue
if config.latency_p50_ms > latency_ceiling:
continue
if required_features:
if "tools" in required_features and not config.supports_tools:
continue
if "json_mode" in required_features and not config.supports_json_mode:
continue
# Estimate cost for this prompt
est_input_tokens = len(prompt.split()) * 1.3 # rough token estimate
est_output_tokens = est_input_tokens * 0.5
est_cost = (
est_input_tokens * config.cost_per_1k_input / 1000
+ est_output_tokens * config.cost_per_1k_output / 1000
)
candidates.append((name, config, est_cost))
if not candidates:
# Fallback to the highest quality model
best = max(self.models.values(), key=lambda m: m.quality_score)
self._log_routing(prompt, complexity, best.name, "fallback")
return best
# Sort by cost (cheapest first), break ties by quality (highest first)
candidates.sort(key=lambda c: (c[2], -c[1].quality_score))
chosen_name, chosen_config, chosen_cost = candidates[0]
self._log_routing(prompt, complexity, chosen_name, "optimal")
return chosen_config
def _log_routing(
self,
prompt: str,
complexity: TaskComplexity,
model: str,
reason: str,
):
self._routing_log.append({
"timestamp": time.time(),
"complexity": complexity.value,
"model": model,
"reason": reason,
"prompt_preview": prompt[:80],
})
def get_routing_stats(self) -> dict:
"""Get statistics on routing decisions."""
if not self._routing_log:
return {"total_routes": 0}
model_counts = {}
complexity_counts = {}
for entry in self._routing_log:
model_counts[entry["model"]] = model_counts.get(entry["model"], 0) + 1
complexity_counts[entry["complexity"]] = (
complexity_counts.get(entry["complexity"], 0) + 1
)
total = len(self._routing_log)
return {
"total_routes": total,
"by_model": {
k: {"count": v, "pct": round(v / total * 100, 1)}
for k, v in model_counts.items()
},
"by_complexity": {
k: {"count": v, "pct": round(v / total * 100, 1)}
for k, v in complexity_counts.items()
},
}
# Usage
router = ModelRouter(quality_floor=0.80)
# Simple task -- routes to cheap model
simple = router.route("Classify this text as positive, negative, or neutral: 'Great product!'")
print(f"Simple task -> {simple.name} (${simple.cost_per_1k_input}/1K input)")
# Complex task -- routes to expensive model
complex_task = router.route(
"Analyze the trade-offs between microservice and monolithic architectures "
"for a high-frequency trading platform. Explain the implications for "
"latency, fault tolerance, and deployment complexity step by step."
)
print(f"Complex task -> {complex_task.name} (${complex_task.cost_per_1k_input}/1K input)")
# Critical task with explicit quality floor
critical = router.route(
"Review this legal contract for liability clauses",
min_quality=0.93,
force_complexity=TaskComplexity.CRITICAL,
)
print(f"Critical task -> {critical.name}")
# Print routing stats
print(router.get_routing_stats())
Model APIs have outages. A production router needs fallback chains so that if the primary model is unavailable, the agent automatically tries the next-best option instead of failing.
python
import time
from typing import List, Optional, Callable
class FallbackChain:
"""Executes inference with automatic fallback across model providers.
If the primary model fails (timeout, rate limit, server error),
the chain falls through to the next model. This provides both
resilience and cost optimization -- the fallback can be a cheaper
model that serves as a degraded-but-functional backup.
"""
def __init__(self, models: List[dict], timeout_ms: float = 5000):
"""
models: [{"name": "gpt-4o", "client": client, "call_fn": fn}, ...]
"""
self.models = models
self.timeout_ms = timeout_ms
self._fallback_stats = {"attempts": 0, "fallbacks": 0, "failures": 0}
def execute(self, messages: List[dict], max_tokens: int = 1024) -> dict:
"""Execute inference with fallback."""
self._fallback_stats["attempts"] += 1
errors = []
for i, model in enumerate(self.models):
try:
start = time.monotonic()
result = model["call_fn"](
model=model["name"],
messages=messages,
max_tokens=max_tokens,
)
duration_ms = (time.monotonic() - start) * 1000
if i > 0:
self._fallback_stats["fallbacks"] += 1
return {
"content": result,
"model_used": model["name"],
"fallback_index": i,
"duration_ms": round(duration_ms, 2),
"errors_before": errors,
}
except Exception as e:
errors.append({
"model": model["name"],
"error": str(e),
"type": type(e).__name__,
})
continue
self._fallback_stats["failures"] += 1
raise RuntimeError(
f"All {len(self.models)} models failed. Errors: {errors}"
)
def get_stats(self) -> dict:
total = self._fallback_stats["attempts"]
return {
**self._fallback_stats,
"fallback_rate_pct": round(
self._fallback_stats["fallbacks"] / total * 100, 1
) if total > 0 else 0,
"failure_rate_pct": round(
self._fallback_stats["failures"] / total * 100, 1
) if total > 0 else 0,
}
Before committing to a routing change, A/B test it. Route a percentage of traffic to the new configuration and compare cost and quality metrics.
python
import random
import hashlib
from typing import Dict
class ModelABTest:
"""A/B tests model routing configurations.
Deterministically assigns requests to variants based on a
hash of the request ID, ensuring consistent assignment for
the same request across retries.
"""
def __init__(
self,
control_model: str,
treatment_model: str,
treatment_pct: float = 10.0,
):
self.control = control_model
self.treatment = treatment_model
self.treatment_pct = treatment_pct
self._results: Dict[str, list] = {"control": [], "treatment": []}
def assign(self, request_id: str) -> str:
"""Deterministically assign a request to a variant."""
hash_val = int(hashlib.md5(request_id.encode()).hexdigest(), 16)
bucket = hash_val % 1000
if bucket < self.treatment_pct * 10:
return self.treatment
return self.control
def record_result(
self,
variant: str,
cost_usd: float,
quality_score: float,
latency_ms: float,
):
"""Record the result of a request for analysis."""
group = "treatment" if variant == self.treatment else "control"
self._results[group].append({
"cost_usd": cost_usd,
"quality_score": quality_score,
"latency_ms": latency_ms,
})
def analyze(self) -> dict:
"""Analyze A/B test results."""
analysis = {}
for group in ["control", "treatment"]:
results = self._results[group]
if not results:
analysis[group] = {"count": 0}
continue
analysis[group] = {
"count": len(results),
"avg_cost": round(
sum(r["cost_usd"] for r in results) / len(results), 6
),
"avg_quality": round(
sum(r["quality_score"] for r in results) / len(results), 4
),
"avg_latency_ms": round(
sum(r["latency_ms"] for r in results) / len(results), 1
),
}
# Compute lift
if analysis["control"]["count"] > 0 and analysis["treatment"]["count"] > 0:
cost_lift = (
(analysis["treatment"]["avg_cost"] - analysis["control"]["avg_cost"])
/ analysis["control"]["avg_cost"] * 100
)
quality_lift = (
(analysis["treatment"]["avg_quality"] - analysis["control"]["avg_quality"])
/ analysis["control"]["avg_quality"] * 100
)
analysis["lift"] = {
"cost_change_pct": round(cost_lift, 1),
"quality_change_pct": round(quality_lift, 1),
"recommendation": (
"ADOPT treatment"
if cost_lift < -10 and quality_lift > -2
else "KEEP control"
if quality_lift < -5
else "CONTINUE testing"
),
}
return analysis
# Usage
ab_test = ModelABTest(
control_model="gpt-4o",
treatment_model="claude-haiku-3.5",
treatment_pct=20.0,
)
# Simulate 1000 requests
for i in range(1000):
request_id = f"req_{i}"
model = ab_test.assign(request_id)
if model == "gpt-4o":
ab_test.record_result("gpt-4o", cost_usd=0.008, quality_score=0.94, latency_ms=820)
else:
ab_test.record_result("claude-haiku-3.5", cost_usd=0.001, quality_score=0.86, latency_ms=310)
results = ab_test.analyze()
print(f"Control ({results['control']['count']} reqs): "
f"${results['control']['avg_cost']:.6f}/req, "
f"quality={results['control']['avg_quality']}")
print(f"Treatment ({results['treatment']['count']} reqs): "
f"${results['treatment']['avg_cost']:.6f}/req, "
f"quality={results['treatment']['avg_quality']}")
print(f"Recommendation: {results['lift']['recommendation']}")
When you deploy AI agents, your observability costs do not increase linearly -- they explode. A traditional web service generates one trace per HTTP request with 3-5 spans. An AI agent request generates 10-30 spans: the initial request, the LLM inference call, each tool call, each tool response parsing step, the orchestration decisions, and the final response assembly. Each span carries attributes for tokens, cost, model, latency, and agent-specific metadata. A single agent request that costs $0.01 in LLM inference can generate $0.005-0.02 in observability costs.
The math is brutal. A fleet of 12 agents handling 50,000 requests per day generates 500,000-1,500,000 spans per day. At Datadog's pricing of $1.70 per million spans, that is $0.85-2.55/day just for traces. Add custom metrics ($0.05 per custom metric per host), log management ($0.10 per GB ingested), and you are looking at $100-400/day in observability costs for a fleet that generates $300-500/day in direct AI costs. Observability becomes 20-80% of total operational cost.
The solution is not to stop observing. The solution is to observe intelligently: sample traces adaptively, manage metric cardinality, optimize log levels, and separate AI-specific telemetry from infrastructure telemetry.
Uniform sampling (e.g., "sample 10% of all traces") throws away valuable data. Errors, slow requests, and high-cost operations should always be sampled. Normal, fast, cheap operations can be sampled aggressively. Adaptive sampling adjusts the sampling rate based on the characteristics of each request.
python
import random
import time
import hashlib
from dataclasses import dataclass, field
from typing import Dict, Optional, Callable
@dataclass
class SamplingConfig:
"""Configuration for adaptive sampling."""
base_rate: float = 0.10 # Sample 10% of normal traffic
error_rate: float = 1.0 # Sample 100% of errors
slow_rate: float = 1.0 # Sample 100% of slow requests
expensive_rate: float = 0.50 # Sample 50% of expensive requests
slow_threshold_ms: float = 2000.0
expensive_threshold_usd: float = 0.05
min_rate: float = 0.01 # Never go below 1%
max_rate: float = 1.0 # Never exceed 100%
class AdaptiveSampler:
"""Adaptive trace sampler for AI agent observability.
Dynamically adjusts sampling rates based on request characteristics:
- Errors: always sampled
- Slow requests: always sampled
- Expensive operations: sampled at 50%
- Normal operations: sampled at configurable base rate
- Under high load: base rate decreases to control costs
The sampler also implements head-based sampling with consistent
hashing, so all spans for a given trace are either all sampled
or all dropped.
"""
def __init__(self, config: Optional[SamplingConfig] = None):
self.config = config or SamplingConfig()
self._stats = {
"total": 0,
"sampled": 0,
"dropped": 0,
"by_reason": {},
}
self._load_window: list = [] # (timestamp, count) for load tracking
self._current_rate = self.config.base_rate
def should_sample(
self,
trace_id: str,
is_error: bool = False,
duration_ms: Optional[float] = None,
cost_usd: Optional[float] = None,
metadata: Optional[Dict] = None,
) -> bool:
"""Decide whether to sample a trace."""
self._stats["total"] += 1
# Determine the sampling rate for this request
rate, reason = self._compute_rate(is_error, duration_ms, cost_usd)
# Consistent hashing: same trace_id always gets same decision
hash_val = int(hashlib.md5(trace_id.encode()).hexdigest()[:8], 16)
threshold = int(rate * 0xFFFFFFFF)
sampled = hash_val <= threshold
if sampled:
self._stats["sampled"] += 1
else:
self._stats["dropped"] += 1
self._stats["by_reason"][reason] = (
self._stats["by_reason"].get(reason, 0) + 1
)
return sampled
def _compute_rate(
self,
is_error: bool,
duration_ms: Optional[float],
cost_usd: Optional[float],
) -> tuple:
"""Compute the sampling rate and reason for this request."""
if is_error:
return self.config.error_rate, "error"
if duration_ms is not None and duration_ms > self.config.slow_threshold_ms:
return self.config.slow_rate, "slow"
if cost_usd is not None and cost_usd > self.config.expensive_threshold_usd:
return self.config.expensive_rate, "expensive"
# Adjust base rate based on load
self._update_load()
return self._current_rate, "normal"
def _update_load(self):
"""Reduce sampling rate under high load to control costs."""
now = time.monotonic()
self._load_window.append(now)
# Keep only the last 60 seconds
cutoff = now - 60
self._load_window = [t for t in self._load_window if t > cutoff]
requests_per_minute = len(self._load_window)
# Scale down base rate as load increases
if requests_per_minute > 10000:
self._current_rate = max(self.config.min_rate, self.config.base_rate * 0.1)
elif requests_per_minute > 5000:
self._current_rate = max(self.config.min_rate, self.config.base_rate * 0.25)
elif requests_per_minute > 1000:
self._current_rate = max(self.config.min_rate, self.config.base_rate * 0.5)
else:
self._current_rate = self.config.base_rate
def get_stats(self) -> dict:
total = self._stats["total"]
return {
"total_decisions": total,
"sampled": self._stats["sampled"],
"dropped": self._stats["dropped"],
"effective_rate_pct": round(
self._stats["sampled"] / total * 100, 1
) if total > 0 else 0,
"current_base_rate": round(self._current_rate, 4),
"by_reason": self._stats["by_reason"],
}
def estimate_cost_savings(
self,
cost_per_span: float = 0.0000017, # $1.70 per million spans
avg_spans_per_trace: int = 15,
) -> dict:
"""Estimate observability cost savings from sampling."""
total = self._stats["total"]
dropped = self._stats["dropped"]
full_cost = total * avg_spans_per_trace * cost_per_span
actual_cost = (total - dropped) * avg_spans_per_trace * cost_per_span
return {
"full_cost_usd": round(full_cost, 4),
"sampled_cost_usd": round(actual_cost, 4),
"savings_usd": round(full_cost - actual_cost, 4),
"savings_pct": round(
(full_cost - actual_cost) / full_cost * 100, 1
) if full_cost > 0 else 0,
}
# Usage
sampler = AdaptiveSampler(SamplingConfig(
base_rate=0.10,
error_rate=1.0,
slow_rate=1.0,
expensive_rate=0.50,
slow_threshold_ms=2000,
expensive_threshold_usd=0.05,
))
# Simulate 10,000 requests with realistic distributions
import uuid
for i in range(10000):
trace_id = uuid.uuid4().hex
is_error = random.random() < 0.02 # 2% error rate
duration = random.gauss(500, 300) # Mean 500ms, some slow outliers
cost = random.gauss(0.01, 0.02) # Mean $0.01, some expensive outliers
sampler.should_sample(
trace_id=trace_id,
is_error=is_error,
duration_ms=max(50, duration),
cost_usd=max(0.001, cost),
)
stats = sampler.get_stats()
print(f"Effective sampling rate: {stats['effective_rate_pct']}%")
print(f"Sampled: {stats['sampled']}, Dropped: {stats['dropped']}")
print(f"By reason: {stats['by_reason']}")
savings = sampler.estimate_cost_savings()
print(f"Observability cost savings: ${savings['savings_usd']:.4f} "
f"({savings['savings_pct']}%)")
Metric cardinality -- the number of unique time series -- is the silent killer of observability budgets. Every unique combination of metric name and label values creates a new time series. If you have a metric agent.request.duration with labels agent_id (50 values), model (6 values), status (3 values), and endpoint (20 values), the cardinality is 50 * 6 * 3 * 20 = 18,000 time series. Add a request_id label by accident and cardinality becomes infinite.
python
from typing import Set, Dict, List, Optional
from collections import defaultdict
class CardinalityGuard:
"""Prevents metric cardinality explosion by enforcing limits
on label values and dropping high-cardinality dimensions.
Typical cost: Prometheus/Grafana charges ~$8 per 1000 active
time series per month. Uncontrolled cardinality can create
millions of series, costing thousands of dollars per month
for metrics alone.
"""
def __init__(
self,
max_cardinality_per_metric: int = 5000,
max_label_values: int = 100,
blocked_labels: Optional[Set[str]] = None,
):
self.max_cardinality = max_cardinality_per_metric
self.max_label_values = max_label_values
self.blocked_labels = blocked_labels or {
"request_id", "trace_id", "session_id",
"user_id", "conversation_id", "message_id",
}
self._label_value_counts: Dict[str, Dict[str, int]] = defaultdict(dict)
self._series_counts: Dict[str, int] = defaultdict(int)
self._dropped: int = 0
def sanitize_labels(
self,
metric_name: str,
labels: Dict[str, str],
) -> Optional[Dict[str, str]]:
"""Sanitize labels to prevent cardinality explosion.
Returns sanitized labels, or None if the metric should be dropped.
"""
# Remove blocked high-cardinality labels
sanitized = {
k: v for k, v in labels.items()
if k not in self.blocked_labels
}
# Check per-label cardinality
for key, value in list(sanitized.items()):
if key not in self._label_value_counts[metric_name]:
self._label_value_counts[metric_name][key] = {}
values = self._label_value_counts[metric_name][key]
if value not in values:
if len(values) >= self.max_label_values:
# Replace with "other" to cap cardinality
sanitized[key] = "__other__"
else:
values[value] = 0
values.get(value, None)
# Check total series cardinality for this metric
series_key = f"{metric_name}:{sorted(sanitized.items())}"
self._series_counts[metric_name] += 1
if len(set(
str(sorted(labels.items()))
for labels in [sanitized]
)) > self.max_cardinality:
self._dropped += 1
return None
return sanitized
def get_stats(self) -> dict:
return {
"tracked_metrics": len(self._series_counts),
"dropped_points": self._dropped,
"label_cardinality": {
metric: {
label: len(values)
for label, values in labels.items()
}
for metric, labels in self._label_value_counts.items()
},
}
# Usage
guard = CardinalityGuard(
max_cardinality_per_metric=5000,
max_label_values=50,
blocked_labels={"request_id", "trace_id", "session_id"},
)
# This label set is safe
safe = guard.sanitize_labels("agent.duration", {
"agent_id": "researcher-01",
"model": "gpt-4o",
"status": "success",
})
print(f"Safe labels: {safe}")
# This would cause cardinality explosion -- request_id is blocked
dangerous = guard.sanitize_labels("agent.duration", {
"agent_id": "researcher-01",
"request_id": "req_abc123", # Blocked -- infinite cardinality
"model": "gpt-4o",
})
print(f"Sanitized labels: {dangerous}") # request_id removed
Agent systems produce massive log volumes. A single LLM call can generate DEBUG-level logs for the full prompt, the full response, token counts, timing breakdowns, cache hits, and model metadata. At INFO level, you get the operation, model, token count, and duration. At WARN level, you get only slow requests and errors.
The optimization: use structured logging with dynamic level control per agent, and route AI-specific logs to a cheaper storage tier.
python
import logging
import json
from typing import Dict, Optional
class CostAwareLogger:
"""Logger that optimizes log volume based on cost impact.
Routes logs to different backends based on category:
- AI inference logs: sampled, stored in cheap tier
- Error logs: always stored, stored in hot tier
- Debug logs: only in development, never in production
Estimated savings: 60-80% of log ingestion costs.
"""
def __init__(
self,
agent_id: str,
production: bool = True,
sample_rate: float = 0.10,
):
self.agent_id = agent_id
self.production = production
self.sample_rate = sample_rate
self._log_counts: Dict[str, int] = {}
self.logger = logging.getLogger(f"agent.{agent_id}")
if production:
self.logger.setLevel(logging.WARNING)
else:
self.logger.setLevel(logging.DEBUG)
def inference(
self,
model: str,
input_tokens: int,
output_tokens: int,
duration_ms: float,
cost_usd: float,
):
"""Log an inference event. Sampled in production."""
self._count("inference")
if self.production and __import__("random").random() > self.sample_rate:
return # Drop this log in production (sampled)
self.logger.info(json.dumps({
"event": "inference",
"agent_id": self.agent_id,
"model": model,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"duration_ms": round(duration_ms, 1),
"cost_usd": round(cost_usd, 6),
}))
def tool_call(self, tool: str, duration_ms: float, status: str):
"""Log a tool call. Always logged (low volume)."""
self._count("tool_call")
self.logger.info(json.dumps({
"event": "tool_call",
"agent_id": self.agent_id,
"tool": tool,
"duration_ms": round(duration_ms, 1),
"status": status,
}))
def error(self, operation: str, error: str, context: Optional[Dict] = None):
"""Log an error. Always logged, never sampled."""
self._count("error")
self.logger.error(json.dumps({
"event": "error",
"agent_id": self.agent_id,
"operation": operation,
"error": error,
"context": context or {},
}))
def _count(self, event_type: str):
self._log_counts[event_type] = self._log_counts.get(event_type, 0) + 1
def get_log_volume_stats(self) -> dict:
total = sum(self._log_counts.values())
return {
"total_events": total,
"by_type": self._log_counts.copy(),
"estimated_bytes": total * 200, # ~200 bytes per structured log
"estimated_monthly_gb": round(total * 200 * 30 / 1e9, 3),
}
Before optimizing tokens, quantify the waste. Token waste is the difference between the tokens you send and the minimum tokens required to achieve the same output quality. Most agent systems waste 30-50% of input tokens on redundant system prompt content, verbose tool descriptions, unnecessary few-shot examples, and suboptimal response formatting instructions.
python
import re
from typing import Dict, List, Tuple
class TokenWasteAnalyzer:
"""Analyzes an agent's prompts for token waste opportunities.
Identifies: redundant instructions, verbose phrasing, unnecessary
few-shot examples, bloated tool descriptions, and suboptimal
response format specifications.
"""
def __init__(self, tokens_per_word: float = 1.3):
self.tokens_per_word = tokens_per_word
def estimate_tokens(self, text: str) -> int:
return int(len(text.split()) * self.tokens_per_word)
def analyze_system_prompt(self, prompt: str) -> Dict:
"""Analyze a system prompt for waste."""
total_tokens = self.estimate_tokens(prompt)
findings = []
# Check for redundant phrases
redundant = self._find_redundant_phrases(prompt)
if redundant:
findings.append({
"type": "redundant_phrases",
"count": len(redundant),
"examples": redundant[:5],
"estimated_waste_tokens": sum(
self.estimate_tokens(phrase) for phrase in redundant
),
})
# Check for verbose instructions
verbose = self._find_verbose_instructions(prompt)
if verbose:
findings.append({
"type": "verbose_instructions",
"count": len(verbose),
"examples": verbose[:3],
"estimated_waste_tokens": int(
sum(self.estimate_tokens(v[0]) - self.estimate_tokens(v[1])
for v in verbose)
),
})
# Check for unnecessary few-shot examples
examples = self._count_examples(prompt)
if examples > 3:
waste = (examples - 2) * 150 # ~150 tokens per extra example
findings.append({
"type": "excessive_examples",
"count": examples,
"recommended": 2,
"estimated_waste_tokens": waste,
})
total_waste = sum(f.get("estimated_waste_tokens", 0) for f in findings)
return {
"total_tokens": total_tokens,
"estimated_waste_tokens": total_waste,
"waste_pct": round(total_waste / total_tokens * 100, 1) if total_tokens > 0 else 0,
"findings": findings,
"potential_savings_per_1k_calls": round(
total_waste * 2.50 / 1_000_000 * 1000, 4
),
}
def _find_redundant_phrases(self, text: str) -> List[str]:
"""Find phrases that appear multiple times."""
# Split into sentences
sentences = re.split(r'[.!?]\s+', text)
seen_concepts = {}
redundant = []
for s in sentences:
key = " ".join(sorted(s.lower().split())[:5])
if key in seen_concepts and len(s.split()) > 5:
redundant.append(s.strip()[:100])
seen_concepts[key] = s
return redundant
def _find_verbose_instructions(self, text: str) -> List[Tuple[str, str]]:
"""Find verbose phrasings and suggest concise alternatives."""
patterns = [
(r"You must always make sure to (.+?)(?:\.|$)",
lambda m: m.group(1).strip()),
(r"It is (?:very |extremely )?important (?:that you |to )(.+?)(?:\.|$)",
lambda m: m.group(1).strip()),
(r"Please (?:make sure to |ensure that you )(.+?)(?:\.|$)",
lambda m: m.group(1).strip()),
]
verbose = []
for pattern, fix in patterns:
for match in re.finditer(pattern, text, re.IGNORECASE):
verbose.append((match.group(0), fix(match)))
return verbose
def _count_examples(self, text: str) -> int:
"""Count few-shot examples in the prompt."""
example_markers = [
r"Example \d+:", r"For example:", r"E\.g\.:",
r"Input:.*Output:", r"User:.*Assistant:",
r"```\n.*?\n```",
]
count = 0
for marker in example_markers:
count += len(re.findall(marker, text, re.IGNORECASE | re.DOTALL))
return count
# Usage
analyzer = TokenWasteAnalyzer()
bloated_prompt = """
You are a helpful financial analysis assistant. You must always make sure to
provide accurate financial data. It is very important that you cite your sources.
Please make sure to format all numbers with two decimal places.
You must always make sure to check your calculations twice. It is extremely
important to verify all financial figures. Please ensure that you provide
context for every number you present.
Example 1:
User: What is AAPL's P/E ratio?
Assistant: Apple's P/E ratio is 28.5 as of April 2026.
Example 2:
User: Compare AAPL and MSFT revenue.
Assistant: Apple revenue: $394B. Microsoft revenue: $245B.
Example 3:
User: What is the S&P 500 YTD return?
Assistant: The S&P 500 YTD return is 8.2% as of April 2026.
Example 4:
User: Calculate compound interest.
Assistant: Using the formula A = P(1 + r/n)^(nt)...
Example 5:
User: What is a good debt-to-equity ratio?
Assistant: Generally, a D/E ratio below 2.0 is considered healthy.
"""
analysis = analyzer.analyze_system_prompt(bloated_prompt)
print(f"Total tokens: {analysis['total_tokens']}")
print(f"Waste tokens: {analysis['estimated_waste_tokens']} ({analysis['waste_pct']}%)")
print(f"Savings per 1K calls: ${analysis['potential_savings_per_1k_calls']}")
for finding in analysis["findings"]:
print(f" {finding['type']}: {finding.get('estimated_waste_tokens', 0)} tokens")
The single largest token waste source is the system prompt. It is sent with every request, so even small reductions compound dramatically. Here is a systematic optimization process.
python
class SystemPromptOptimizer:
"""Optimizes system prompts through progressive compression.
Process:
1. Remove filler words and phrases
2. Consolidate duplicate instructions
3. Convert verbose rules to terse directives
4. Reduce few-shot examples to minimum effective set
5. Validate output quality is maintained
"""
def optimize(self, prompt: str) -> Tuple[str, dict]:
"""Optimize a system prompt. Returns (optimized, stats)."""
original_words = len(prompt.split())
result = prompt
# Step 1: Remove filler
result = self._remove_filler(result)
# Step 2: Consolidate duplicates
result = self._consolidate_duplicates(result)
# Step 3: Terse directives
result = self._to_terse_directives(result)
# Step 4: Clean up whitespace
result = re.sub(r'\n{3,}', '\n\n', result)
result = re.sub(r' +', ' ', result)
result = result.strip()
optimized_words = len(result.split())
reduction = (1 - optimized_words / original_words) * 100
return result, {
"original_words": original_words,
"optimized_words": optimized_words,
"reduction_pct": round(reduction, 1),
"estimated_token_savings": int(
(original_words - optimized_words) * 1.3
),
}
def _remove_filler(self, text: str) -> str:
fillers = [
r'\bplease\b', r'\bkindly\b', r'\bmake sure to\b',
r'\bit is (?:very |extremely )?important (?:that |to )',
r'\byou must always\b', r'\byou should always\b',
r'\bensure that you\b', r'\bremember to\b',
r'\bkeep in mind (?:that )?\b', r'\bnote that\b',
]
for filler in fillers:
text = re.sub(filler, '', text, flags=re.IGNORECASE)
return text
def _consolidate_duplicates(self, text: str) -> str:
sentences = re.split(r'(?<=[.!?])\s+', text)
seen = set()
unique = []
for s in sentences:
normalized = ' '.join(s.lower().split())
if normalized not in seen and len(normalized) > 10:
seen.add(normalized)
unique.append(s)
return ' '.join(unique)
def _to_terse_directives(self, text: str) -> str:
replacements = [
(r'When the user asks you to (.+?), you should (.+?)\.', r'\1: \2.'),
(r'If (?:the user |a user |someone )(.+?), then (.+?)\.', r'\1 -> \2.'),
(r'Always provide (.+?) in your responses', r'Include \1'),
(r'You are a (.+?) assistant', r'Role: \1'),
]
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
return text
optimizer = SystemPromptOptimizer()
optimized, stats = optimizer.optimize(bloated_prompt)
print(f"Before: {stats['original_words']} words")
print(f"After: {stats['optimized_words']} words")
print(f"Reduction: {stats['reduction_pct']}%")
print(f"Token savings: {stats['estimated_token_savings']}")
JSON output is often 30-50% more tokens than equivalent structured text. If your downstream consumer can parse structured text, switch from JSON to a compact text format.
python
class ResponseFormatOptimizer:
"""Optimizes response format instructions to minimize output tokens.
JSON: ~1.5x tokens vs structured text for the same information.
Markdown tables: ~1.2x tokens vs TSV.
Verbose JSON with descriptions: ~2.5x tokens vs compact JSON.
"""
FORMAT_COSTS = {
"verbose_json": 2.5, # {"name": "John", "age": 30, "city": "NYC"}
"compact_json": 1.5, # {"n":"John","a":30,"c":"NYC"}
"structured_text": 1.0, # John|30|NYC
"markdown_table": 1.2, # | John | 30 | NYC |
"tsv": 0.9, # John\t30\tNYC
}
@staticmethod
def json_to_compact_instruction() -> str:
"""Generate a prompt instruction for compact JSON output."""
return (
"Output: JSON. No whitespace. Short keys. "
"Example: {\"n\":\"val\",\"s\":1} not {\"name\": \"val\", \"score\": 1}"
)
@staticmethod
def estimate_format_savings(
records: int,
fields: int,
avg_value_length: int = 10,
from_format: str = "verbose_json",
to_format: str = "compact_json",
) -> dict:
"""Estimate token savings from format change."""
base_tokens = records * fields * (avg_value_length + 5)
from_tokens = base_tokens * ResponseFormatOptimizer.FORMAT_COSTS[from_format]
to_tokens = base_tokens * ResponseFormatOptimizer.FORMAT_COSTS[to_format]
savings = from_tokens - to_tokens
savings_pct = savings / from_tokens * 100
return {
"from_format": from_format,
"to_format": to_format,
"from_tokens": int(from_tokens),
"to_tokens": int(to_tokens),
"savings_tokens": int(savings),
"savings_pct": round(savings_pct, 1),
"savings_per_1k_calls_usd": round(
savings * 10.00 / 1_000_000 * 1000, 4 # Output token pricing
),
}
# Example: 50 records, 5 fields each
savings = ResponseFormatOptimizer.estimate_format_savings(
records=50, fields=5, from_format="verbose_json", to_format="compact_json"
)
print(f"Format change: {savings['from_format']} -> {savings['to_format']}")
print(f"Token savings: {savings['savings_tokens']} ({savings['savings_pct']}%)")
print(f"Savings per 1K calls: ${savings['savings_per_1k_calls_usd']}")
Agent tool descriptions are sent as part of the system prompt on every request. Twelve tools with verbose descriptions can add 3,000-5,000 tokens to every call. Compressing tool descriptions to the minimum needed for the model to use them correctly saves tokens on every single request.
python
class ToolDescriptionCompressor:
"""Compresses tool descriptions to minimize system prompt tokens.
A typical tool description with full parameter docs is 200-400 tokens.
Compressed to essential info only, it drops to 50-100 tokens.
With 12 tools, that is 1,800-3,600 tokens saved per request.
"""
def compress_tool(self, tool: dict) -> dict:
"""Compress a tool description."""
compressed = {
"name": tool["name"],
"description": self._shorten_description(
tool.get("description", "")
),
"parameters": self._compress_parameters(
tool.get("parameters", {})
),
}
return compressed
def _shorten_description(self, desc: str) -> str:
"""Shorten description to first sentence + key constraints."""
sentences = desc.split(". ")
if sentences:
short = sentences[0]
# Keep constraint sentences (containing "must", "required", "max")
for s in sentences[1:]:
if any(w in s.lower() for w in ["must", "required", "max", "min", "limit"]):
short += ". " + s
return short + "."
return desc
def _compress_parameters(self, params: dict) -> dict:
"""Remove verbose parameter descriptions, keep type and required."""
if "properties" not in params:
return params
compressed = {
"type": params.get("type", "object"),
"properties": {},
"required": params.get("required", []),
}
for name, prop in params["properties"].items():
compressed["properties"][name] = {
"type": prop.get("type", "string"),
}
if "enum" in prop:
compressed["properties"][name]["enum"] = prop["enum"]
return compressed
def compress_toolkit(self, tools: list) -> tuple:
"""Compress all tools and report savings."""
import json
original_tokens = sum(
len(json.dumps(t).split()) * 1.3 for t in tools
)
compressed = [self.compress_tool(t) for t in tools]
compressed_tokens = sum(
len(json.dumps(t).split()) * 1.3 for t in compressed
)
savings = original_tokens - compressed_tokens
return compressed, {
"original_tokens": int(original_tokens),
"compressed_tokens": int(compressed_tokens),
"savings_tokens": int(savings),
"savings_pct": round(savings / original_tokens * 100, 1),
"savings_per_1k_calls_usd": round(
savings * 2.50 / 1_000_000 * 1000, 4
),
}
# Example: compress a verbose tool description
compressor = ToolDescriptionCompressor()
verbose_tool = {
"name": "search_documents",
"description": (
"Search the document database for relevant documents matching "
"the given query. This tool searches across all indexed documents "
"including PDFs, Word documents, and plain text files. The search "
"uses semantic similarity matching powered by embeddings. Results "
"are returned in order of relevance. You must provide a query "
"parameter. Maximum 50 results per query."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to match against documents. "
"Should be a natural language description of what "
"you are looking for. More specific queries produce "
"better results.",
},
"max_results": {
"type": "integer",
"description": "The maximum number of results to return. "
"Defaults to 10. Maximum allowed value is 50.",
},
"file_type": {
"type": "string",
"enum": ["pdf", "docx", "txt", "all"],
"description": "Filter results by file type. Use 'all' to "
"search across all file types.",
},
},
"required": ["query"],
},
}
compressed = compressor.compress_tool(verbose_tool)
print(f"Original description: {len(verbose_tool['description'].split())} words")
print(f"Compressed description: {len(compressed['description'].split())} words")
Every cost optimization strategy in the previous chapters shares a blind spot: they measure cost in terms of inputs consumed -- tokens processed, API calls made, spans recorded, bytes stored. Input-based billing tells you what you spent, but it tells you nothing about what you got for that spending. A research agent that burns $4.20 in tokens and produces a report that saves the user three hours of manual work is wildly profitable. The same agent burning $0.80 and producing a hallucinated report that the user discards is infinitely expensive -- the cost-per-useful-outcome is undefined because the outcome was worthless.
Outcome-based metering flips the equation. Instead of asking "how much did we spend?", it asks "how much did we spend per successful outcome?" This reframing changes every optimization decision. A model upgrade that increases token cost by 40% but doubles the success rate from 45% to 90% actually cuts cost-per-outcome by 30%. A prompt compression that saves 20% on tokens but drops success rate by 15% is a net loss. Without outcome metering, you would celebrate the token savings and never notice the quality degradation until customer complaints arrived weeks later.
The shift requires three capabilities: defining what constitutes a successful outcome, attributing outcomes across multi-step agent pipelines, and building the metering infrastructure to track cost-per-outcome in real time.
Not every agent invocation has a binary success/failure. Outcomes exist on a spectrum, and your metering system must capture that nuance. Here is a taxonomy that works across most agent use cases.
1. Task Completion. Did the agent finish the requested task? A code generation agent that produces compilable code has completed its task. One that errors out mid-generation has not. Completion rate is the baseline metric -- if your agents are not completing tasks, nothing else matters.
2. Quality Score. Given that the task completed, how good was the output? Quality is domain-specific: for a summarization agent, it might be a ROUGE score against human summaries. For a classification agent, it is accuracy against labeled data. For a research agent, it might be the percentage of cited claims that are verifiable. Quality scores should be normalized to 0.0-1.0.
3. User Acceptance. Did the end user accept the agent's output? This is the ultimate arbiter. An agent that produces technically correct output that the user rewrites from scratch has a zero acceptance rate. Track explicit signals (user clicked "accept", "reject", "edit") and implicit signals (user copied the output, user ignored it, user asked a follow-up that implies the first answer was wrong).
4. Business Value. What was the downstream impact? A lead qualification agent that correctly identifies a $500K deal opportunity delivers more value than one that correctly classifies a $5K opportunity, even though both are "correct." Business value metering connects agent cost to revenue impact.
python
import time
import hashlib
from dataclasses import dataclass, field
from typing import Optional, Dict, List, Any
from enum import Enum
from datetime import datetime, timezone
class OutcomeStatus(Enum):
SUCCESS = "success"
PARTIAL = "partial" # Task completed but with quality issues
FAILURE = "failure" # Task did not complete
REJECTED = "rejected" # User explicitly rejected the output
PENDING = "pending" # Awaiting user feedback
@dataclass
class OutcomeEvent:
"""Records the outcome of an agent task alongside its cost."""
outcome_id: str
agent_id: str
task_type: str
status: OutcomeStatus
cost_usd: float
quality_score: float # 0.0 to 1.0
user_accepted: Optional[bool] = None
business_value_usd: float = 0.0
input_tokens: int = 0
output_tokens: int = 0
latency_ms: float = 0.0
retries: int = 0
timestamp: str = field(
default_factory=lambda: datetime.now(timezone.utc).isoformat()
)
metadata: Dict[str, Any] = field(default_factory=dict)
class OutcomeMeter:
"""Tracks cost-per-outcome across agent operations.
Thread-safe. Computes real-time cost efficiency metrics
including cost-per-success, cost-per-quality-point, and
ROI based on business value attribution.
"""
def __init__(self):
self._outcomes: List[OutcomeEvent] = []
def record(self, outcome: OutcomeEvent):
"""Record an outcome event."""
self._outcomes.append(outcome)
def cost_per_success(
self,
agent_id: Optional[str] = None,
task_type: Optional[str] = None,
) -> Dict[str, Any]:
"""Calculate cost per successful outcome.
This is the core metric. Total cost divided by number of
successful outcomes gives you the true unit economics of
your agent fleet.
"""
events = self._filter(agent_id, task_type)
if not events:
return {"error": "no outcomes recorded"}
total_cost = sum(e.cost_usd for e in events)
successes = [e for e in events if e.status == OutcomeStatus.SUCCESS]
failures = [e for e in events if e.status == OutcomeStatus.FAILURE]
partials = [e for e in events if e.status == OutcomeStatus.PARTIAL]
success_count = len(successes)
cost_per_success = total_cost / success_count if success_count > 0 else float("inf")
# Cost wasted on failures
wasted_cost = sum(e.cost_usd for e in failures)
# Cost of retries (successful outcomes that required retries)
retry_overhead = sum(
e.cost_usd * (e.retries / (e.retries + 1))
for e in successes if e.retries > 0
)
return {
"total_outcomes": len(events),
"successes": success_count,
"failures": len(failures),
"partials": len(partials),
"success_rate_pct": round(success_count / len(events) * 100, 1),
"total_cost_usd": round(total_cost, 4),
"cost_per_success_usd": round(cost_per_success, 4),
"wasted_cost_usd": round(wasted_cost, 4),
"wasted_pct": round(wasted_cost / total_cost * 100, 1) if total_cost > 0 else 0,
"retry_overhead_usd": round(retry_overhead, 4),
}
def quality_adjusted_cost(
self,
agent_id: Optional[str] = None,
task_type: Optional[str] = None,
) -> Dict[str, Any]:
"""Calculate quality-adjusted cost per outcome.
Weights each outcome by its quality score. A $0.05 outcome
with quality 0.9 costs $0.056 per quality point. A $0.03
outcome with quality 0.5 costs $0.060 per quality point.
The cheaper outcome is actually more expensive when adjusted
for quality.
"""
events = self._filter(agent_id, task_type)
if not events:
return {"error": "no outcomes recorded"}
total_cost = sum(e.cost_usd for e in events)
total_quality = sum(e.quality_score for e in events)
avg_quality = total_quality / len(events)
quality_adjusted = total_cost / total_quality if total_quality > 0 else float("inf")
# Segment by quality tier
high_quality = [e for e in events if e.quality_score >= 0.8]
mid_quality = [e for e in events if 0.5 <= e.quality_score < 0.8]
low_quality = [e for e in events if e.quality_score < 0.5]
return {
"total_outcomes": len(events),
"avg_quality_score": round(avg_quality, 3),
"total_cost_usd": round(total_cost, 4),
"cost_per_quality_point_usd": round(quality_adjusted, 4),
"high_quality_count": len(high_quality),
"high_quality_avg_cost": round(
sum(e.cost_usd for e in high_quality) / len(high_quality), 4
) if high_quality else 0,
"mid_quality_count": len(mid_quality),
"mid_quality_avg_cost": round(
sum(e.cost_usd for e in mid_quality) / len(mid_quality), 4
) if mid_quality else 0,
"low_quality_count": len(low_quality),
"low_quality_avg_cost": round(
sum(e.cost_usd for e in low_quality) / len(low_quality), 4
) if low_quality else 0,
}
def roi_analysis(
self,
agent_id: Optional[str] = None,
task_type: Optional[str] = None,
) -> Dict[str, Any]:
"""Calculate ROI based on business value attribution.
Compares the total cost of agent operations against the
business value generated by successful outcomes.
"""
events = self._filter(agent_id, task_type)
if not events:
return {"error": "no outcomes recorded"}
total_cost = sum(e.cost_usd for e in events)
total_value = sum(e.business_value_usd for e in events if e.status == OutcomeStatus.SUCCESS)
roi = ((total_value - total_cost) / total_cost * 100) if total_cost > 0 else 0
return {
"total_cost_usd": round(total_cost, 2),
"total_business_value_usd": round(total_value, 2),
"net_value_usd": round(total_value - total_cost, 2),
"roi_pct": round(roi, 1),
"cost_to_value_ratio": round(
total_cost / total_value, 4
) if total_value > 0 else float("inf"),
}
def _filter(
self,
agent_id: Optional[str],
task_type: Optional[str],
) -> List[OutcomeEvent]:
events = self._outcomes
if agent_id:
events = [e for e in events if e.agent_id == agent_id]
if task_type:
events = [e for e in events if e.task_type == task_type]
return events
# Usage: track outcomes for a research agent fleet
meter = OutcomeMeter()
# Simulate 500 research tasks across two agents
import random
random.seed(42)
for i in range(500):
agent = random.choice(["researcher-01", "researcher-02"])
quality = random.gauss(0.78, 0.15)
quality = max(0.0, min(1.0, quality))
failed = quality < 0.3
retries = random.randint(0, 3) if not failed else 0
base_cost = random.gauss(0.04, 0.015)
cost = base_cost * (1 + retries * 0.8)
meter.record(OutcomeEvent(
outcome_id=f"task_{i:04d}",
agent_id=agent,
task_type="research_report",
status=OutcomeStatus.FAILURE if failed else OutcomeStatus.SUCCESS,
cost_usd=max(0.005, cost),
quality_score=quality,
business_value_usd=0 if failed else random.uniform(2.0, 15.0),
retries=retries,
))
cps = meter.cost_per_success()
print(f"Success rate: {cps['success_rate_pct']}%")
print(f"Cost per success: ${cps['cost_per_success_usd']}")
print(f"Wasted on failures: ${cps['wasted_cost_usd']} ({cps['wasted_pct']}%)")
qac = meter.quality_adjusted_cost()
print(f"Cost per quality point: ${qac['cost_per_quality_point_usd']}")
roi = meter.roi_analysis()
print(f"ROI: {roi['roi_pct']}%")
print(f"Cost-to-value ratio: {roi['cost_to_value_ratio']}")
In a single-agent system, attribution is trivial: the agent either succeeded or it did not. Multi-agent pipelines break this simplicity. A research pipeline with an intake agent, a search agent, a synthesis agent, and a review agent involves four cost centers. When the pipeline produces a high-quality report, which agent gets credit? When it produces garbage, which agent is at fault?
The answer is proportional attribution with failure isolation. Each agent in the pipeline gets a share of the outcome value proportional to its cost contribution, but failure attribution is localized to the specific agent that caused the failure.
python
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
@dataclass
class PipelineStep:
"""A single step in a multi-agent pipeline."""
agent_id: str
step_name: str
cost_usd: float
quality_score: float # Quality of this step's output
duration_ms: float
input_tokens: int = 0
output_tokens: int = 0
passed_quality_gate: bool = True
failure_reason: Optional[str] = None
@dataclass
class PipelineOutcome:
"""Complete outcome of a multi-agent pipeline execution."""
pipeline_id: str
steps: List[PipelineStep]
final_status: OutcomeStatus
final_quality: float
business_value_usd: float = 0.0
class PipelineAttributor:
"""Attributes costs and outcomes across multi-agent pipelines.
Uses pro