Help us shape the future of Go! Take the 2026 Go Developer Survey

usage

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Persistent cache for the rolling 30-day usage dashboard — the aggregate report AND the per-file incremental index, both on disk. This is the safe redesign:

  • Persisted (disk) = ~/.rmote/agent/usage_month.json. Holds the version, lastRefreshMs, and a per-file index: (path, mtime, size) → {that file's full-history (date,model) contribution}. A few hundred KB → low MB (paths + day×model cells dominate). Loaded on daemon start.
  • In-memory = the same index + the running aggregate, rebuilt from the persisted index on load. GET serves the finalized (windowed) report instantly.

Cold/full scans happen ONLY when the cache file is missing, corrupt, the schema version mismatches, or (rare) the aggregate is nil. A normal `brm` loads the index, so the first Refresh after restart is already incremental (~ms) — no multi-second scan. Unchanged files are skipped; changed/new files are re-parsed and their contribution REPLACED (subtract old, merge new); deleted/aged-out files (>30d mtime, never seen by the walk) are dropped.

The window is ROLLING 30 days (not calendar month), so there is no month-rollover cold reset — the window slides and finalize() re-applies it each refresh. Per-file contributions are the file's FULL history (window-independent), so subtract/merge stay exact as the window moves.

Safety vs. the old saga: this stores per-file (date,model)-bucketed contributions (one tiny struct per cell), never the 175k per-line records (~80MB), and loads once at startup with none of the eviction/reload churn that spiked to 300MB. No polling, no background tick — refresh only on explicit POST.

On-demand rolling 30-day usage — types + accumulator. The wire shape (monthReport) matches the old UsageReport Codable so the restored dashboard decodes it unchanged. Scanning/caching lives in month_cache.go; this file holds the pure data structures shared by the cache and the per-file parsers.

The window is a rolling 30 days (not calendar month), so the dashboard's Daily/Weekly/Monthly tabs can all slice one scan: Daily=today, Weekly=last 7d, Monthly=last 30d. byDay + byDayModel are keyed by ABSOLUTE date ("2006-01-02") — day-of-month keying would collide across months.

Per-file contributions are parsed as the file's FULL history (no since filter) so a contribution is window-independent: subtract/merge stay exact as the 30-day window slides, and finalize() applies the window. The walk bounds the index by skipping files with mtime older than the window (they carry no in-window turns).

Memory-safety invariant (the lesson from the dashboard's idle-memory saga): the persisted index holds one tiny (date,model)-bucketed struct per transcript file — never the ~175k per-line records that cost ~80MB, and never the 20MB blob that spiked to 300MB. No polling, no background tick.

Per-agent transcript parsers for the on-demand month view (month_total.go). Each parser streams one transcript file and folds its in-month tokens into a shared *monthAccum. None of them retain per-record state — the accum's buckets are the only thing that survives past a line loop.

Field names + paths reused verbatim from the deleted report.go / codex_usage.go / grok_usage.go (verified against on-host transcripts):

Claude ~/.claude/projects/**/*.jsonl
  type=="assistant", message.usage.{input,output,cache_read,cache_creation}_tokens
  message.model, timestamp (RFC3339Nano)

Codex  ~/.codex/sessions/**/*.jsonl   (ONE cumulative record per session)
  session_meta.payload.timestamp  → session date
  turn_context.payload.model      → model (last wins)
  event_msg payload.type=="token_count" → payload.info.total_token_usage
    {input_tokens, output_tokens, total_tokens(=in+out)}

Grok   ~/.grok/sessions/<cwd>/<uuid>/signals.json
  contextTokensUsed (context SNAPSHOT, not throughput) + primaryModelId
  dated by file mtime

Pi (earendil-works pi coding-agent) usage for the rolling 30-day dashboard. Each pi session is a JSONL tree at ~/.pi/agent/sessions/<dir>/<ISOts>_<uuid>.jsonl. Assistant messages carry a nested `message.usage` object:

{"type":"message","id":...,"timestamp":"2026-08-11T16:00:47.572Z",
 "message":{"role":"assistant","model":"glm-5.2",
           "usage":{"input":1612,"output":2269,"cacheRead":62336,
                    "cacheWrite":0,"reasoning":2047,"totalTokens":66217,
                    "cost":{...}}}}

totalTokens = input+output+cacheRead+cacheWrite (reasoning excluded — same convention codex uses for its reasoning tokens). The four buckets map onto the accum's input/output/cacheRead/cacheCreate (pi's cacheWrite = cache creation). cost is ignored: BYO-key providers report 0 and the daemon never prices. The line-level `timestamp` is ISO-8601 (the nested message.timestamp is epoch-ms; either resolves to the same day bucket — we use the line level for consistency with the Claude parser).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FetchZaiUsage

func FetchZaiUsage() ([]byte, error)

FetchZaiUsage returns the raw z.ai quota JSON. Returns (nil, nil) when the host has no BYOK token configured — the handler maps that to 404. A non-nil error means the upstream call failed (timeout, non-2xx); the handler maps that to 502 so iOS keeps the last good value rather than retrying tightly.

func GetCachedMonth

func GetCachedMonth() *monthReport

GetCachedMonth returns the cached report WITHOUT scanning. Before the first refresh it is an empty report with lastRefreshMs=0 (iOS bootstraps a refresh when it sees that sentinel). Takes the lock only briefly.

func InvalidateClaudeQuotaCache

func InvalidateClaudeQuotaCache()

InvalidateClaudeQuotaCache clears the cached Claude quota (tests + future force-refresh entry point).

func InvalidateCodexQuotaCache

func InvalidateCodexQuotaCache()

InvalidateCodexQuotaCache clears the cached codex quota. For tests + future "rescan" entry points.

func InvalidateGrokQuotaCache

func InvalidateGrokQuotaCache()

InvalidateGrokQuotaCache clears the cached grok quota. For tests + future "rescan" entry points.

func InvalidateZaiCache

func InvalidateZaiCache()

InvalidateZaiCache clears the cached z.ai response. For tests + a future "force refresh" entry point.

func RefreshMonth

func RefreshMonth() *monthReport

RefreshMonth performs an incremental rescan and returns the fresh report. Cold (full) scan only when agg is nil — i.e. the cache file was missing, corrupt, or the schema version changed. Otherwise only changed/new files are re-parsed and files no longer seen (deleted/aged past the window) are dropped. The rolling window slides each call; finalize re-applies it. Holds the lock for the scan duration (acceptable: refresh is on-demand and rare; warm refreshes are ms).

func RegisterPaymentDetector

func RegisterPaymentDetector(agent string, d PaymentDetector)

RegisterPaymentDetector adds an agent's detector to the registry. Called from init(); idempotent.

func RegisterQuotaReader

func RegisterQuotaReader(agent string, r QuotaReader)

RegisterQuotaReader adds an agent's quota reader to the registry. Idempotent — late init() calls replace earlier registrations (useful in tests).

Types

type PaymentDetector

type PaymentDetector interface {
	Detect() PaymentModel
}

PaymentDetector returns the auto-detected (pre-override) model for one agent. Agents with no registered detector default to subscription.

type PaymentModel

type PaymentModel string

PaymentModel is an agent's billing classification. Values match iOS's PaymentModelWire.model / PaymentModel.rawValue decoders exactly.

const (
	PaymentByokZai      PaymentModel = "byokZai"      // Claude Code BYOK via z.ai — quota endpoint + cost
	PaymentByokProxy    PaymentModel = "byokProxy"    // BYOK via a paid proxy — cost only, no quota
	PaymentSubscription PaymentModel = "subscription" // flat-fee sub (Claude Max / ChatGPT / Grok)
	PaymentUnknown      PaymentModel = "unknown"
)

type PaymentModelWire

type PaymentModelWire struct {
	Agent       string `json:"agent"`
	Model       string `json:"model"`
	DisplayName string `json:"displayName"`
	Detected    string `json:"detected"`
	ShowsCost   bool   `json:"showsCost"`
	ShowsQuota  bool   `json:"showsQuota"`
}

PaymentModelWire is the per-agent row iOS's PaymentModelWire Codable reads. JSON tags mirror the Swift field names exactly (agent/model/displayName/ detected/showsCost/showsQuota). showsCost + showsQuota are concrete bools (no omitempty) so iOS always receives the column gates, matching the Mac server's wire shape.

func ResolvedPaymentModels

func ResolvedPaymentModels() []PaymentModelWire

ResolvedPaymentModels returns the wire list the iOS dashboard + chip read to decide which columns/chips to render. Order is stable (knownAgents order) so iOS's layout doesn't reshuffle between polls.

func SavePaymentOverride

func SavePaymentOverride(agent, model string) ([]PaymentModelWire, error)

SavePaymentOverride writes a single agent override atomically into ~/.rmote/usage-models.json (read-modify-write; tmp + rename in the same dir). Returns the resolved wire list so the PUT handler can echo it directly.

type Quota

type Quota struct {
	Percent      float64       `json:"percent"`
	ResetEpochMs int64         `json:"resetEpochMs"`
	Period       string        `json:"period,omitempty"`
	Windows      []QuotaWindow `json:"windows,omitempty"`

	// Source identifies where this reading came from so a fallback (e.g. the
	// OAuth→log-scrape path in codex/grok) is visible to the user rather than
	// indistinguishable from authoritative data. Values: "oauth" | "log-scrape"
	// | "cache". Omitted (empty) on legacy/z.ai paths that never fall back.
	Source string `json:"source,omitempty"`
}

Quota is the wire shape iOS's GrokQuotaResponse Codable decodes. Both /api/grok/quota and /api/codex/quota return this shape so iOS reuses one decoder (GrokQuotaService + CodexQuotaService both decode GrokQuotaResponse). All fields are optional on the iOS side, so zero-values are acceptable.

func ReadQuota

func ReadQuota(agent string) (*Quota, error)

ReadQuota looks up the agent's reader and invokes it. Returns (nil, nil) when the agent has no registered reader — the handler maps that to 404.

type QuotaReader

type QuotaReader interface {
	ReadQuota() (*Quota, error)
}

QuotaReader reads the subscription/quota state for one agent that exposes a quota endpoint. Returns (nil, nil) when no quota has been logged yet — the handler translates that into 404 ("none logged yet"), which iOS treats as an honest empty state rather than an error.

type QuotaWindow

type QuotaWindow struct {
	Label        string  `json:"label"`
	Percent      float64 `json:"percent"`
	ResetEpochMs int64   `json:"resetEpochMs"`
}

QuotaWindow is a single rate window (e.g. "5h", "1w"). iOS renders each window as its own row with % used + reset countdown. Codex exposes two (primary 5h + secondary 1w); grok exposes one (weekly).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL