Token Tracking

Read real token spend from any agent — no flags, no setup

Every agent records its token usage; read the running total whenever you need it.

Diagram

Token Tracking

🤖 Agent Runs

💾 Collector Records

📊 Session Summary

Token accounting is on for every agent that runs through the native OpenAI path — the default when you pass a bare model name like "gpt-4o-mini". You don’t turn it on, you just read it back.

Quick Start

  1. Run an agent and read the total

    A bare Agent(llm="gpt-4o-mini") records its usage automatically. Read it back with the global collector:

    python
    from praisonaiagents import Agent
    from praisonaiagents.telemetry.token_collector import get_token_collector
    
    agent = Agent(llm="gpt-4o-mini")
    agent.start("Summarise the theory of relativity in one paragraph")
    
    summary = get_token_collector().get_session_summary()
    print(summary["total_interactions"])
    print(summary["total_metrics"]["input_tokens"])
    print(summary["total_metrics"]["output_tokens"])

    The numbers are real spend — no metrics=True, no verbose, no display flag required.

  2. Break usage down per agent

    Pass a name to each agent and the collector rolls usage up under by_agent:

    python
    from praisonaiagents import Agent
    from praisonaiagents.telemetry.token_collector import get_token_collector
    
    researcher = Agent(name="researcher", llm="gpt-4o-mini")
    writer = Agent(name="writer", llm="gpt-4o-mini")
    
    researcher.start("Find three facts about the Moon")
    writer.start("Write a haiku about the Moon")
    
    summary = get_token_collector().get_session_summary()
    print(summary["by_agent"]["researcher"]["output_tokens"])
    print(summary["by_agent"]["writer"]["output_tokens"])

    Agents built with a real LLM instance can also carry a stable agent_id alongside the display name. The collector’s by_agent keying matches whichever attribution the runtime installed (current_agent_name today; scoped team views still filter by Agent.name).

How It Works

Accounting happens inside the native OpenAIClient after every successful completion — one record per tool-loop iteration — and lands in the global TokenCollector.

Diagram
CollectorOpenAIClientAgentUserCollectorOpenAIClientAgentUseragent.start(“…”)chat_completion_with_tools(agent_name=…)track_tokens(model, agent, metrics)responseget_session_summary(){total, by_model, by_agent}

Session Summary Shape

get_session_summary() returns a plain dict you can read or serialise:

python
{
    "total_interactions": 1,
    "total_tokens": 320,
    "total_metrics": {
        "input_tokens": 200,
        "output_tokens": 120,
        "cached_tokens": 0,
        "reasoning_tokens": 0,
        "audio_input_tokens": 0,
        "audio_output_tokens": 0,
    },
    "by_model": {
        "gpt-4o-mini": {
            "input_tokens": 200,
            "output_tokens": 120,
            "cached_tokens": 0,
            "reasoning_tokens": 0,
            "audio_input_tokens": 0,
            "audio_output_tokens": 0,
        }
    },
    "by_agent": {
        "researcher": {
            "input_tokens": 200,
            "output_tokens": 120,
            "cached_tokens": 0,
            "reasoning_tokens": 0,
            "audio_input_tokens": 0,
            "audio_output_tokens": 0,
        }
    },
}
FieldTypeDescription
total_interactionsintNumber of recorded completions this session
total_tokensintinput_tokens + output_tokens across all interactions
total_metricsdictBroken-out token counts for the whole session
by_modeldictSame token breakout keyed by model name
by_agentdictSame token breakout keyed by agent name

In the example above total_tokens is 320 because it is input_tokens (200) + output_tokens (120) — nothing else. Each metrics block also carries cached_tokens, reasoning_tokens, audio_input_tokens, and audio_output_tokens for visibility, but those are subsets of the input/output counts, so they are not added into the total.

Concrete illustration you can compare against:

python
from praisonaiagents.telemetry.token_collector import TokenMetrics

m = TokenMetrics(input_tokens=1000, output_tokens=500, cached_tokens=800, reasoning_tokens=400)
assert m.total_tokens == 1500  # input + output only, not 2700

The 1500 is the provider’s own reported total. The cached_tokens (800) sit inside the 1000 input tokens, and the reasoning_tokens (400) sit inside the 500 output tokens — summing all four would report 2700, which is wrong.

Scoping to one team (concurrent instances)

The get_token_collector() singleton is process-wide: every agent in every PraisonAIAgents instance writes into the same collector. Reading it directly gives you a combined total across the whole process.

Diagram

Team B

Team A

filtered by team_a’s names

filtered by team_b’s names

🤖 A_researcher

🤖 A_writer

🤖 B_analyst

💾 TokenCollector
process-wide

📊 team_a.get_token_usage_summary

📊 team_b.get_token_usage_summary

Since PraisonAI PR #4462 (finalized in PR #4470, which closes issue #4446), PraisonAIAgents exposes instance-scoped views that filter the singleton to the team’s own agents — so two concurrent teams never see each other’s spend.

python
from praisonaiagents import Agent, PraisonAIAgents

team_a = PraisonAIAgents(agents=[Agent(name="A_researcher"), Agent(name="A_writer")])
team_b = PraisonAIAgents(agents=[Agent(name="B_analyst")])

# Run both teams concurrently (or one after another)
team_a.start()
team_b.start()

# Instance-scoped: only team_a's own agents
print(team_a.get_token_usage_summary())
# {'total_metrics': {...team_a only...}, 'by_agent': {'A_researcher': {...}, 'A_writer': {...}}, ...}

# Instance-scoped: only team_b's own agents
print(team_b.get_token_usage_summary())
# {'total_metrics': {...team_b only...}, 'by_agent': {'B_analyst': {...}}, ...}

# Global process-wide totals (unchanged behavior)
from praisonaiagents.telemetry.token_collector import get_token_collector
print(get_token_collector().get_session_summary())
# {'total_metrics': {...team_a + team_b...}, ...}

The scoped methods:

MethodScopeFallback
PraisonAIAgents.get_token_usage_summary()This team’s agents onlyUnfiltered summary if no named agents
PraisonAIAgents.get_detailed_token_report()This team’s agents only (summary + recent interactions + cost estimate)Unfiltered if no named agents
PraisonAIAgents.display_token_usage()This team’s agents only (prints table)Unfiltered if no named agents
get_token_collector().get_session_summary()Process-wide (all agents in the process)—

Common Patterns

How much did this run cost?

Reset before, read the summary after — you get the spend for just this run:

python
from praisonaiagents import Agent
from praisonaiagents.telemetry.token_collector import get_token_collector

collector = get_token_collector()
collector.reset()

agent = Agent(llm="gpt-4o-mini")
agent.start("Draft a product announcement")

summary = collector.get_session_summary()
print(summary["total_metrics"]["input_tokens"], summary["total_metrics"]["output_tokens"])
Multi-agent breakdown

by_agent gives a per-agent rollup so you can attribute spend to the right worker:

python
summary = get_token_collector().get_session_summary()
for name, metrics in summary["by_agent"].items():
    print(name, metrics["output_tokens"])

In multi-agent teams the executor’s LLM instance (executor_agent.llm_instance) is the one wired into set_current_agent() / last_token_metrics, so per-agent accounting is accurate for agents built with a real LLM object — not just a bare model-name string. That instance exposes set_current_agent(name, agent_id=None) plus current_agent_name and current_agent_id, so you can read back which agent a completion was attributed to.

Stable per-agent attribution across concurrent tasks

A single LLM instance shared by concurrent agents (gateway channels, asyncio.gather) previously risked one agent’s set_current_agent() clobbering another’s mid-completion (issues #5052 and #1746). Since PraisonAI PR #5359, attribution lives in per-instance context variables and set_current_agent() also accepts an agent_id, so downstream aggregation keys stay stable even when two agents share a display name.

python
llm.set_current_agent(agent_name="researcher", agent_id="researcher-01")
print(llm.current_agent_name, llm.current_agent_id)
# researcher researcher-01

An Agent (and the LLM it uses) is now safely deep-copyable, so cloning an agent with copy.deepcopy for parallel runs no longer raises.

Every tool-loop iteration is billed and recorded

When an agent calls a tool and then completes, each model call is its own paid iteration. A single tool-using turn therefore records total_interactions == 2 — one for the tool-call step and one for the final answer — and both are summed into the totals.

Latest Call Only

For CLI or Harbor envelopes that need the most recent completion’s usage — not the session total — read OpenAIClient.last_token_metrics. It exposes the latest completion’s TokenMetrics and is cleared when a response carries no usable usage.

Related