Files
openclaw/docs/concepts/usage-tracking.md
Peter Steinberger f7d7148cf0 docs: rewrite published docs grounded in current source (#100142)
Source-grounded rewrite of 529 published docs pages with per-unit information-loss verification: 1,713 factual corrections cited to src/**, generated surfaces regenerated, frontmatter titles preserved for i18n, release notes pages untouched. All docs gates green.

Closes #100141
2026-07-05 00:32:47 -04:00

329 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
summary: "Usage tracking surfaces and credential requirements"
read_when:
- You are wiring provider usage/quota surfaces
- You need to explain usage tracking behavior or auth requirements
title: "Usage tracking"
---
## What it is
- Pulls provider usage/quota directly from each provider's usage endpoint. No estimated costs; only provider-reported quota windows, balances, 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.
- macOS menu bar: a root "Usage" section appears below Context when provider usage snapshots are available. See [Menu bar](/platforms/mac/menu-bar).
`openclaw channels list` no longer prints provider usage; it points users to `openclaw status` or `openclaw models list` instead.
## Default usage footer mode
`/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:
```jsonc
{
"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 → `default``off`.
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.
## Custom `/usage full` footer
`/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.
```json
{
"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:
```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": "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
```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).
| 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
```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
Usage is hidden when no usable provider usage auth can be resolved. Providers
supply their own usage-fetch logic; when that is unavailable OpenClaw falls back
to matching OAuth/API-key credentials from auth profiles, environment variables,
or config.
- **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.
- **ClawRouter**: API key (`CLAWROUTER_API_KEY`). Shows a monthly budget window
when a budget is configured, otherwise a request/token/cost summary.
- **DeepSeek**: API key via env/config/auth store (`DEEPSEEK_API_KEY`).
Shows the provider-reported account balance as text instead of a percent-left
quota window.
- **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). API-key-only OpenAI usage is not tracked.
- **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`).
## Related
- [Token use and costs](/reference/token-use)
- [API usage and costs](/reference/api-usage-costs)
- [Prompt caching](/reference/prompt-caching)
- [Menu bar](/platforms/mac/menu-bar)