n4nAI

Token-level tracing: seeing inside agent decisions

A practical guide to token-level tracing AI agents: capture per-token usage, attribute decisions, and rebuild agent reasoning paths with real code.

n4n Team4 min read834 words

Audio narration

Coming soon — every post will get a voice note here.

Most agent observability stacks stop at span-level token counts. Token-level tracing AI agents goes deeper: it records which exact tokens triggered a tool call, which were served from cache, and how prompt prefixes shifted across iterations. That granularity turns black-box agent loops into auditable decision streams.

Why span-level metrics hide agent failures

A span that reports prompt_tokens: 4200, completion_tokens: 180 tells you the call was expensive. It does not tell you that 3,900 of those prompt tokens were redundant repetitions of the system prompt because your agent loop mutated the message array incorrectly. It does not tell you the model emitted a malformed tool call that your parser silently dropped, wasting 40 completion tokens.

When you debug agents in production, the bug is rarely “the model was wrong.” It is “the prompt we built was not the prompt we thought we built.” Token-level tracing AI agents exposes the prompt construction and generation process as data, not guesswork.

Step 1: Capture raw token events at the boundary

Intercept every model call at the HTTP boundary. If you route through a gateway, you get one place to hook. A gateway like n4n.ai that exposes an OpenAI-compatible endpoint across 240+ models and meters per-token usage gives you a single interception point for token-level tracing AI agents without per-provider glue.

Wrap the client so you always stream and ask for usage:

from openai import OpenAI

client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="sk-...")

def traced_chat(messages, model="anthropic/claude-3.5-sonnet", **kwargs):
    chunks = []
    usage_total = None
    stream = client.chat.completions.create(
        model=model,
        messages=messages,
        stream=True,
        stream_options={"include_usage": True},
    )
    for chunk in stream:
        if chunk.usage:
            usage_total = chunk.usage
        if chunk.choices and chunk.choices[0].delta.content:
            chunks.append(chunk.choices[0].delta.content)
    return "".join(chunks), usage_total

Streaming adds marginal latency but gives you chunk boundaries. If you cannot stream, fall back to non-streaming usage in the response—you lose intra-call timing but keep totals.

Step 2: Attribute tokens to agent phases

An agent run is a sequence of phases: retrieve, plan, act, observe, reflect. Tag each message with a phase identifier before sending. Store the tag alongside the token count for that call.

messages = [
    {"role": "system", "content": sys_prompt, "meta": {"phase": "init"}},
    {"role": "user", "content": query, "meta": {"phase": "retrieve"}},
]

After the call, record:

trace_log.append({
    "phase": "retrieve",
    "model": model,
    "prompt_tokens": usage_total.prompt_tokens,
    "completion_tokens": usage_total.completion_tokens,
    "call_id": call_id,
})

Do not rely on span names from your framework. Framework spans often group multiple model calls under one “agent step.” You need the per-call phase mapping to see where tokens actually went.

Step 3: Decode cached vs generated tokens

Providers that support prompt caching return breakdown details. OpenAI-compatible responses include prompt_tokens_details when caching is active:

{
  "prompt_tokens": 1820,
  "completion_tokens": 64,
  "prompt_tokens_details": {
    "cached_tokens": 1500
  }
}

If cached_tokens is low on calls where you expected a stable prefix, your message array is shifting. A common cause: injecting a timestamp or request ID into the system prompt on every iteration. That breaks the cache prefix and silently multiplies cost. Token-level tracing AI agents makes this visible because you watch cached_tokens drop to zero across loops.

Step 4: Reconstruct tool-call boundaries

Function-calling agents emit structured tokens for function_call or tool blocks. Capture the substring where the tool name first appears and the closing brace. This lets you measure how many completion tokens were spent formatting the call versus reasoning beforehand.

import re

def split_tool_call(text):
    m = re.search(r"<tool_call>(.*?)</tool_call>", text, re.DOTALL)
    if m:
        return text[:m.start()], m.group(1)
    return text, None

If the pre-tool reasoning span grows across iterations, your agent is looping. That pattern shows up clearly only when you trace tokens per call, not when you look at daily aggregate spend.

Step 5: Store traces for post-hoc replay

Write traces to a columnar store or even JSONL. Keep the raw prompt token estimate (using the provider’s tokenizer or tiktoken for OpenAI models) alongside the provider’s count. Mismatch is expected for non-OpenAI models; record both.

{"run_id":"r1","call_id":"c3","phase":"act","model":"anthropic/claude-3.5-sonnet","prompt_tokens":1820,"cached_tokens":1500,"completion_tokens":64,"tool":"search"}

Replay scripts can then reconstruct the exact message list for any call id if you also snapshot the message array (minus raw document contents if privacy-sensitive).

Common pitfalls and tradeoffs

Tokenizer drift. tiktoken cl100k is wrong for Llama or Claude. Use the provider’s returned counts as source of truth. Local tokenizers are only for estimating before the call.

Storage cost. Full token-level tracing AI agents means storing per-call metadata for every run. At 10k runs/day with 5 calls each, that is 50k small records—manageable in Postgres, but decide retention up front. Raw prompt snapshots are the expensive part; hash them and store blobs in object storage.

Streaming overhead. Some legacy load balancers buffer streams. Verify your gateway forwards chunks uninterrupted, or your include_usage event arrives only at the end, reducing timeliness.

Privacy leakage. Prompt snapshots contain user data. Redact before writing to long-term trace stores. Cache hit rates can be computed without storing the cached prefix.

False precision. Token counts are not thought counts. A low completion token count does not mean a simple decision. Use tracing to locate anomalies, then read the actual text.

Production checklist

  • All model calls routed through one instrumented boundary.
  • stream_options.include_usage enabled on every streaming call.
  • Phase tags attached to messages before send.
  • prompt_tokens_details.cached_tokens alerted when below expected threshold.
  • Tool-call token splits computed for at least the top three tools.
  • Trace retention policy set; raw prompts redacted or externed.
  • Replay script validated against last 100 production runs.

Token-level tracing AI agents is not about watching tokens for their own sake. It is about making the agent’s internal contract with the model explicit: what we sent, what we reused, what we wasted. Ship the capture first, build dashboards later. The first time a cache miss triples your bill, the trace will pay for itself.

Tagstracingtokensobservabilityai-agents

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agent observability & tracing posts →