Local usage history

September 10, 2026 · View on GitHub

The web dashboard and macOS app's Status → Usage History show Today, Yesterday, 7/30/90 Days, and This Month, with model filtering, token totals, per-model generation speed, and a day/hour token heatmap. History starts when this version first serves requests; existing all-time totals cannot be backfilled.

Usage stays on this server. No telemetry is sent. Only the canonical oMLX model ID, hourly bucket, request/token counts, and accumulated durations are stored. There are no prompts, responses, messages, token IDs, API keys, headers, client IPs, upload names, or document contents. Model IDs are the same identifiers used by the engine pool and existing serving statistics, not display aliases or full model paths. Renaming an ID starts a new series; replacing weights under the same ID continues that series. Removed/unloaded models remain in history.

Accounting

History consumes the existing completed-request serving-statistics hook for OpenAI completions/chat/responses, Anthropic messages, embeddings, reranking, and audio. It inherits that hook's coverage: failed/rejected or disconnected requests that do not reach accounting are not counted. Local benchmark requests that use these serving endpoints also count. Audio contributes requests with zero tokens because byte/character counts are not token counts.

  • Prompt tokens: the engine's full input count, including cached tokens.
  • Output tokens: the engine's completion count, including generated reasoning and tool-call tokens counted by the engine.
  • Total tokens: prompt + output; cached tokens are already part of prompt.
  • Cached tokens: the engine-reported prompt tokens actually reused from KV prefix cache, after reconstruction, alignment, trimming, and fallback. This includes memory/SSD reuse where the engine reports it; it is neither a count of cache lookups nor cache writes. Unsupported/no reuse reports zero. Anthropic cache-control billing fields do not replace these engine counters.
  • Cache efficiency (API): cached / prompt, a fraction from 0 to 1.
  • Generation speed: sum(output tokens) / sum(generation seconds), matching existing serving-statistics weighting; not the mean of per-request speeds.
  • Prefill speed (API): sum(prompt − cached) / sum(prefill seconds).
  • Request seconds (API): measured serving-handler duration, including waits within that measurement, not HTTP middleware/network latency. Loading inclusion follows the existing endpoint timer. Unknown durations (currently audio) are excluded from timed_requests and average_request_seconds.

Prefill/generation timing follows existing endpoint accounting (time to first output and subsequent generation; supported diffusion engines use native timing). Overlapping requests each contribute their own duration. Summed seconds are not GPU busy time. Missing speed measurements return null, not invented throughput.

Storage and retention

<base_path>/usage.sqlite3 lives alongside stats.json (normally ~/.omlx/usage.sqlite3; follows OMLX_BASE_PATH and the app's base-path setting). Python's built-in SQLite stores one row per active model/hour with schema version 1 (PRAGMA user_version). Existing cumulative stats.json and its clear controls remain independent. No new dependency or configuration is required.

A background thread checks for pending aggregates every 5 seconds and flushes on normal shutdown. With no pending data, it skips database access unless daily retention maintenance is due or a storage failure needs retrying. The UI polls every 15 seconds. Sudden termination can lose the unflushed batch. Records are attributed to the local hour when accounting completes, not split across the hours of a long request. Epoch bucket keys distinguish repeated DST hours; calendar queries use server-local dates, including 23/25-hour days and fractional UTC offsets. The heatmap combines repeated hours. Changing the server's timezone reinterprets historical bucket timestamps; boundaries then have hourly precision, not exact request-level precision.

Hourly rows older than 400 days are pruned daily; SQLite reuses freed pages and incrementally reclaims space. Idle models do not generate rows. At most 4,096 pending model/hour aggregates are retained during storage outages; overflow drops analytics only. The API reports current-process dropped_requests and storage availability. Reads use committed snapshots and never wait for a flush. Storage failures do not prevent inference; recoverable write failures retry. Corruption found at startup is moved to one usage.sqlite3.corrupt recovery backup (plus any SQLite sidecars) and a fresh database is created. Future schema versions are left untouched. Runtime corruption can require a server restart.

To reset history, stop oMLX and remove usage.sqlite3, usage.sqlite3-wal, and usage.sqlite3-shm from the configured base directory, if present. Remove the .corrupt backup and its sidecars too if desired. Restart oMLX to begin fresh. Clearing Session or All Time in the dashboard does not erase history.

Disabling

Usage history is on by default. Turn it off with the Record usage history switch (web dashboard: Settings → Usage History; macOS app: Server → Usage History), or set usage.usage_history to false in settings.json. The OMLX_USAGE_HISTORY environment variable (0/false/off or 1/true/on) overrides the saved value at startup.

The switch applies immediately, without a restart. Turning it off flushes any pending aggregates and then stops recording; usage.sqlite3 stays in place, so turning it back on resumes the same history. A server started with recording off does not create, open, or repair the database; initialization waits until recording is enabled. After pending aggregates have been flushed (including retries after storage failures), nothing is read from or written to the database while off: the Usage History panel reports that history is off and points to Settings, and GET /admin/api/usage returns "enabled": false with zero totals instead of the unavailable error. Cumulative Session and All Time statistics are unaffected.

Admin API

GET /admin/api/usage?range=7d&model=<canonical-model-id> uses the existing admin session-cookie authentication (and the existing explicit auth-bypass setting). range accepts today (default), yesterday, 7d, 30d, 90d, or month. Multi-day ranges include today; Yesterday is the preceding calendar day. Omit model for all models. Filtering uses the exact canonical ID, even after unload or removal. OpenAI-compatible responses and endpoints are unchanged.

The default JSON includes totals, models, and heatmap, plus range, retention, refresh, enabled, availability, and overflow metadata. Add include_details=true to also compute and return the full daily and hourly aggregates. The web and Mac panels use the compact default response. Aggregate metrics are requests, prompt_tokens, completion_tokens, total_tokens, cached_tokens, prefill_seconds, generation_seconds, request_seconds, timed_requests, cache_efficiency, generation_tps, prefill_tps, and average_request_seconds. Unsupported ranges return 422; inaccessible history returns a generic 503.

For example, with an admin session cookie file obtained through the existing login flow:

curl -b /path/to/admin-cookies.txt \
  'http://127.0.0.1:8000/admin/api/usage?range=month&model=your-model-id'