What it is

Where it shows up

Since v2026.5.7, apus channels list no longer prints provider usage; it points users to apus status or apus models list instead.

/usage cost warns that the Today and Last 30d totals may be incomplete if their aggregate cache is refreshing, partial, or stale, and suggests running the command again later. The Session total is loaded separately. The CLI apus gateway usage-cost also reports the recorded cache state before its totals.

The Control UI checks incomplete usage totals again after 5, 10, and 20 seconds. These spaced checks give large histories time to load while keeping refresh traffic bounded. Until a cold cache has any usage data, the page shows a loading placeholder instead of zero totals. Available partial totals stay visible; if automatic checks finish without complete data, select Refresh to try again.

Session reports preserve agent attribution when agents share a session store. A background usage refresh does not invalidate a selected report when its session store is unchanged.

Usage opens with the last 30 calendar days selected. Today, 7d, 30d, 90d, 1y, All, or the date inputs change the reporting range. Historical lineage includes retained earlier instances of a session; the date range still controls which activity appears in the chart. Totals and daily charts come from the same session report, including sessions beyond the visible list limit.

A recorded zero-dollar cost is valid cost data. The average-cost hint warns about missing prices only when the selected report contains unpriced usage; filtering to sessions with known zero cost clears that warning.

Started by groups usage by the recorded session creator. Select an identity to filter the full report, including its history and totals. Human profiles, agents, and system-created sessions remain distinct; historical sessions without a recorded creator appear as Unattributed. This attributes the whole session to its creator, not individual turns to participants or charges to provider API accounts. Current account settings are not used to guess historical attribution.

Selecting chart days narrows creator totals and session counts across the full report, including sessions beyond the visible list limit. A session active on several selected days counts once. Session, text, and hour filters use the loaded session rows instead.

Usage date ranges

The Gateway methods usage.cost and sessions.usage interpret date ranges in UTC by default. In mode: "specific", use a valid IANA timeZone, such as Europe/Vienna, to follow local calendar days and daylight saving changes. It takes precedence over the legacy utcOffset field.

Without a timeZone, a fixed utcOffset must be between UTC-12:00 and UTC+14:00, inclusive (for example, UTC+5:30). An invalid, nonblank offset returns INVALID_REQUEST instead of silently using UTC. Omitting both fields uses UTC. usage.cost also treats a blank offset as omitted; sessions.usage requires any supplied offset to match the UTC offset format, so omit the field instead of sending a blank value.

Anthropic and OpenAI cost history

Subscription quota and API billing are different provider surfaces:

Admin credentials take precedence because they provide actual organization billing. Apus does not combine these provider-reported totals with its local session estimates; the two sections intentionally answer different questions.

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:

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

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 renders a plain Usage: X in / Y out line with cache counters when available. Missing input or output counts stay ?; Apus does not infer the split from a total. When neither direction is reported, a known total appears as Usage: 1.3k total. Cache counters remain visible even when input, output, and total counts are unavailable. This mode never estimates cost. 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": "~/.apus/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": "apus.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": "apus.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 (0..100, 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)

fixed:N accepts only a complete decimal integer from 0 through 100. Invalid precision arguments make that interpolation empty.

meter:W:SCALE accepts only a complete decimal integer width from 1 through 100. Leave the width blank to use the default 5 (meter::braille); invalid widths make that interpolation empty.

Piece forms

Example

{
  "schema": "apus.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. Apus 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.

Related