Back to Openclaw

Usage tracking

docs/concepts/usage-tracking.md

2026.6.811.5 KB
Original Source

What it is

  • Pulls provider usage/quota directly from their usage endpoints.
  • No estimated costs; only provider-reported quota windows or account-state summaries.
  • Human-readable quota-window status output is normalized to X% left, even when an upstream API reports consumed quota, remaining quota, or only raw counts. Providers without resettable quota windows can show provider summary text instead, such as a balance.
  • Session-level /status and session_status can fall back to the latest transcript usage entry when the live session snapshot is sparse. 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. Existing nonzero live values still win.

Where it shows up

  • /status in chats: emoji-rich status card with session tokens + estimated cost (API key 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 (OAuth shows tokens only).
  • /usage cost in chats: local cost summary aggregated from OpenClaw session logs.
  • CLI: openclaw status --usage prints a full per-provider breakdown.
  • CLI: openclaw channels list prints the same usage snapshot alongside provider config (use --no-usage to skip).
  • macOS menu bar: "Usage" section under Context (only if available).

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

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:

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

Missing or empty templates fall back to the built-in footer quietly. Unreadable or invalid configured templates 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:

jsonc
{
  "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": "usage.has_split_tokens",
        "text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
      },
      { "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
      { "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
      { "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": "usage.has_split_tokens",
          "text": " ↕️ {usage.input_tokens|num|?}/{usage.output_tokens|num|?}",
        },
        { "when": "usage.has_total_only_tokens", "text": " ↕️ {usage.total_tokens|num}" },
        { "when": "usage.cache_hit_pct", "text": " 🗄 {usage.cache_hit_pct|pct}" },
        { "when": "cost.turn_usd", "text": " 💰{cost.turn_usd|fixed:4}" },
      ],
    },
  },
}

Shape

jsonc
{
  "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).

PathMeaning
surfacechannel id (discord/telegram/etc.)
model.provider / model.display_nameprovider id / model id
model.reasoningeffort (off through xhigh)
model.is_fallback / model.is_overridebool: fallback used / model pinned
state.fast_modebool: fast vs slow
context.max_tokens / context.pct_usedwindow budget / 0-100 used
usage.input_tokens / usage.output_tokens / usage.total_tokensturn aggregate
usage.has_split_tokens / usage.has_total_only_tokens / usage.cache_hit_pcttoken display guards and cache percent
usage.last.input_tokens / usage.last.output_tokens / usage.last.cache_hit_pctfinal model call only
cost.turn_usdestimated turn cost
identity.name / identity.emojiagent name / chosen emoji

(Provider rate-limit windows are not in this contract.)

Verbs

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

VerbEffectExample
numcompact count272000 -> 272k
fixed:NN decimals (default 2)0.0377
durseconds to duration14820 -> 4h07m
pctappend %96 -> 96%
inv100 - xfor used to remaining
alias:TABLElookup in aliases, echo if unlistedmedium -> 🌗
meter:W:SCALEW-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.
  • { "each": "limits.windows", "item": "{label}" }: iterate an array.

Example

jsonc
{
  "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

  • Anthropic (Claude): OAuth tokens in auth profiles.
  • GitHub Copilot: OAuth tokens in auth profiles.
  • Gemini CLI: OAuth tokens in auth profiles.
    • JSON usage falls back to stats; stats.cached is normalized into cacheRead.
  • OpenAI Codex: OAuth tokens in auth profiles (accountId used when present).
  • 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.
    • Coding-plan 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.
  • Xiaomi MiMo: API key via env/config/auth store (XIAOMI_API_KEY).
  • z.ai: API key via env/config/auth store.
  • DeepSeek: API key via env/config/auth store (DEEPSEEK_API_KEY). OpenClaw calls DeepSeek's balance endpoint and shows the provider-reported balance as text instead of a percent-left quota window.

Usage is hidden when no usable provider usage auth can be resolved. Providers can supply plugin-specific usage auth logic; otherwise OpenClaw falls back to matching OAuth/API-key credentials from auth profiles, environment variables, or config.