usage

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package usage — claude_quota.go: real claude.ai subscription quota from the statusLine hook drop file.

Unlike codex (which persists rate_limits into its rollout jsonl), the claude CLI exposes subscription 5h/7d usage ONLY as the `rate_limits` field of the JSON it pipes to a user-configured statusLine command. We capture that field via an opt-in passthrough hook (scripts/claude-statusline-hook.sh) which writes ~/.deepwork/claude-rate-limits.json. This reader consumes that file.

Honest degradation: if the file is absent (hook not installed, or no API response yet this session), we return nil windows — the report renders 「—」 rather than a fabricated number.

Package usage — codex_model_source.go: ModelScanSource backed by the codex CLI's own rollout transcripts.

Data path: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl (or DW_CODEX_HOME for tests — the same override github.com/brightman-ai/kit/transcript.CodexSource honors).

SSOT: rollout discovery is delegated to transcript.CodexSource.ListSessions (the SAME source runtime_session_routes.go's buildAggregator wires for per-session transcript viewing), and per-turn token extraction is delegated to transcript.ScanCodexModelUsage (the shared primitive factored out of CodexSource.LoadTranscript's usage-footer logic). This file adds NO rollout parsing of its own — only (date, model) bucketing over the shared source's output, mirroring the shape of JSONLTokenSource.ScanModelRange for claude.

Package usage — codex_probe.go: the ON-DEMAND, authoritative codex quota probe.

Why this exists. The offline reader (codex_quota.go) can only see what codex has already written to a rollout, and codex writes the rate-limit family of the model it is CURRENTLY running. Work a session on a per-model plan (e.g. GPT-5.3-Codex-Spark) and the ACCOUNT limit simply stops being refreshed — so after the account's 5h window rolls over, the newest account reading on disk is the pre-reset one, and no amount of re-reading the disk will ever produce a fresher number. Pressing 刷新 moved nothing, because there was nothing new to move to.

This probe asks the account directly, the same way `codex-switch` does: one request to the Codex backend with the stored OAuth token, reading the x-codex-* rate-limit headers off the response. Requesting a NON-Spark model is deliberate — the headers then describe the ACCOUNT limit, which is the number `codex /status` prints and the number the chip shows.

Cost and consent. This is a real API request, so it is USER-INITIATED ONLY: the 刷新 button calls it; the background poll never does. The access token is read from ~/.codex/auth.json, used for exactly this one call, and never logged, never persisted, never returned to any caller.

Package usage — codex_quota.go: the codex ACCOUNT rate limit, read from the rollout transcripts codex writes to ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl.

Two things make this harder than "read the last rate_limits object", and getting either wrong produces numbers that look plausible and are simply false:

  1. A rollout carries SEVERAL limit families. Alongside the account limit (limit_id "codex", no limit_name) codex emits per-model sub-limits — e.g. {"limit_id":"codex_bengalfox","limit_name":"GPT-5.3-Codex-Spark"} — which have their OWN 5h/7d windows and their own, much lower, usage. The sub-limit heartbeats more often, so the LAST rate_limits in a file is usually a sub-limit. Reading it reported "92% left" while `codex /status` said 26% — the account was nearly out of quota and the UI said it was fine. We therefore select the UNNAMED (account) family and ignore named per-model limits.

  2. The newest file's newest ACCOUNT entry is not its last line, and may not even be in the newest file. We take the entry with the greatest EVENT timestamp across the few most recent rollouts, and use that timestamp as the reading's capture time — so "更新于 …" tells the truth instead of tracking a file's mtime.

Read-only, no API call, no auth: the same data `codex /status` shows is already on disk.

Package usage — cost.go: the cost adapter over the pricing SSOT (CHG-016).

设计 (Linus 单一来源): cost = tokens × 单价. 价表不再活在本仓 —— 已迁出到共享 kit/pricing (github.com/brightman-ai/kit/pricing), 由 deepwork-terminal 与 deepwork-pro 同源消费, 谁都不拥有它. 本文件只剩一层薄适配: 把 pro 内部历史沿用的 ComputeCost / HasPrice / CostResult 形态映射到 pricing.Lookup / pricing.Usage, 调用方 (session_routes / runtime_session_routes / od_routes / report) 签名不变.

