Usage

Privacy-safe call log for tracking spend.

Marmot keeps a privacy-safe log of every metered call so you can answer "what did I run this week and how much did it cost?" without enabling any per-provider response cache or binding sessions. The log records call shape and outcome but never prompt text, query strings, or person identifiers.

marmot usage                    # last 7 days, grouped by provider
marmot usage --since 24h        # last 24h
marmot usage --by verb          # grouped by verb
marmot usage --provider parallel
marmot usage --failed-only
marmot usage --json
marmot usage prune --older-than 90d

What's recorded

One JSON record per call, written to ~/.marmot/usage/<UTC-DATE>.jsonl. Schema:

FieldTypeNotes
request_idstring (UUID)Unique per request. For async work, equals the provider's task id so submit/poll/completion records can be joined. (Pre-0.6.0 records used call_id; the reader aliases the legacy field automatically.)
tsISO 8601Wall-clock when the call returned.
verbstringsearch, scrape, run, enrich, etc.
providerstringProvider slug.
modelstring | undefinedPresent for AI verbs.
presetstring | undefinedPreset NAME (never contents).
flagsobject | undefinedNon-sensitive flag values: limit, depth, freshness, format, type, etc.
flag_presenceobject | undefinedSensitive flags as boolean presence only: includeDomains, email, linkedin, schema, etc.
cachedbooleanTrue when served from local response cache.
duration_msintegerWall-clock duration.
quantityobject | undefinedVerb-shaped counts. AI: tokens_input, tokens_output, tokens_cache_read, tokens_cache_write. Web: results, pages, urls, entities, citations.
costnumber | null | undefinedUSD cost when the provider reports it (OpenRouter, AI Gateway). null otherwise.
exit'ok' | 'error'Call outcome.
error_categorystring | undefinedPresent on error: validation, provider, auth, cache, io.
sessionstring | nullSession name when bound.
sensitiveobject | undefinedOpt-in audit payload. Populated only when logging.recordSensitive: true is configured (or MARMOT_RECORD_SENSITIVE=1 is set). Contains the actual prompt, query, target URLs, and identifier values that flag_presence flags as present. NEVER recorded by default. See Sensitive recording below.

What's NOT recorded by default

Never written unless you explicitly opt in: prompt text, query text, system prompt, schema bodies, --include-domains and --exclude-domains values, all enrich/lookup/verify identifiers (--email, --linkedin, --phone, --first-name, etc.), API keys.

Reading the log

marmot usage                                # default 7 days, by provider
marmot usage --since 1h                     # last hour
marmot usage --since 24h                    # last 24h
marmot usage --since 30d                    # last 30 days
marmot usage --from 2026-05-01 --to 2026-05-06   # custom range
marmot usage --by verb                      # group by verb
marmot usage --by day                       # per-day breakdown
marmot usage --by model                     # per-model (AI verbs)
marmot usage --provider parallel            # filter to one
marmot usage --verb search                  # filter to one
marmot usage --failed-only                  # only error rows
marmot usage --json                         # structured envelope

--since accepts a positive integer plus h, d, or w: 1h, 24h, 7d, 4w. Composable: marmot usage --since 30d --by verb --provider openrouter --json returns 30 days of OpenRouter calls grouped by verb as JSON.

What you get

Every grouping (by provider / verb / day / model) reports:

  • requests, errors, errorRate, cached, cacheHitRate (top-level totals only — rows omit errorRate / cacheHitRate)
  • durationTotalMs, durationAvgMs, durationP50Ms, durationP95Ms
  • costTotal, costAvgUsd (over requestsWithCost), requestsWithoutCost
  • quantityTotals — sum of every numeric child key

The human-formatted output renders all timestamps in your local timezone. Storage stays UTC (one file per UTC day) so file boundaries are stable. The window header adapts to the range:

  • Sub-day windows show time-of-day on both sides — Usage — last 1h (May 6 09:14 to 10:14).
  • Multi-day windows with --since echo the duration token — Usage — last 7d (May 1 to May 8).
  • Explicit --from/--to show the range only — Usage — May 1 to May 8.

Cost is only summed where the provider reports it. The summary always discloses how many calls had cost data so you know your coverage.

Live tail — --watch

marmot usage --watch prints each new record as it lands in today's usage file. Useful while running a batch job in another shell or iterating on a script:

