Files
openclaw-openclaw/docs/concepts/usage-tracking.md
Peter Steinberger 07e2d633cc feat: show auto-detected provider plans and billing (#100520)
* feat(providers): auto-discover usage billing

* feat(ui): show provider plans and billing

* fix(ui): translate provider billing labels
2026-07-06 04:53:09 +01:00

19 KiB

summary, read_when, title
summary read_when title
Usage tracking surfaces and credential requirements
You are wiring provider usage/quota surfaces
You need to explain usage tracking behavior or auth requirements
Usage tracking

What it is

  • Pulls provider usage/quota directly from each provider's usage endpoint. No estimated provider billing; only provider-reported plan names, quota windows, balances, spend, budgets, or account-state summaries.
  • Human-readable quota-window output is normalized to X% left, even when a provider reports consumed quota, remaining quota, or only raw counts. Providers without resettable quota windows show provider summary text instead (for example a balance).
  • Session-level /status and the session_status tool fall back to the session's transcript log when the live session snapshot is missing token/model data. That fallback fills missing token/cache counters, can recover the active runtime model label, and prefers the larger prompt-oriented total when session metadata is missing or smaller (totalTokensFresh !== true, zero, or below the transcript-derived value). Nonzero live values always win over the fallback.

Where it shows up

  • /status in chats: status card with session tokens and estimated cost (API key models only). Provider usage shows for the current model provider when available, as a normalized X% left window or provider summary text.
  • /usage off|tokens|full in chats: per-response usage footer.
  • /usage cost in chats: local cost summary aggregated from OpenClaw session logs.
  • CLI: openclaw status --usage prints a full per-provider usage/quota breakdown.
  • CLI: openclaw models status lists OAuth/token auth profiles and shows a usage-window summary next to each provider that has one.
  • Control UI: Usage shows provider plan and billing cards above OpenClaw's session-derived token and estimated-cost analysis.
  • macOS menu bar: a root "Usage" section appears below Context when provider usage snapshots are available. See Menu bar.

openclaw channels list no longer prints provider usage; it points users to openclaw status or openclaw models list instead.

/usage off|tokens|full sets the footer for a session and is remembered for that session. messages.responseUsage seeds that mode for sessions that have not chosen one, so the footer can be on by default without typing /usage each time.

Set one mode for every channel, or a per-channel map with a default fallback:

{
  "messages": {
    "responseUsage": "tokens",
    // or: { "default": "off", "discord": "full" }
  },
}

Accepted values: "off", "tokens", "full", and the legacy alias "on" (treated as "tokens").

Three distinct session states

A session's responseUsage field has three representable states, each with different semantics:

State Stored value Effective mode
Unset / inherit undefined (absent) Falls through to messages.responseUsage config default, then off.
Explicit off "off" (stored) Always off, a non-off config default cannot re-enable the footer.
Explicit on "tokens" or "full" (stored) That mode, regardless of config default.

Precedence

Effective mode = session override → channel config entry → defaultoff.

An explicit /usage off is persisted as the literal value "off" in the session, not the same as "unset." A non-off messages.responseUsage default cannot turn the footer back on once the user has explicitly disabled it.

Resetting vs. turning off

  • /usage off forces the footer off and persists that choice. A configured non-off default cannot override this.
  • /usage reset (aliases: default, inherit, inherited, clear, unpin) clears the session override. The session then inherits the effective config default (messages.responseUsage). If no default is configured, the footer stays off.
  • A full session reset (/reset or /new) or a session rollover preserves the explicit usage-mode preference so the user's display choice survives session rollovers. Only /usage reset (and its aliases) clears the override.

Toggle behavior

/usage with no arguments cycles: off → tokens → full → off. The starting point for the cycle is the effective current mode (session override falling through to the config default when unset), so the cycle always matches what the user currently sees in the footer.

Config

With no config the prior behavior holds (footer off until /usage). Use /usage reset to clear a session override and re-inherit the configured default.

/usage tokens always renders a plain Usage: X in / Y out line (plus cache and estimated-cost suffixes when available). Only /usage full renders the richer footer described below.

/usage full shows a built-in compact footer with model, reasoning, fast/slow, context window, and cost when those fields are available. No template file is required for the built-in footer.

messages.usageTemplate is only for advanced custom layouts. The value is a JSON file path (supports ~) or an inline object, and it replaces the built-in footer when valid. A file path is watched and reloaded live on change.

{
  "messages": {
    "usageTemplate": "~/.openclaw/usage-footer.json"
  }
}

Missing or empty templates fall back to the built-in footer quietly. Unreadable or invalid configured templates (bad JSON, or a shape with no renderable output pieces) also fall back to the built-in footer and emit an operator warning.

Start custom templates from the built-in shape, then edit the parts you want to change:

{
  "schema": "openclaw.usageBar.v1",
  "scales": {
    "braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿",
    "block": "░▏▎▍▌▋▊▉█",
    "shade": "░▒▓█",
    "moon": "🌑🌘🌗🌖🌕",
    "level": "▁▂▃▄▅▆▇█",
    "weather": ["🥶", "☁️", "🌥", "⛅️", "🌤", "☀️"],
    "plants": ["🪾", "🍂", "🌱", "☘️", "🍀", "🌿"],
    "moons6": ["🌑", "🌚", "🌘", "🌗", "🌖", "🌝"],
  },
  "aliases": {
    "models": {
      "claude-opus-4-6": "opus46",
      "claude-opus-4-8": "opus48",
      "claude-sonnet-4-6": "sonnet46",
      "claude-haiku-4-5": "haiku45",
      "gpt-5.5": "gpt5.5",
    },
    "reasoning": {
      "off": "🌑",
      "minimal": "🌚",
      "low": "🌘",
      "medium": "🌗",
      "high": "🌕",
      "xhigh": "🌝",
    },
  },
  "output": {
    "sep": "",
    "default": [
      { "text": "{model.provider}{identity.emoji|🤖}{model.display_name|alias:models}" },
      { "map": "model.is_fallback", "cases": { "true": "🔄" } },
      { "map": "model.is_override", "cases": { "true": "📌" } },
      { "when": "model.reasoning", "text": "{model.reasoning|alias:reasoning}" },
      { "map": "state.fast_mode", "cases": { "true": "⚡️", "false": "🐌" } },
      {
        "when": "context.max_tokens",
        "text": " | 📚[{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
      },
      { "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
    ],
    "surfaces": {
      "discord": [
        { "text": "-# -\n" },
        { "text": "-# {model.provider}{identity.emoji|🤖}{model.display_name|alias:models}" },
        { "map": "model.is_fallback", "cases": { "true": "🔄" } },
        { "map": "model.is_override", "cases": { "true": "📌" } },
        { "when": "model.reasoning", "text": "{model.reasoning|alias:reasoning}" },
        { "map": "state.fast_mode", "cases": { "true": "⚡️", "false": "🐌" } },
        {
          "when": "context.max_tokens",
          "text": " | 📚[{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
        },
        { "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
      ],
    },
  },
}

Shape

{
  "schema": "openclaw.usageBar.v1",
  "scales": { "<name>": "low-to-high glyphs" }, // string (1 glyph/char) or array
  "aliases": { "<table>": { "<value>": "<label>" } },
  "output": {
    "sep": "", // joins surviving pieces
    "default": [
      /* pieces */
    ], // fallback for any surface
    "surfaces": {
      "discord": [
        /* pieces */
      ],
      "telegram": [
        /* pieces */
      ],
    },
  },
}

Each surface is an ordered list of pieces; the engine renders each, drops empties, and joins survivors with sep. A surface with no entry uses output.default.

Contract Paths

A piece reads values from the per-turn contract by dot-path. Absent values are empty (so a when guard or a |fallback keeps the piece clean).

Path Meaning
surface channel id (discord/telegram/etc.)
agentId / chat_type owning agent id / chat surface kind
model.id / model.display_name / model.provider model id / display name / provider id
model.actual, model.resolved_ref provider/model ref actually used for the turn
model.requested provider/model ref requested (before fallback)
model.reasoning effort (off through xhigh)
model.is_fallback / model.is_override bool: fallback used / model pinned
model.override_source / model.auth_mode override source label / credential mode (oauth, api-key, token, mixed, aws-sdk, unknown)
state.fast_mode bool: fast vs slow
state.compactions compaction count for the session
context.max_tokens / context.used_tokens / context.pct_used window budget / occupied tokens / 0-100 used
usage.input_tokens / usage.output_tokens / usage.total_tokens turn aggregate
usage.cache_read_tokens / usage.cache_write_tokens cache-read and cache-write tokens for the turn
usage.has_tokens / usage.has_split_tokens / usage.has_total_only_tokens token display guards
usage.cache_hit_pct cache-read share of total prompt tokens
usage.last.input_tokens / usage.last.output_tokens / usage.last.cache_hit_pct final model call only (also has cache_read_tokens, cache_write_tokens, total_tokens)
cost.turn_usd / cost.available estimated turn cost / whether a cost table resolved
timing.duration_ms wall-clock turn duration
identity.name / identity.emoji / identity.avatar agent identity name / emoji / avatar
session.id session id

(Provider rate-limit windows are not in this contract; there is no array-valued path today, so an each piece has nothing to iterate.)

Verbs

Pipe a value through verbs left to right; a non-verb segment is the fallback.

Verb Effect Example
num compact count 272000 -> 272k
fixed:N N decimals (default 2) 0.0377
dur seconds to duration 14820 -> 4h07m
pct append % 96 -> 96%
inv 100 - x for used to remaining
alias:TABLE lookup in aliases, echo if unlisted medium -> 🌗
meter:W:SCALE W-cell glyph bar over a 0-100 value [⣿⣿⠐⠐⠐] (meter:1 = one glyph)

Piece forms

  • { "text": "📚 {context.max_tokens|num}" }: literal + interpolation.
  • { "when": "<path>", "text": "..." }: render only if the path is truthy.
  • { "map": "<path>", "cases": { "true": "⚡", "false": "🐌" } }: value to glyph (a _default case covers unmatched values).
  • { "each": "<array-path>", "item": "{label}" }: iterate an array-valued path (no current contract path is an array).

Example

{
  "schema": "openclaw.usageBar.v1",
  "scales": { "braille": "⠐⡀⡄⡆⡇⣇⣧⣷⣿" },
  "aliases": { "reasoning": { "medium": "🌗", "high": "🌕" } },
  "output": {
    "surfaces": {
      "discord": [
        { "text": "{model.display_name}" },
        { "when": "model.reasoning", "text": " {model.reasoning|alias:reasoning}" },
        { "map": "state.fast_mode", "cases": { "true": " ⚡", "false": " 🐌" } },
        {
          "when": "context.max_tokens",
          "text": " | 📚 [{context.pct_used|meter:5:braille}]{context.max_tokens|num}",
        },
      ],
    },
  },
}

renders e.g. claude-sonnet-4-6 🌗 🐌 | 📚 [⣿⣿⣿⣿⣧]272k.

Providers + credentials

Usage is hidden when no usable provider usage auth can be resolved. OpenClaw automatically discovers enabled provider plugins that declare contracts.usageProviders and implement both resolveUsageAuth and fetchUsageSnapshot; there is no separate core provider allowlist. The static contract keeps discovery scoped without importing every provider plugin. Each plugin owns its upstream endpoint and response mapping. The shared snapshot keeps plan names, quota windows, balances, spend, and budgets provider-neutral for CLI, app, and Control UI consumers.

  • Anthropic (Claude): OAuth tokens in auth profiles. If the OAuth token lacks user:profile scope, falls back to a claude.ai web session (CLAUDE_AI_SESSION_KEY, CLAUDE_WEB_SESSION_KEY, or a sessionKey= cookie in CLAUDE_WEB_COOKIE) when set. Model-scoped limits and enabled extra-usage monthly spend/budgets are included when Anthropic reports them.
  • ClawRouter: API key (CLAWROUTER_API_KEY). Shows a monthly budget window and typed USD budget when configured; otherwise shows aggregate spend and a request/token/cost summary.
  • DeepSeek: API key via env/config/auth store (DEEPSEEK_API_KEY). Shows each provider-reported currency balance.
  • GitHub Copilot: OAuth tokens in auth profiles.
  • Gemini CLI: OAuth tokens in auth profiles.
  • MiniMax: API key or MiniMax OAuth auth profile. OpenClaw treats minimax, minimax-cn, and minimax-portal as the same MiniMax quota surface, prefers stored MiniMax OAuth when present, and otherwise falls back to MINIMAX_CODE_PLAN_KEY, MINIMAX_CODING_API_KEY, or MINIMAX_API_KEY. Usage polling derives the Coding Plan host from models.providers.minimax-portal.baseUrl or models.providers.minimax.baseUrl when configured, and otherwise uses the MiniMax CN host. MiniMax's raw usage_percent / usagePercent fields mean remaining quota, so OpenClaw inverts them before display; count-based fields win when present.
    • Window labels come from provider hours/minutes fields when present, then fall back to the start_time / end_time span.
    • If the coding-plan endpoint returns model_remains, OpenClaw prefers the chat-model entry, derives the window label from timestamps when explicit window_hours / window_minutes fields are absent, and includes the model name in the plan label.
  • OpenAI (Codex/ChatGPT plan): OAuth tokens in auth profiles (ChatGPT-Account-Id header sent when an account id is present). Shows the ChatGPT plan, resettable Codex windows, and a credit balance when reported. Credits remain provider credits; OpenClaw does not label them as dollars. API-key-only OpenAI usage is not tracked.
  • OpenRouter: API key or OAuth-backed API key (OPENROUTER_API_KEY or an auth profile). Combines the account credits endpoint with the key quota endpoint, so account balance/spend, key budget, and daily/weekly/monthly usage appear when the credential can access them. Either endpoint can enrich the snapshot independently.
  • Venice: API key via env/config/auth store (VENICE_API_KEY). Shows USD and DIEM balances plus DIEM epoch allocation usage when reported.
  • Xiaomi MiMo: two separate usage surfaces. Pay-as-you-go uses an API key (XIAOMI_API_KEY); the Token Plan uses a separate key (XIAOMI_TOKEN_PLAN_API_KEY). Neither currently reports quota windows.
  • z.ai: API key via env/config/auth store (ZAI_API_KEY or Z_AI_API_KEY).