THINKING TOKENS (诚实): Claude transcript 的 usage 无独立 thinking/reasoning 字段 — extended thinking 的产出 token 计入 output_tokens. 故 thinking 与 output 同价, 不单列.

缺价诚实 (RED LINE §5 "不伪造 cost"): pricing.Lookup 未命中 → CostResult.HasPrice=false, 调用方显 tokens 不显 cost (绝不蒙一个数).

Package usage is the host-agnostic SSOT for LLM usage, cost, and subscription quota reporting. It reads locally-written agent transcript logs and rate-limit drop files, aggregates per-model token deltas, and prices them via kit/pricing — ccusage-style: local logs + public pricing, no proprietary data, no network.

Layering (why this lives in kit, not in a host):

  • It sits on kit/transcript (session enumeration + per-turn token scanning) and kit/pricing (model → USD/CNY, the pricing SSOT).
  • It is pure compute + local file reads: every source is derived from $HOME or an env override (CLAUDE_CONFIG_DIR / DEEPWORK_HOME / DW_CODEX_HOME). No DB, no config injection, no HTTP. Missing/empty inputs degrade honestly (ok=false) rather than guessing.
  • HTTP exposure ( /usage/quota, /usage/report ) belongs to the HOST that mounts it (deepwork-terminal serves it; deepwork-pro forwards to the same handler). Routing is deliberately NOT in this package — kit ships primitives, hosts ship endpoints.

Both deepwork-terminal (standalone) and deepwork-pro (embedded) import this one package, so the numbers can never drift between deployments.

The three entry points:

  • BuildReport(window, TokenSource) — per-provider token + cost report.
  • ComputeCost(model, in, out, cacheRead, cacheCreate) — single-call cost.
  • QueryAllQuotas() — subscription 5h/7d remaining%, per detected runtime.

Sources (compose via CombinedModelScanSource / JSONLTokenSource / CodexModelScanSource) turn on-disk transcripts into the token deltas the report and cost paths consume.

Package usage provides subscription quota inspection and usage reporting. jsonl_source.go: TokenSource backed by Claude JSONL transcript files.

Data path: ~/.claude/projects/**/*.jsonl Each JSONL file contains rows with type="assistant" that have message.usage. Because Claude streams updates, duplicate rows per message are possible — we use max-per-key dedup (same strategy as agent_intel.UsageAccumulator).

Reset-on-restart: this source is stateless/read-only; it rescans files on every DailyTokens call. There is no persistent DB backing. Limitation: counts only Claude CLI sessions (Codex/Gemini not yet included).

Package usage — multi_source.go: merges several ModelScanSource (claude JSONL, codex rollout, ...) into one so BuildReport can price every wired runtime's usage in a single report (CHG cost-across-windows: codex cost was previously absent from the usage report because the only wired source was claude-only JSONLTokenSource).

Package usage — presence.go: axis 1 of the quota model, "does this account exist on this host?".

This is a USER fact and it is deliberately kept independent of whether the CLI can be executed. Probing the binary (axis 4) answers a different question, and letting it answer this one is what made a logged-in, actively-billing Claude account disappear from the UI the moment a self-update dropped the executable bit.

Evidence is a SET, not a single file — a lone credentials file can be residue from an explicit logout, so we report WHICH evidence we found and let the UI phrase itself honestly ("已登录" vs "仅存历史记录") instead of over-claiming.

Read-only, existence-level probes. We look at whether an auth file is populated and (for codex) which FIELD carries the credential — never at the credential's value, and nothing here is ever surfaced to the frontend.

WHERE the files live is not decided here: kit/transcript owns root resolution for both runtimes, so presence, spend and quota can never read three different ~/.claude dirs.

Package usage — provider.go: the quota domain's two abstractions.

A runtime's quota is assembled from READINGS, and served by a PROVIDER.

Reading  — one observation of the account's windows, with a time and a provenance.
           Several sources can produce one (a statusline hook, a rollout transcript, a
           live probe); the freshest wins, and the winner remembers where it came from.
Provider — one runtime's quota domain: how to observe it offline, and whether it can be
           asked directly.

Both exist because the alternative kept costing us. QueryAllQuotas used to hardcode a call per runtime, so adding one meant editing two places; the refresh endpoint hardcoded which runtimes could be probed, so the domain leaked into the transport; and a reading carried no provenance, so when a user asked "why does refresh change nothing?" nobody could answer without going and reading rollout files by hand. (The answer: codex only records the rate-limit family of the model it is currently running, so a session on a per-model plan stops refreshing the ACCOUNT limit entirely.) Snapshot.Source now says that out loud.

Package usage provides subscription quota inspection and usage reporting for supported CLI runtimes (claude, codex, gemini).

Index

Constants

View Source
const (
	SourceHook    = "hook"    // claude's statusLine hook drop file (claude reports as it renders)
	SourceRollout = "rollout" // codex's rollout transcript (codex reports as it works)
	SourceProbe   = "probe"   // we asked the provider directly (user-initiated refresh)
)

Reading provenance — WHERE a quota observation came from. Surfaced to the UI, because "which source is this number from, and can refreshing it help?" is a question users actually ask.

View Source
const (
	ProbeOK           = "ok"
	ProbeFailed       = "failed"
	ProbeNotSupported = "not_supported"
)

Probe statuses.

View Source
const (
	BillingSubscription = "subscription" // has 5h/7d subscription windows
	BillingAPI          = "api"          // pay-per-token; no subscription window exists by design
	BillingUnknown      = "unknown"      // not enough evidence — say so, never guess
)

Billing modes.

View Source
const (
	EvidenceCredentials = "credentials" // an auth/credentials file exists
	EvidenceSnapshot    = "snapshot"    // we hold a (possibly old) quota reading
	EvidenceSessions    = "sessions"    // local transcript/session history exists
)

Presence evidence kinds (axis 1). Reported so the UI can distinguish "logged in" from "only residual history" — an explicit logout removes credentials, and we must not keep claiming the account is live just because a stale reading is on disk.

View Source
const (
	HealthNotInstalled       = "not_installed"        // nothing named like the CLI on PATH
	HealthNotExecutable      = "not_executable"       // found, but cannot be executed (lost +x, bad interpreter)
	HealthVersionCheckFailed = "version_check_failed" // executed, but `--version` errored
	HealthNotImplemented     = "not_implemented"      // this runtime is not supported at all
)

Health reasons (axis 4).

View Source
const (
	StaleWindowRolled = "window_rolled" // every window it describes has since reset
	StaleTooOld       = "too_old"       // no reset to check against, and it predates maxSnapshotAge
)

Stale reasons (axis 3).

Variables

This section is empty.

Functions

func HasPrice

func HasPrice(model string) bool

HasPrice reports whether a model has a known price row (used for honest UI gating). Thin delegate to the pricing SSOT.

Types

type CodexModelScanSource

type CodexModelScanSource struct {
	// contains filtered or unexported fields
}

CodexModelScanSource implements ModelScanSource by walking every codex rollout transcript (across all projects) and bucketing their per-turn, model-tagged token deltas by UTC date.

func NewCodexModelScanSource

func NewCodexModelScanSource() *CodexModelScanSource

NewCodexModelScanSource creates a source rooted at ~/.codex (or DW_CODEX_HOME).

func NewCodexModelScanSourceAt

func NewCodexModelScanSourceAt(dir string) *CodexModelScanSource

NewCodexModelScanSourceAt creates a source for a custom codex home dir (tests). dir must contain a "sessions/" subtree, matching the real ~/.codex layout (transcript.CodexSource.Root).

func (*CodexModelScanSource) DailyTokens

func (s *CodexModelScanSource) DailyTokens(date string) (inputTokens, outputTokens, cacheReadTokens int64, err error)

DailyTokens implements TokenSource for interface completeness (a *CodexModelScanSource can then be passed to BuildReport standalone, or type- asserted as ModelScanSource by CombinedModelScanSource). BuildReport prefers the richer ScanModelRange path above whenever it succeeds — which is always, since ScanModelRange never itself returns an error — so this is not on the hot path in practice.

func (*CodexModelScanSource) ScanModelRange

func (s *CodexModelScanSource) ScanModelRange(startDate, endDate string) ([]ModelTokens, error)

ScanModelRange implements ModelScanSource: enumerates every codex rollout (ListSessions with an empty projectDir applies no cwd filter — every session across every project is included, matching JSONLTokenSource's whole-tree scan), extracts each one's per-turn model-tagged deltas via the shared transcript.ScanCodexModelUsage primitive, and re-aggregates into per-(date, model) bundles restricted to [startDate, endDate] (UTC, inclusive).

DEDUP: transcript.ScanCodexModelUsage already sums only last_token_usage (the per-turn delta) — never the cumulative total_token_usage a rollout carries — so this layer never re-derives or double-sums a running total; it only adds up already-deduped per-turn deltas.

type CombinedModelScanSource

type CombinedModelScanSource struct {
	Sources []ModelScanSource
}

CombinedModelScanSource merges N ModelScanSource into one. Concatenating their bundles is safe: BuildReport's buildFromModelBundles re-aggregates every ModelTokens bundle into its own (date, model) key regardless of which source produced it — callers don't need to pre-merge across sources, and different runtimes never collide on (date, model) since model ids are runtime-distinct (e.g. "claude-opus-4-8" vs "gpt-5.4").

func (*CombinedModelScanSource) DailyTokens

func (c *CombinedModelScanSource) DailyTokens(date string) (inputTokens, outputTokens, cacheReadTokens int64, err error)

DailyTokens implements TokenSource for interface completeness (BuildReport's signature requires it). BuildReport always prefers the ModelScanSource path above — which never errors here — so this is not exercised in practice, but sums correctly across sources if ever called directly.

func (*CombinedModelScanSource) ScanModelRange

func (c *CombinedModelScanSource) ScanModelRange(startDate, endDate string) ([]ModelTokens, error)

ScanModelRange implements ModelScanSource. One source's error does not abort the whole report — the other sources' usage still surfaces (honest partial data beats an all-or-nothing report); it is never itself an error.

type CostResult

type CostResult struct {
	InputCost       float64 `json:"input_cost"`
	OutputCost      float64 `json:"output_cost"`
	CacheReadCost   float64 `json:"cache_read_cost"`
	CacheCreateCost float64 `json:"cache_create_cost"`
	TotalCost       float64 `json:"total_cost"`
	Currency        string  `json:"currency"`
	HasPrice        bool    `json:"has_price"`
}

CostResult is the computed cost for one (model, token-bundle). HasPrice=false ⇒ no matching price row ⇒ caller renders tokens but NOT cost (honest: 缺价不蒙). Costs are in Currency units (¥ for CNY, $ for USD).

func ComputeCost

func ComputeCost(model string, inputTok, outputTok, cacheReadTok, cacheCreateTok int64) CostResult

ComputeCost is the single per-turn/per-bundle cost adapter (fleet + settings 共用). It delegates the price table AND the per-request cost to kit/pricing v0.4 and only keeps pro's per-category breakdown + currency + HasPrice shape. thinking tokens 计入 output (见包头注), 故无单独 thinking 项. 缺价 → HasPrice=false.

CACHE-WRITE TTL (诚实声明 — 已知微量低估): kit/pricing v0.4 把 cache-write 按 TTL 拆成 CacheWrite5m (1.25× input) 与 CacheWrite1h (2× input). pro 的逐 turn/逐 bundle 聚合层 不携带 5m/1h 拆分 (只有 terminal 的逐消息路径有), 调用方仅传单一 cacheCreate 写入总量. 故本适配器把它全记为 CacheWrite5m (保守的 1.25× 基准价). 对包含 1h 缓存写的 turn 这是 相对 terminal 逐消息口径的微量低估 —— pro 的 turn 聚合不背 TTL 拆分, 这是已知且可接受的偏差.

TotalCost 以 pricing.Cost (单请求 + 上下文分层权威) 为准; 逐类 breakdown 由 Lookup 的 base Tier 单价自算 (CostResult 的明细字段, 用于 UI 展示, 与 TotalCost 同源价表).

type DayTokens

type DayTokens struct {
	InputTokens       int64
	OutputTokens      int64
	CacheReadTokens   int64
	CacheCreateTokens int64
}

DayTokens is one calendar day's deduplicated token totals (codex H4).

type JSONLTokenSource

type JSONLTokenSource struct {
	// contains filtered or unexported fields
}

JSONLTokenSource implements TokenSource by scanning Claude JSONL transcripts. It rescans on every call — suitable for low-frequency reporting endpoints.

func NewJSONLTokenSource

func NewJSONLTokenSource() *JSONLTokenSource

NewJSONLTokenSource creates a new source scanning the default claude projects dir.

func NewJSONLTokenSourceAt

func NewJSONLTokenSourceAt(dir string) *JSONLTokenSource

NewJSONLTokenSourceAt creates a source for a custom projects directory (tests).

func (*JSONLTokenSource) DailyTokens

func (s *JSONLTokenSource) DailyTokens(date string) (inputTokens, outputTokens, cacheReadTokens int64, err error)

DailyTokens returns deduplicated token totals for the given UTC date (YYYY-MM-DD). Scans all *.jsonl files under ~/.claude/projects/ and returns (input, output, cacheRead, nil). Returns (0, 0, 0, nil) when no data exists for that date.

func (*JSONLTokenSource) ScanModelRange

func (s *JSONLTokenSource) ScanModelRange(startDate, endDate string) ([]ModelTokens, error)

ScanModelRange implements ModelScanSource (CHG-014 R3 cost dim): one tree-walk that buckets every in-range assistant message by (UTC date, model), capturing all four token categories (incl. cache_creation, which the aggregate ScanRange drops). Dedup is per (date, model, file, messageID) with max-per-key, matching ScanRange.

func (*JSONLTokenSource) ScanRange

func (s *JSONLTokenSource) ScanRange(startDate, endDate string) (map[string]DayTokens, error)

ScanRange implements RangeTokenSource (codex H4): it walks the JSONL tree ONCE and buckets every in-range assistant message by its UTC date, returning a date→DayTokens map. This replaces the old per-day DailyTokens path that walked the entire tree once PER DAY (7×/30× per report request). Dedup is per (date, file, messageID) with max-per-key, matching DailyTokens semantics.

type ModelScanSource

type ModelScanSource interface {
	ScanModelRange(startDate, endDate string) ([]ModelTokens, error)
}

ModelScanSource is the richest (optional) source interface: it returns per-(date, model) token bundles for the window so BuildReport can price each model with its own rate (cost = Σ tokens_model × price_model) and build the per-provider table. When a source implements this, BuildReport prefers it over RangeTokenSource.

type ModelTokens

type ModelTokens struct {
	Date              string
	Model             string
	InputTokens       int64
	OutputTokens      int64
	CacheReadTokens   int64
	CacheCreateTokens int64
}

ModelTokens is one (date, model) deduplicated token bundle (CHG-014 R3 cost dim).

type ProbeResult added in v0.13.0

type ProbeResult struct {
	Runtime string `json:"runtime"`
	// Status is "ok" | "failed" | "not_supported".
	Status string `json:"status"`
	Reason string `json:"reason,omitempty"`
}

ProbeResult reports what one runtime's probe did. A runtime that cannot be probed says so rather than being silently absent — "we did not ask" and "we asked and it failed" are different facts, and the UI phrases them differently.

func ProbeAll added in v0.13.0

func ProbeAll(ctx context.Context) []ProbeResult

ProbeAll asks every runtime that can be asked for its current quota, and reports what each one did. It never returns an error: a failed probe degrades to the last-known reading, which the caller re-queries afterwards.

USER-INITIATED ONLY. Each probe is a real provider request; the background poll must never call this.

type ProviderRow

type ProviderRow struct {
	// Provider is the display key (e.g. "Claude" / "OpenAI" / "Gemini").
	Provider string `json:"provider"`
	// Runtime is the canonical runtime kind ("claude"|"codex"|"gemini"|"other").
	Runtime           string   `json:"runtime"`
	InputTokens       int64    `json:"input_tokens"`
	OutputTokens      int64    `json:"output_tokens"`
	CacheReadTokens   int64    `json:"cache_read_tokens"`
	CacheCreateTokens int64    `json:"cache_create_tokens"`
	TotalTokens       int64    `json:"total_tokens"`
	Cost              *float64 `json:"cost"`
	Currency          string   `json:"currency,omitempty"`
	// TopModel is the highest-token model id under this provider (主要消耗).
	TopModel string `json:"top_model,omitempty"`
	// Spark is the per-day total-token trend for this provider (oldest-first).
	Spark []int64 `json:"spark"`
}

ProviderRow is one provider/runtime cost-breakdown row (settings 报表 §Gap-3). Derived from per-model aggregation: each model id is mapped to a provider class + a runtime kind via simple model-name heuristics (claude/codex/gemini/...).

type QuotaInfo

type QuotaInfo struct {
	// Runtime is the CLI name: "claude", "codex", "gemini".
	Runtime string `json:"runtime"`

	// ── axis 1: account presence (the only axis allowed to hide a provider)
	Present bool `json:"present"`
	// Evidence lists WHY we believe the account is present (credentials/snapshot/sessions).
	// Empty ⟹ Present is false ⟹ the runtime is not shown at all.
	Evidence []string `json:"evidence,omitempty"`

	// ── axis 2: billing mode
	Billing string `json:"billing,omitempty"`

	// ── axis 3: the reading
	Plan string `json:"plan,omitempty"`
	// Family names WHICH set of limits these windows belong to. The provider can switch a
	// codex account between families with different windows ("codex" = 5h+7d, "premium" = one
	// 7-day window), and when it does, a bar simply disappears. Saying which family is active
	// is the only way the user can tell "the provider changed my plan's shape" from
	// "this app broke".
	Family  string        `json:"family,omitempty"`
	Windows []QuotaWindow `json:"windows,omitempty"`
	// Snapshot is nil when no reading has ever been captured.
	Snapshot *SnapshotMeta `json:"snapshot,omitempty"`

	// ── axis 4: runtime health
	Health RuntimeHealth `json:"health"`

	// Note is a human-readable supplementary message.
	Note string `json:"note,omitempty"`
}

QuotaInfo describes the quota state for one runtime, along the four axes above.

func QueryAllQuotas

func QueryAllQuotas() []QuotaInfo

QueryAllQuotas returns the quota state of every known runtime.

type QuotaProvider added in v0.13.0

type QuotaProvider interface {
	Runtime() string
	// Query assembles the runtime's four axes from what is already on disk. Never reaches out.
	Query() QuotaInfo
	// CanProbe reports whether this runtime can be asked for its current quota.
	CanProbe() bool
	// Probe asks the provider directly and persists the answer so the offline path sees it too.
	// Only ever called on an explicit user request — it costs a real provider round-trip.
	Probe(ctx context.Context) error
}

QuotaProvider is one runtime's quota domain.

Offline observation is mandatory: every runtime can at least be looked at on disk. Probing is optional — claude exposes its 5h/7d usage ONLY through the statusLine hook, so there is nothing to ask; codex answers directly. CanProbe() states that rather than making the caller know it.

type QuotaWindow

type QuotaWindow struct {
	// Kind is "5h" | "7d" (derived from WindowMinutes: ≤300 → 5h else 7d).
	Kind string `json:"kind"`
	// WindowMinutes is the window size the runtime reports (300 / 10080).
	WindowMinutes int `json:"window_minutes,omitempty"`
	// UsedPercent / RemainingPercent in [0,100].
	UsedPercent      float64 `json:"used_percent"`
	RemainingPercent float64 `json:"remaining_percent"`
	// ResetAt is the ISO-8601 time the window resets.
	ResetAt string `json:"reset_at,omitempty"`
	// Expired marks a window whose reset time had PASSED by the time we looked. The counter
	// rolled over, so the used% we captured is not merely old — it is wrong. Expiry is
	// per-window on purpose: a 5h window can roll while the 7d window stays perfectly valid,
	// and condemning both would throw away a number that is still true.
	Expired bool `json:"expired,omitempty"`
	// Inferred marks a value we DERIVED rather than observed.
	//
	// Only one inference is made, and only for an expired window: the counter rolled, and any
	// use since would have made the runtime report a fresher reading — none exists, so the
	// window has not been touched since it reset. Its usage is therefore zero. The reset time
	// is rolled forward by whole window lengths to the next real boundary.
	//
	// This is the ONE place the package computes a number it did not observe, and it is
	// labelled as such all the way to the UI.
	Inferred bool `json:"inferred,omitempty"`
}

QuotaWindow is one rolling rate-limit window (abtop model): 5h primary / 7d secondary. UsedPercent comes straight from the runtime's own accounting.

type RangeTokenSource

type RangeTokenSource interface {
	// ScanRange returns per-day token totals for every date in the inclusive
	// [startDate, endDate] range (YYYY-MM-DD, UTC) that has records. Dates with
	// no data may be absent from the map (callers fill zeros).
	ScanRange(startDate, endDate string) (map[string]DayTokens, error)
}

RangeTokenSource is an optional, MORE EFFICIENT interface (codex H4): it scans the data once for an inclusive [start, end] UTC date range and returns a date→totals map. A source implementing it lets BuildReport avoid the old O(days) full tree-walk (7 walks for 7d, 30 for 30d). When a TokenSource also implements RangeTokenSource, BuildReport prefers ScanRange.

type Reading added in v0.13.0

type Reading struct {
	CapturedAt time.Time
	Source     string // SourceHook | SourceRollout | SourceProbe
	Plan       string
	Billing    string // BillingSubscription | BillingAPI — an api reading has no windows by design
	// Family is WHICH set of limits this reading describes. Codex accounts have several
	// ("codex" = 5h+7d, "premium" = a single 7-day window, plus per-model families), and the
	// provider switches which one is ACTIVE. Two families are not two views of one truth —
	// they have different windows entirely — so readings from different families must never
	// be merged window-by-window. The freshest reading wins whole, and it says which family
	// it speaks for, because "why did my 5h bar disappear?" has no other answer.
	Family  string
	Windows []QuotaWindow
}

Reading is one observation of a runtime's quota windows.

type ReportRow

type ReportRow struct {
	// Date is the ISO-8601 date string for this bucket (YYYY-MM-DD).
	Date string `json:"date"`
	// InputTokens consumed in this bucket.
	InputTokens int64 `json:"input_tokens"`
	// OutputTokens generated in this bucket. (thinking tokens roll into output —
	// Claude usage has no separate thinking field; see cost.go header.)
	OutputTokens int64 `json:"output_tokens"`
	// CacheReadTokens read from cache in this bucket.
	CacheReadTokens int64 `json:"cache_read_tokens"`
	// CacheCreateTokens written to cache in this bucket (CHG-014 R3 — newly aggregated).
	CacheCreateTokens int64 `json:"cache_create_tokens"`
	// TotalTokens is the sum of all token categories.
	TotalTokens int64 `json:"total_tokens"`
	// Cost is the per-day estimated cost (nil when no model in the day had a
	// known price → honest「—」, never a fabricated default).
	Cost *float64 `json:"cost"`
	// Currency for Cost ("USD"|"CNY"|""). Empty when Cost is nil.
	Currency string `json:"currency,omitempty"`
}

ReportRow represents one time-bucketed usage measurement.

type ReportSummary

type ReportSummary struct {
	InputTokens       int64 `json:"input_tokens"`
	OutputTokens      int64 `json:"output_tokens"`
	CacheReadTokens   int64 `json:"cache_read_tokens"`
	CacheCreateTokens int64 `json:"cache_create_tokens"`
	TotalTokens       int64 `json:"total_tokens"`
	// Cost / Currency: window total estimated cost. Cost nil → no priced model.
	Cost     *float64 `json:"cost"`
	Currency string   `json:"currency,omitempty"`
	// CostComplete is false when ≥1 model in the window had NO price row (so the
	// shown cost UNDER-counts). UI may flag "≈" / "部分模型无价" honestly.
	CostComplete bool `json:"cost_complete"`
}

ReportSummary holds aggregate totals for the window.

type RuntimeHealth added in v0.13.0

type RuntimeHealth struct {
	OK      bool   `json:"ok"`
	Reason  string `json:"reason,omitempty"`
	Version string `json:"version,omitempty"`
}

RuntimeHealth is the CLI-executability probe (axis 4). A failure here NEVER hides the provider — it only tells the user "the CLI is broken, these numbers are the last ones we saw".

type SnapshotMeta added in v0.13.0

type SnapshotMeta struct {
	// CapturedAt is the ISO-8601 time the reading was taken.
	CapturedAt string `json:"captured_at,omitempty"`
	// AgeSeconds is how old the reading was at query time.
	AgeSeconds int64 `json:"age_seconds"`
	// Source is where the reading came from: SourceHook | SourceRollout | SourceProbe.
	// This is the field that answers "why did refreshing change nothing?" — a rollout reading
	// only moves when the runtime chooses to write one, whereas a probe is us going and asking.
	Source string `json:"source,omitempty"`
	// Stale marks a reading that can no longer be read as current AS A WHOLE (every window it
	// describes has rolled, or it is simply too old). A SINGLE rolled window does not set this —
	// that lives on QuotaWindow.Expired, so a valid 7d number survives an expired 5h one.
	Stale       bool   `json:"stale"`
	StaleReason string `json:"stale_reason,omitempty"`
}

SnapshotMeta describes the reading behind the windows: when it was taken, WHERE it came from, and whether it can still be read as current (axis 3). A nil QuotaInfo.Snapshot means we have never captured a reading — render 「等待额度数据」, never a fabricated 0%/100%.

type TokenSource

type TokenSource interface {
	// DailyTokens returns (input, output, cacheRead) token totals for the
	// given UTC calendar date (YYYY-MM-DD).  It returns (0,0,0,nil) when the
	// date has no records.
	DailyTokens(date string) (inputTokens, outputTokens, cacheReadTokens int64, err error)
}

TokenSource is an optional interface that a persistence layer can implement to supply per-day token counts. When nil, BuildReport returns a stub with Available=false so callers always receive a well-typed response.

type UsageReport

type UsageReport struct {
	// Window is the requested reporting window ("7d" or "30d").
	Window WindowKind `json:"window"`
	// StartDate is the ISO-8601 date of the first bucket.
	StartDate string `json:"start_date"`
	// EndDate is the ISO-8601 date of the last bucket (today).
	EndDate string `json:"end_date"`
	// Rows contains per-day buckets ordered oldest-first.
	// Rows are included even when zero so the frontend can render a full chart.
	Rows []ReportRow `json:"rows"`
	// Summary is the aggregate over the whole window.
	Summary ReportSummary `json:"summary"`
	// Providers is the per-provider cost breakdown (settings 报表). Empty when the
	// source can't attribute usage to models (e.g. legacy aggregate-only sources).
	Providers []ProviderRow `json:"providers"`
	// DataSource describes where the data came from.
	DataSource string `json:"data_source"`
	// Available is false when no usage data source is wired up.
	Available bool `json:"available"`
	// Reason explains why Available is false.
	Reason string `json:"reason,omitempty"`
}

UsageReport is the top-level response shape for GET /api/usage/report.

func BuildReport

func BuildReport(window WindowKind, src TokenSource) UsageReport

BuildReport assembles a UsageReport for the requested window. src may be nil; in that case the report is marked Available=false with Reason="no_data_source".

Cost (CHG-014 R3): when src implements ModelScanSource, BuildReport prices each (date, model) bundle with its OWN rate (ComputeCost) and sums — so the report carries per-day cost, a window total, and a per-provider breakdown. Models with no price row are counted in tokens but skipped for cost, and CostComplete is set false so the UI can flag the under-count honestly (RED LINE: 缺价不蒙).

type WindowKind

type WindowKind string

WindowKind names a reporting window.

const (
	Window24h WindowKind = "24h" // 最近 24 小时(按日粒度 = 今天)
	Window7d  WindowKind = "7d"
	Window14d WindowKind = "14d"
	Window30d WindowKind = "30d" // 月
)

Jump to

Keyboard shortcuts

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