marmot usage --watch                              # human format, one record per line
marmot usage --watch --json                       # JSONL on stdout for piping
marmot usage --watch --provider openrouter        # filter live
marmot usage --watch --verb run --failed-only     # only failed run calls

Implementation notes:

  • Polls today's ~/.marmot/usage/<UTC-DATE>.jsonl every 500ms. Initial scan jumps to end-of-file so existing records aren't replayed — you only see what's new.
  • Filters (--provider, --verb, --failed-only) apply per record.
  • At UTC midnight the watcher swaps to the new day's file automatically; a one-line stderr note marks the rollover.
  • Ctrl-C exits cleanly.

Browse individual calls — marmot history

marmot history lists individual recent records (newest first) instead of aggregates. Useful when you need the timestamp / duration / outcome of a specific call without the rollup math:

marmot history                              # default last 10
marmot history --since 1h --limit 50        # last hour, up to 50
marmot history --provider parallel --json   # filter + structured envelope
marmot history --failed-only                # only error rows
FlagDescription
--since <duration>Time window: Nh, Nd, Nw (default 7d).
--from <YYYY-MM-DD>Window lower bound (overrides --since).
--to <YYYY-MM-DD>Window upper bound (inclusive).
--provider <slug>Filter to one provider.
--verb <name>Filter to one verb.
--failed-onlyOnly error records.
--limit <n>Max records to return (default 10, cap 1000).
--jsonEmit { ok, window, limit, count, records: [...] }.

Timestamps render in your local timezone. The preset_id field on each record is resolved to the current slug at render time, so renames don't break historical readability — if the preset has been deleted, the row falls back to (preset:<short-id>).

Pruning

marmot usage prune --older-than 90d

Deletes day files older than the cutoff. Returns the number of files deleted and bytes freed.

Disabling

Default: ON. Three precedence levels (highest first):

  1. Per call: marmot search "..." --no-log skips the record for this invocation only.
  2. Env var: MARMOT_NO_LOG=1 marmot search "..." does the same — useful for scripts.
  3. Globally: marmot config set logging.enabled false.

marmot doctor reports the current state.

Sensitive recording (opt-in)

The default log captures call shape and outcome but never the actual content (prompt, query, URLs, identifiers). For a full audit trail — the kind you'd want when reconstructing exactly what was asked of which provider — opt in:

marmot config set logging.recordSensitive true

Or per-call: MARMOT_RECORD_SENSITIVE=1 marmot search "...".

When opted in, every record gains a sensitive field with verb-appropriate contents:

VerbSensitive payload
run{ prompt, system?, schema? }
image / speak / video{ prompt, flags?: { negative | instructions } }
transcribe{ urls: [audioPath], flags?: { prompt } }
search / answer{ query, flags?: { includeDomains, excludeDomains, afterDate, beforeDate } }
scrape / map{ urls, flags?: { query | search } }
crawl{ urls: [url], flags?: { instructions, includePaths, excludePaths } }
research / findall{ query, schema?, flags?: { instructions | matchConditions } }
enrich / lookup / verify{ flags?: { email, linkedin, phone, name, firstName, lastName, q, title, ... } }

Per-call override: --redact

Even with recordSensitive on globally, you can redact a single call:

marmot search "this query is private" --redact

The record is still written (with metadata + flag_presence), but the sensitive field is omitted. Same effect: MARMOT_REDACT=1 marmot search "...".

Precedence (highest first):

  1. --redact flag or MARMOT_REDACT=1 — force OFF.
  2. MARMOT_RECORD_SENSITIVE=1 — force ON for this call.
  3. config.logging.recordSensitive — global default (false unless set).

Verb coverage

As of 0.5.0, usage logging is wired into all 15 verbs: AI (run, image, speak, transcribe, video), web (search, scrape, answer, map, crawl, research, findall), and data (enrich, lookup, verify). Async verbs (crawl, research, findall) use the provider's task id as request_id so submit / poll / completion records can be joined.

File format on disk

~/.marmot/usage/2026-05-06.jsonl
~/.marmot/usage/2026-05-07.jsonl
~/.marmot/usage/2026-05-08.jsonl

One UTC day per file, append-only. JSONL — one record per line. Files are mode 0600, directory 0700.