Symphony Token Accounting: How OpenAI's Symphony Tracks Codex Usage

Symphony tracks Codex token usage by extracting absolute cumulative totals from TokenUsageInfo structs, comparing them against per-thread snapshots stored in the orchestrator, and ignoring delta values unless absolutely necessary.

Symphony is an open-source orchestration framework developed by OpenAI that manages complex multi-turn conversations with Codex. When Codex processes requests, it emits token usage telemetry through the app-server protocol. Understanding how Symphony token accounting transforms this raw data into reliable per-thread metrics is essential for building accurate usage dashboards and billing systems.

Understanding the TokenUsageInfo Payload Structure

Codex communicates token consumption via a TokenUsageInfo struct containing two critical fields:

  • total_token_usage – An absolute, cumulative snapshot of tokens consumed on the thread since inception.
  • last_token_usage – The incremental delta that produced the current snapshot.
pub struct TokenUsageInfo {
    pub total_token_usage: TokenUsage,
    pub last_token_usage: TokenUsage,
    pub model_context_window: Option<i64>,
}

Symphony's orchestrator prioritizes the absolute total over the delta. According to the design specifications in elixir/docs/token_accounting.md, the absolute value serves as the canonical source of truth, while deltas are treated as supplementary data only used when absolute totals are unavailable.

Extracting Absolute Totals in the Orchestrator

The core extraction logic resides in lib/symphony_elixir/orchestrator.ex, where Symphony processes incoming Codex events and normalizes token usage data.

The Extraction Pipeline

The extract_token_usage/1 function implements a defensive search strategy across multiple possible payload locations:

defp extract_token_usage(update) do
  payloads = [
    update[:usage],
    Map.get(update, "usage"),
    Map.get(update, :usage),
    update[:payload],
    Map.get(update, "payload"),
    update
  ]

  Enum.find_value(payloads, &absolute_token_usage_from_payload/1) ||
    Enum.find_value(payloads, &turn_completed_usage_from_payload/1) ||
    %{}
end

This function attempts to locate token data in six potential locations, prioritizing explicit usage keys before falling back to generic payload objects or the raw update itself.

Path Resolution Strategy

Once a candidate payload is identified, absolute_token_usage_from_payload/1 traverses a prioritized list of nested paths to locate the absolute total:

absolute_paths = [
  ["params", "msg", "payload", "info", "total_token_usage"],
  [:params, :msg, :payload, :info, :total_token_usage],
  ["params", "msg", "info", "total_token_usage"],
  [:params, :msg, :info, :total_token_usage],
  ["params", "tokenUsage", "total"],
  [:params, :tokenUsage, :total],
  ["tokenUsage", "total"],
  [:tokenUsage, :total]
]

The function returns the first valid token usage map found at any of these locations, supporting both string and atom-keyed maps to handle different serialization formats.

Computing Token Deltas and Thread State

After extracting the absolute total, Symphony computes the actual delta against previously stored values using compute_token_delta/4:

defp compute_token_delta(running_entry, token_key, usage, reported_key) do
  next_total = get_token_usage(usage, token_key)
  prev_reported = Map.get(running_entry, reported_key, 0)

  delta =
    if is_integer(next_total) and next_total >= prev_reported do
      next_total - prev_reported
    else
      0
    end

  %{delta: max(delta, 0), reported: if(is_integer(next_total), do: next_total, else: prev_reported)}
end

The orchestrator maintains three critical fields per thread:

  • absolute_total: The latest cumulative snapshot received from Codex (e.g., tokenUsage.total).
  • accumulated_total: The value exposed to UI and API consumers, derived from absolute_total.
  • last_seen_turn_id: The identifier of the turn that produced the snapshot, used to prevent double-counting when events arrive out of order or duplicate.

Rendering Token Usage in the Dashboard

The status dashboard module (lib/symphony_elixir/status_dashboard.ex) reuses the same extraction helpers to generate human-readable descriptions of token consumption:

defp humanize_codex_wrapper_event("token_count", payload) do
  usage = extract_first_path(payload, token_usage_paths())

  case format_usage_counts(usage) do
    nil -> "token count update"
    usage_text -> "token count update (#{usage_text})"
  end
end

This ensures consistency between the accounting logic and user-facing displays, confirming that only absolute totals—not intermediate deltas—surface in the interface.

Token Accounting Design Principles

The elixir/docs/token_accounting.md document establishes strict guidelines for Symphony token accounting:

  1. Prefer thread/tokenUsage/updated.tokenUsage.total or info.total_token_usage as the authoritative source.
  2. Ignore last_token_usage, tokenUsage.last, and generic usage maps when calculating cumulative totals.
  3. Never double-count turn-completed usage payloads on top of live thread totals.
  4. Key all accounting by thread_id, recognizing that conversations span multiple turns across different model invocations.

These rules prevent the "delta accumulation drift" that occurs when systems erroneously sum incremental values instead of relying on absolute snapshots.

Summary

  • Symphony extracts absolute cumulative totals (total_token_usage) from Codex events rather than summing deltas.
  • The orchestrator in lib/symphony_elixir/orchestrator.ex implements a defensive search across multiple payload paths to locate token data.
  • Per-thread state tracks absolute_total, accumulated_total, and last_seen_turn_id to ensure idempotent accounting.
  • The system ignores last_token_usage unless absolute data is missing, preventing double-counting and drift.
  • All accounting keys by thread_id, treating conversations as persistent streams rather than isolated request-response pairs.

Frequently Asked Questions

What is the difference between total_token_usage and last_token_usage in Symphony?

total_token_usage represents the absolute cumulative count of tokens consumed on a thread since its creation, while last_token_usage represents only the incremental delta from the most recent turn. Symphony exclusively uses total_token_usage for accounting calculations, treating last_token_usage as a fallback signal only when absolute totals are unavailable.

How does Symphony prevent double-counting tokens?

Symphony prevents double-counting by storing the last_seen_turn_id alongside the absolute_total for each thread. When processing new events, the orchestrator compares the incoming absolute total against the stored snapshot. If the new total is less than or equal to the stored value, or if the turn ID matches a previously processed turn, the event is discarded.

Where does Symphony extract token usage data from Codex events?

The extraction logic in lib/symphony_elixir/orchestrator.ex searches six potential payload locations including update[:usage], Map.get(update, "payload"), and the raw update itself. Once a candidate payload is found, the system traverses eight predefined nested paths (such as ["params", "msg", "payload", "info", "total_token_usage"]) to locate the absolute token usage struct.

Why does Symphony prefer absolute totals over delta values for token accounting?

Absolute totals eliminate cumulative drift errors that accumulate when delta values are summed over long-running threads. By using the cumulative total_token_usage as the source of truth and computing deltas only through subtraction against stored snapshots, Symphony ensures that network retries, out-of-order events, or duplicate payloads cannot inflate usage counts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →