Token Usage Log

August 8, 2026 · View on GitHub

LLxprt Code writes a per-session JSONL file that records token usage for every turn and significant lifecycle event. You can use this file to analyze token consumption, audit cost, and correlate spending with specific conversation turns.

Where the file lives

<projectTempDir>/token-usage/<sessionId>.jsonl

The <projectTempDir> is the .llxprt/tmp directory for the current project (see Application Directories). The <sessionId> is the unique session identifier (for example a1b2c3d4-e5f6-...). Each line in the file is a self-contained JSON object.

Enabling and disabling

Token usage logging is controlled by the token-usage-log setting.

PropertyValue
ScopePer-profile or global settings
PersistencePersists to settings.json
Defaulttrue (enabled)
Valid valuestrue, false

To disable logging, add the following to your user settings or workspace .llxprt/settings.json:

{
  "token-usage-log": false
}

When disabled, no file is created and no records are written.

Record types

Every record carries a record_type discriminator and a schema_version. Records written before schema versioning (no schema_version and no record_type) are read back as version 0 turn records, so existing files remain readable.

record_typePurpose
turnOne billed API request within a conversation turn
compressionA context-compression event that reduced the token count
provider_switchA different provider began serving the session
model_switchA different model began serving the session
session_resumeReserved for session resume events
context_truncationReserved for context truncation events

Note: session_resume and context_truncation are defined in the schema and their serialization is fully tested, but they are not emitted from production code. See Lifecycle records for why.

Turn record fields

Each turn record captures the token economics of a single billed API request. Fields are grouped by purpose.

Identity and join keys

These fields let you connect a token-usage record to the corresponding turn in the conversation log.

FieldTypeDescription
prompt_idstringUnique identifier for the prompt that initiated this turn. Joins to metadata.promptId on the recorded conversation content.
session_idstringThe session identifier. Matches the filename.
turn_idstring | nullStable identifier for the turn this request served. Minted before the request is sent and stamped on the persisted turn, so the two always agree.
user_turnnumber | nullUser-turn index of the conversation state this request was built from — the newest turn already in history, not the turn being sent. null on the first request of a session. See the caution below.
stepnumber | nullStep index within that same prior user turn. Same caveat as user_turn.
runtime_idstringThe runtime identifier for the agent that served this request.
parent_runtime_idstring | nullThe parent runtime's identifier for subagent requests; null for the main agent.
subagent_namestring | nullThe subagent name; null for the main agent.

Estimator and calibration

The 13 fields below, together with ts, prompt_id, provider and model from the identity section above, are the original 17 token-estimation columns. Their names and meanings are stable and will not change.

FieldTypeDescription
estimated_tokensnumberThe pre-send token estimate from the configured estimator.
estimatorstringWhich estimator was used (openai-tiktoken, anthropic-char, core-fallback).
estimator_methodstringEstimation method (exact or calibrated). Omitted when not applicable.
estimator_familystringModel family the estimator is calibrated for. Omitted when not applicable.
estimator_versionstringEstimator asset version. Omitted when not applicable.
asset_revisionstringCalibration asset revision. Omitted when not applicable.
projection_revisionnumberProjection revision number. Omitted when not applicable.
protocolstringProvider protocol (for example openai-chat). Omitted when not applicable.
tiktoken_tokensnumber | nullThe tiktoken-based token count measured at send time. null when tiktoken was not available.
tiktoken_estimation_failedbooleanWhether the tiktoken measurement failed.
actual_prompt_tokensnumberThe actual prompt token count reported by the provider.
cached_tokensnumberThe cached token count reported by the provider (legacy name, retained for compatibility).
effective_actual_tokensnumberactual_prompt_tokens - cached_tokens, clamped to a minimum of 0.

Cost

These fields are present only when the provider reports them. Fields the provider does not report are omitted — zero and "not reported" are distinguishable.

FieldTypeDescription
output_tokensnumberOutput (completion) tokens billed.
reasoning_tokensnumberReasoning/thinking tokens billed.
cache_write_tokensnumberTokens written to the provider cache on this request.
cache_read_tokensnumberTokens read from the provider cache on this request.
tool_tokensnumberTool-related tokens billed by the provider.
total_tokensnumberTotal tokens for this request as reported by the provider.

Attempt

A single logical turn can produce multiple billed attempts (retries). Each attempt gets its own record.

FieldTypeDescription
attempt_indexnumber0-based index within the logical turn.
attempt_outcomestringsuccess, error, aborted, or abandoned.
retry_reasonstringWhy a retry was triggered, when known. Omitted when not applicable.
http_statusnumberHTTP status code from the response, when available. Omitted when not applicable.
backend_profilestringThe load-balancer sub-profile that served this request. Omitted when not applicable.

Tool attribution

These fields attribute token cost to tool results present in the request.

FieldTypeDescription
tool_callsarrayOne entry per tool result in this request: { call_id, tool_name, result_tokens, was_truncated }. Only identifiers and counts — never the result body or arguments.
new_tool_result_tokensnumberTotal tokens from tool results entering the prompt for the first time on this send.
carried_tool_result_tokensnumberTotal tokens from tool results already present in a previous send.

Request shape

These fields describe the composition of the request prefix, measured at the agents-layer send seam.

These are real token counts, produced by the same tiktoken-based estimator the rest of the codebase uses — not character approximations. Each content is tokenized once per session rather than once per request: measurements are cached by the stable content id, falling back to the tool call_id for tool results (whose content id the history pipeline does not preserve). A tool result carried across sixty turns is therefore measured once, not sixty times.

Two caveats worth knowing:

  • They are local estimates, not provider billing. Use actual_prompt_tokens for what you were actually charged; the buckets exist to apportion it.
  • They will not sum exactly to actual_prompt_tokens, because providers add their own wire framing and because the estimator is not the provider's tokenizer. Treat the split as attribution, not reconciliation.
FieldTypeDescription
instructions_tokensnumberToken count of the system instruction.
tools_schema_tokensnumberToken count of the tool schemas.
history_tokensnumberToken count of the conversation history, including tool calls and tool results (which are conversation, not injections). Excludes media and injected content.
media_tokensnumberToken count of media blocks (images, audio).
injected_tokensnumberToken count of genuinely injected content — synthetic content that carries no tool or media blocks. Note that metadata.synthetic alone is not the test: the provider pipeline stamps it on tool turns it merely re-ordered, so buckets are decided structurally first (media, then tool, then synthetic).
prefix_fingerprintstringFirst 16 hex characters of the SHA-256 of a stable serialization of the request prefix: the instructions and tool schemas in full, then the leading history identified by content id. Bounded on purpose — the prefix is the part a provider can cache, so a change there breaks the cache while a change at the tail cannot. Detects the changes that defeat caching (edited instructions, changed tool schemas, a head rewritten by compression, reordered or dropped turns). See the privacy section for what this does and does not guarantee.
prefix_fingerprint_changedboolean | nullWhether the fingerprint differs from the previous send in the same session. null on the first send.

Lifecycle records

Beyond turn records, the log carries typed lifecycle records that mark discontinuities in the token burn curve. Each carries session_id and, when known, turn_id.

Compression

Emitted once per completed compression. Without this record, a compression appears as an unexplained drop in the token count.

FieldTypeDescription
record_type"compression"Discriminator.
schema_versionnumberSchema version.
tsstringISO timestamp.
session_idstringSession identifier.
turn_idstring | nullTurn identifier at compression time, when known.
tokens_beforenumberTotal token count before compression.
tokens_afternumberTotal token count after compression.
compression_modelstring | nullThe model that served the compression.
compression_providerstring | nullThe provider that served the compression.
compression_prompt_tokensnumberThe compression call's own prompt token usage. Omitted when not reported.
compression_output_tokensnumberThe compression call's own output token usage. Omitted when not reported.

Provider switch and model switch

A switch is recorded by observing which provider and model actually served each request, not by watching the settings mutation that requested the change. The settings layer that initiates a switch has no path to the per-session logger, and observation at the send seam records the change that actually affected billing — which is the question this log exists to answer.

A provider change is written as provider_switch (it necessarily carries a model change too); a model change under the same provider is written as model_switch. Each switch is written once, not once per subsequent request, and the first request of a session is never a switch.

FieldMeaning
from_provider / to_providerProvider before and after the switch
from_model / to_modelModel before and after the switch
providerOn model_switch, the unchanged provider

Session resume and context truncation

These record types are defined in the schema and their serialization and round-trip parsing are fully tested, but they are not currently emitted from production code:

  • session_resume: Session resume flows through the session-control layer, which restores history without touching the token-usage logger. Reaching the logger would require threading it through the client contract across package boundaries.

  • context_truncation: In LLxprt Code, context truncation is performed by the compression pipeline (the top-down-truncation fallback strategy). It is already captured by the compression record. Emitting a separate context_truncation record would duplicate the same event.

When these records are wired in the future, their schemas will not change.

Joining to conversation turns

The token-usage log and the conversation log live in the same project temp directory but in different subdirectories:

<projectTempDir>/token-usage/<sessionId>.jsonl   ← token usage
<projectTempDir>/chats/session-<timestamp>-<prefix>.jsonl  ← conversation

The conversation filename embeds only the first 12 characters of the session id, so to go from a token-usage record to its recording: match that prefix against session_id, then confirm the file's session_start record has payload.sessionId equal to the full session_id.

Join keys

Token-usage fieldConversation content fieldMeaning
prompt_idmetadata.promptIdLinks a billed request to the turn it served
turn_idmetadata.turnIdLinks to the stable turn identifier
session_idfilename prefix, confirmed against session_start.payload.sessionIdScopes both streams to one session

Caution: join on prompt_id or turn_id. Do not join on user_turn/step. The token-usage record is written before the turn it describes reaches history, so those two fields describe the conversation state the request was built from — the preceding turn — rather than the turn being billed. They are useful for ordering and for seeing how much conversation preceded a request; they are not a turn identity.

Worked example

Suppose the token-usage file contains:

{"record_type":"turn","prompt_id":"p-abc123","session_id":"sess-001","turn_id":"t-42","user_turn":3,"step":1,"estimated_tokens":1200,"actual_prompt_tokens":1150,...}

To find the corresponding turn in the conversation log:

  1. Open the session-*.jsonl file in <projectTempDir>/chats/ for session sess-001.
  2. Search for a content entry where metadata.promptId === "p-abc123".
  3. Alternatively, search for a content entry where metadata.turnId === "t-42". Both keys resolve to the same turn.

To go the other direction — from a conversation turn to its cost:

  1. Read the metadata.promptId from the conversation content entry.
  2. Search the token-usage file for the matching prompt_id.

Schema versioning

schema_versionDescription
0Pre-versioning records. No schema_version or record_type field was written. The reader normalizes these to schema_version: 0, record_type: "turn".
1Current schema. Every record carries schema_version: 1 and a record_type discriminator.

The tolerant reader (parseTokenUsageLogRecord) accepts records with or without versioning. A record with neither schema_version nor record_type is treated as a version-0 turn record. This ensures existing log files remain readable after an upgrade.

Privacy posture

The token-usage log contains counts, identifiers, and hashes only. It explicitly does not contain:

  • Prompt text or user messages
  • Model output or AI responses
  • Tool arguments or parameters
  • Tool result bodies

The prefix_fingerprint field is the first 16 hex characters of the SHA-256 of a stable serialization of the system instruction, tool schemas and conversation history. It exists solely to detect when the request prefix changes between sends.

It stores no prompt content, and SHA-256 is not invertible. It is not a confidentiality guarantee: anyone who can guess a candidate prefix can hash it and compare, so a low-entropy or already-known prefix can be confirmed. Treat a fingerprint as an equality token for prefixes you already hold, not as a secret about prefixes you do not. Truncation to 64 bits also makes collisions possible in a large enough corpus.

Fields deliberately not populated

FieldReason
prompt_cache_keyDeclared in the schema but never written today. The key is derived and sanitized inside the provider executors, below the seam that writes this log, so a value produced here could differ from the one actually sent. Recording the real key means surfacing it from the transport layer.

Future work

Consuming surfaces — such as a /stats burn CLI view or an offline cross-session analysis script — are planned as separate work. This document covers the data format and the join model that those surfaces will build on.