usage

package
v1.801.256 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Analytics entitlement contract.

This file is the ONE place cloud maps a Hanzo plan to the access that gates the RICH per-org usage-analytics surface. It does NOT introduce a second datastore and does NOT touch the metering WRITE path (the hanzoai/ai router still writes every AI call into the ONE global hanzo.cloud_usage ledger; the org column is the row-level tenant filter). It gates who may READ the rich lens over that ledger.

The split it enforces:

  • BASIC own-org usage (GET /v1/usage/summary) is ALWAYS allowed — every tenant can read its own footprint. Never gated here.
  • RICH per-org analytics/insights (retention window, exports, per-provider/BYO/ fee breakdowns — GET /v1/usage/analytics) is a PAID entitlement, gated here.

The values are NOT hardcoded: they are resolved from the @hanzo/plans catalog (the single source of truth) via the canonical `analytics.*` entitlement vocabulary, so the numbers live in exactly one place and are read, never duplicated.

Contract (keys to add to hanzoai/plans entitlements.schema.json):

analytics.datastore       boolean  access to the rich per-org analytics surface
analytics.retention_days  int      how far back the tenant may query
analytics.export          boolean  data export allowed

FOLLOW-UP (documented, coordinated — not built here). Per-request ENFORCEMENT needs the caller's effective plan id (org -> plan), which is a subscription lookup owned by the billing plane (commerce /v1/billing/subscriptions); the gateway principal carries org/project but NO plan claim today, and no org->plan resolver exists in clients/plan or clients/billing yet. Until it lands, ResolveAnalyticsAccess takes an explicit plan id and GET /v1/usage/analytics/access echoes any plan's access so dashboards self-config against the live catalog, and GET /v1/usage/analytics reads ?plan= for its gate. Wiring the resolver into the handler (swap the ?plan= line for the resolved caller-org plan) is the rollout step — the gate logic is unchanged by it.

Pure core of the usage summary: the response shape, the ledger→category mapper, the spend roll-up + gap-filled series assembler, and the datastore value coercers. Everything here is I/O-free so the tests drive it with plain structs and mock rows — no commerce, no datastore. The handler (usage.go) is the thin orchestration that fetches and calls these.

Package usage is the ONE Hanzo Cloud usage surface (/v1/usage). It owns ALL usage — clients/link owns links and nothing usage — as one coherent plane over one window grammar:

  • POST /v1/usage record account-usage samples (the collector's data plane; write path over the hanzo.account_usage warehouse series, datastore.go).
  • GET /v1/usage/samples one provider account's own lane dash (the time series).
  • GET /v1/usage/summary the flagship own-scoped footprint roll-up (below).
  • GET /v1/usage/analytics{,/access} the entitlement-gated rich per-org read.

The summary answers "what am I running and what does it cost" by composing THREE complementary sources, each degrading independently to honest zeros (a source marker says which answered — a partial deploy never fabricates spend or usage):

  • Spend (the genuinely-missing categorized cost roll-up): the commerce ledger — usage-rollup (authoritative month-to-date consumed + prepaid wallet) plus the raw transaction ledger, rolled up server-side into spend-by-category over time. Every metered resource (GPU, machine-hours, LLM tokens, datastore footprint) debits this ONE ledger with a category tag, so it is already the unified cost source — this endpoint is the categorizing lens over it.
  • LLM usage totals: hanzo.cloud_usage, the per-org warehouse ledger (the same table /v1/analytics/* and the o11y board read). Totals only here — the per-model / timeseries detail stays at /v1/analytics/*.
  • The account board: the caller's OWN linked provider accounts (a Claude Max plan's window %, metered from the provider's own login) beside the org's Hanzo-routed usage, every row labelled by source/scope and NEVER summed. This is the account-usage global view, unified here from clients/link.

The console Usage view composes THIS with the existing org-scoped inventory endpoints (/v1/machines, /v1/gpus, /v1/agents, provisioning lists) for the per-kind counts — one screen, this one authoritative money source.

TENANT ISOLATION (the bar). The org is the VALIDATED IAM owner claim (principal.Org — the trusted X-Org-Id the identity middleware minted from the caller's verified bearer, HIP-0026; NEVER a client header) AND a validated principal is required (c.User() set only for a verified bearer). The commerce subject is pinned server-side to that org; every warehouse query binds org (and, for the account board, subject) POSITIONALLY. A caller can only ever read/write its OWN org — fail-closed: no principal → 401.

Registered as "usage" (order 131, OwnsHealth=false): the generic GET /v1/usage/health liveness route is a distinct path and never shadows these; order 131 binds /v1/usage/* before the ai subsystem's /v1/* catch-all (150).

Index

Constants

View Source
const (
	Range1h  = "1h"
	Range24h = "24h"
	Range7d  = "7d"
	Range30d = "30d"
)

Ranges — the closed allowlist for a sample-dash read window. This is the FINE-GRAINED account-usage grammar (a live lane dash cares about the 1h/6h scale); the coarser cost/analytics grammar (aiobject.ResolveCloudUsageWindow: 24h/7d/30d/custom + a bucket interval) drives summary and analytics. Two lanes, two grammars, each complete for its read.

View Source
const (
	Window6h    = "6h"
	WindowDay   = "day"
	WindowWeek  = "week"
	WindowMonth = "month"
)

The CANONICAL window value: the ONE closed vocabulary that rate limits, quotas, and usage rollups all share, platform-wide. Lowercase, no case variants, no synonyms — a window is one of exactly these four values.

This declaration is the single source of the vocabulary. (hanzoai/commerce today carries an ad-hoc set — weekly/daily/monthly/hourly plus capitalized variants, and no sub-day window at all; a later commerce pass adopts THESE values. Do not copy the old strings back in.)

View Source
const (
	ConfidenceExact       = "exact"
	ConfidenceEstimated   = "estimated"
	ConfidencePercentOnly = "percentOnly"
	ConfidenceUnknown     = "unknown"
)

Confidence values — how real a sample's numbers are. These mirror @hanzo/usage's UsageDataConfidence EXACTLY (they are the meter's own wire values, already carried on Usage.Confidence), so a value crosses the whole system unmapped.

This is the ANTI-FABRICATION FLAG, and the reason it is a column: a Claude sample is `percentOnly`, so its token counters are 0 because they are UNKNOWN — not because the account consumed nothing. Without this value a reader cannot tell "0 tokens" from "no idea", and would render a fabricated zero. With it, the console renders "—".

View Source
const (
	SourceAccount = "account"
	SourceHanzo   = "hanzo"
)

Sources for the global view: which plane a usage row came from. They are NOT interchangeable and are never added together —

  • SourceAccount the provider's OWN plan consumption, metered from the user's own login. Its cost is what the PROVIDER says it charged (0 for a flat subscription); its percent is plan quota. NOT a Hanzo charge.
  • SourceHanzo hanzo.cloud_usage — Hanzo-routed inference. COST OF RECORD.
View Source
const (
	ScopeUser = "user" // the caller's OWN linked accounts (org+subject)
	ScopeOrg  = "org"  // the whole org's Hanzo-routed usage
)

Scopes for a global-view row: the tenancy the row's numbers cover. The two sources answer at different scopes and the row says which, so a reader never silently compares a user's plan usage against an org's whole spend.

View Source
const (
	KindSubscription = "subscription"
	KindAPIKey       = "apikey"
)

Account-kind vocabulary — how a metered account is credentialed. It mirrors the link registry's Kind (subscription | apikey) so a Sample and its Link agree, but each package validates its OWN inputs: the value is a fact about the account, not a shared place, so the two are decoupled and neither can force a change on the other.

Variables

View Source
var FreeAnalyticsAccess = AnalyticsAccess{Datastore: false, RetentionDays: 7, Export: false}

FreeAnalyticsAccess is the fail-closed floor: basic own-org usage only — no rich datastore surface, a 7-day query window, no export. It is applied whenever no plan can be resolved (unknown plan, or the catalog is unreachable), so a catalog outage degrades to exactly Free — never above.

Functions

func Mount

func Mount(app cloud.Router, deps cloud.Deps) error

Mount wires the usage surface onto app per HIP-0106 — one line over the generic subsystem entrypoint: build the state, register the routes.

Types

type Accounts added in v1.801.60

type Accounts struct {
	Rows    []TotalView `json:"rows"`
	Account SourceState `json:"account"`
	Hanzo   SourceState `json:"hanzo"`
}

Accounts is the account-usage board folded into the summary: the caller's OWN linked provider accounts (metered from each provider's own login — a Claude Max plan's window %) beside the org's Hanzo-routed usage. Every row is labelled by source/scope/confidence and the two are NEVER summed together — a plan's percent is not money, and a provider's own spend is not a Hanzo charge. Each side reports its own availability, so half a warehouse never turns the other half into zeros. This is the account-usage plane's global view (moved from clients/link) unified under the ONE /v1/usage/summary; the per-account time series is GET /v1/usage/samples.

type AnalyticsAccess added in v1.786.146

type AnalyticsAccess struct {
	Datastore     bool // analytics.datastore
	RetentionDays int  // analytics.retention_days
	Export        bool // analytics.export
}

AnalyticsAccess is the resolved analytics entitlement for one caller. Datastore grants the rich per-org analytics/datastore surface; RetentionDays caps how far back the tenant may query; Export allows data export.

func AnalyticsAccessFromEntitlements added in v1.786.146

func AnalyticsAccessFromEntitlements(ent map[string]any) AnalyticsAccess

AnalyticsAccessFromEntitlements maps a resolved `analytics.*` entitlement block onto AnalyticsAccess. Pure and total: any key absent from the block keeps the Free-floor value, so a partial catalog can only ever NARROW access (never widen past the floor). Wrong-typed values are ignored (kept at floor).

func ResolveAnalyticsAccess added in v1.786.146

func ResolveAnalyticsAccess(ctx context.Context, planID string) (AnalyticsAccess, error)

ResolveAnalyticsAccess resolves analytics access for a plan id from the @hanzo/plans catalog (the single source of truth). An empty id short-circuits to FreeAnalyticsAccess (the floor) WITHOUT a catalog round-trip, so an unauthenticated or plan-less caller fails closed and CI stays catalog-independent. On any resolution error it returns FreeAnalyticsAccess together with the error, so callers fail closed while logging the degradation.

type AnalyticsView added in v1.786.146

type AnalyticsView struct {
	Scope         Scope             `json:"scope"`
	Plan          string            `json:"plan"`
	Range         string            `json:"range"`
	Start         string            `json:"start"`
	End           string            `json:"end"`
	RetentionDays int               `json:"retentionDays"`
	Export        bool              `json:"export"`
	Providers     ProviderBreakdown `json:"providers"`
}

AnalyticsView is the entitlement-gated rich read: the per-provider breakdown of the org's LLM usage over the retention-clamped window, plus the plan's retention + export decision so the console renders the right controls.

type CategorySpend

type CategorySpend struct {
	Category string `json:"category"`
	Cents    int64  `json:"cents"`
	Count    int64  `json:"count"`
}

CategorySpend is one row of the spend-by-category breakdown: a friendly category bucket, the total cents spent in the window, and how many ledger lines rolled up into it. Categories are DERIVED from the commerce ledger's own tags — never fabricated — so an unknown tag surfaces as its own honest bucket.

type LLM

type LLM struct {
	Available        bool   `json:"available"`
	Requests         int64  `json:"requests"`
	Tokens           int64  `json:"tokens"`
	PromptTokens     int64  `json:"promptTokens"`
	CompletionTokens int64  `json:"completionTokens"`
	CostCents        int64  `json:"costCents"`
	Models           int64  `json:"models"`
	Source           string `json:"source"`
}

LLM is the org's LLM usage totals from the warehouse ledger. Available=false when the datastore is not connected (honest zeros). The detailed per-model / timeseries breakdown lives at /v1/analytics/*; this is the KPI-band total.

type ProviderBreakdown added in v1.786.146

type ProviderBreakdown struct {
	Available bool          `json:"available"`
	Items     []ProviderRow `json:"items"`
	Source    string        `json:"source"`
}

ProviderBreakdown is the per-provider roll-up. Available=false is honest-empty (the datastore is not connected) — never fabricated, exactly like the summary's LLM block.

type ProviderRow added in v1.786.146

type ProviderRow struct {
	Provider  string `json:"provider"`
	Requests  int64  `json:"requests"`
	Tokens    int64  `json:"tokens"`
	CostCents int64  `json:"costCents"`
}

ProviderRow is one provider's windowed totals. BYO/fee/account columns land with the hanzoai/ai metering-write branch; see buildAnalyticsBlock for the one-line projection extension.

type Sample added in v1.801.60

type Sample struct {
	Provider string // matches Link.Provider / @hanzo/usage providerRegistry id
	Account  string // the account label (an identifier, never a secret)
	Plan     string // the plan name from the provider identity; display only
	Kind     string // subscription | apikey (validKind) — mirrors Link.Kind
	Machine  string // the machine that OBSERVED this (an attribute, not a key)

	Lane          string    // the meter's lane id (five_hour|seven_day_opus|…) — the identity
	Window        string    // the canonical class (6h|day|week|month)
	WindowMinutes int32     // the duration the meter reported (300 for Claude's 5h)
	WindowStart   time.Time // WHICH instance of the window this measures — the dedup key
	ResetsAt      time.Time // when the window resets (RateWindow.resetsAt); zero = unknown

	// UsedPct is the lane's used percent, 0..100 — RateWindow.usedPercent. For a
	// subscription account this is THE quota signal and often the ONLY one.
	//
	// QUOTA GAP (deliberate, reported, not faked): there is NO absolute
	// used/limit token quota column, because nothing can populate one. The meter
	// reports a percent, not a limit (Claude: dataConfidence `percentOnly`). The
	// hanzoai/plans catalog expresses `ai.requests_per_min` / `ai.tokens_per_min`
	// — per-MINUTE rate limits, a different concept from a window quota — and in
	// any case it is HANZO's own plan catalog, which cannot know what Anthropic
	// grants a Claude Max plan. An always-zero quota_limit would read as "the
	// limit is zero"; absent is honest. It lands as an additive column when a
	// source for it exists.
	UsedPct    float64
	Confidence string // exact|estimated|percentOnly|unknown — see the const block
	// Synthetic marks a lane the meter FABRICATED (RateWindow.isSyntheticPlaceholder
	// — set when a provider returned null for a lane and the adapter filled it in).
	// Carried so a made-up lane is never rendered as observed truth.
	Synthetic bool

	// Absolute counters — UsageTotals. Present only when the source really reports
	// them (`exact`); zero otherwise, which Confidence disambiguates.
	Requests          int64
	InputTokens       int64
	OutputTokens      int64
	TotalTokens       int64
	CachedInputTokens int64

	// Money — ProviderCostSnapshot, in minor units (cents). CostCents is what the
	// PROVIDER says this account spent (Claude's extra_usage overage; 0 for a flat
	// subscription — the plan is the charge). CostLimitCents is that meter's budget
	// (ProviderCostSnapshot.limit) — a MONEY cap, never a token quota. Neither is a
	// Hanzo charge: this plane holds no metering client.
	CostCents      int64
	CostLimitCents int64
	Currency       string // ProviderCostSnapshot.currencyCode; "" = unknown
}

Sample is one metering lane's consumption of one provider account at one observation.

TENANCY IS NOT ON THE VALUE. Org and subject are bound by the server from the validated principal at the boundary, so a client cannot assert whose usage this is — the shape makes cross-tenant writes unrepresentable rather than merely refused.

func (Sample) Sanitize added in v1.801.60

func (s Sample) Sanitize(now time.Time) Sample

Sanitize bounds every field so a warehouse row stays small, finite, and well-formed no matter what a client sends: strings trimmed and length-capped, counters non-negative and clamped, percents coerced into [0,100] even for NaN/Inf, instants bounded to a sane window around `now`, and the two enums defaulted rather than trusted. It is TOTAL (never errors), so the write path always has a safe value — validation of the CLOSED vocabularies (window, kind) is the boundary's job and is a 400, because silently rewriting a caller's window class would corrupt their dash.

It does NOT set the observation clock: the server stamps that, so a client can never backdate a sample or pin a stale one as newest.

type Scope

type Scope struct {
	Org  string `json:"org"`
	User string `json:"user,omitempty"` // the caller's subject; whose linked-account rows the accounts block carries
}

type SourceState added in v1.801.60

type SourceState struct {
	Available bool   `json:"available"`
	Scope     string `json:"scope"`
	Source    string `json:"source"` // the table of record
	Note      string `json:"note"`
}

SourceState is one side of the account board's availability + labelling: which ledger answered, at what scope, and a human note.

type Sources

type Sources struct {
	Commerce  bool `json:"commerce"`
	Warehouse bool `json:"warehouse"`
}

Sources reports which upstreams actually answered, so the console can badge a board as "connected" vs "no data yet" honestly instead of showing a fabricated zero as if it were real.

type Spend

type Spend struct {
	Available      bool            `json:"available"`
	TotalCents     int64           `json:"totalCents"`
	MTDCents       int64           `json:"mtdCents"`
	OverageCents   int64           `json:"overageCents"`
	BalanceCents   int64           `json:"balanceCents"`
	AvailableCents int64           `json:"availableCents"`
	ByCategory     []CategorySpend `json:"byCategory"`
	Series         []SpendPoint    `json:"series"`
	Source         string          `json:"source"`
}

Spend is the cost roll-up: the genuinely-missing aggregation. TotalCents is the windowed consumption (self-consistent with ByCategory + Series); MTDCents is commerce's authoritative month-to-date figure; Balance/Available are the prepaid wallet the gateway debits. Available=false means commerce was unconfigured or unreachable — honest zeros, never fabricated spend.

type SpendPoint

type SpendPoint struct {
	T     string `json:"t"` // RFC3339 bucket start (UTC)
	Cents int64  `json:"cents"`
}

SpendPoint is one time bucket of consumption (usage/withdrawal cents).

type Summary

type Summary struct {
	Range    string   `json:"range"`
	Start    string   `json:"start"`
	End      string   `json:"end"`
	Interval string   `json:"interval"`
	Scope    Scope    `json:"scope"`
	Spend    Spend    `json:"spend"`
	LLM      LLM      `json:"llm"`
	Accounts Accounts `json:"accounts"`
	Sources  Sources  `json:"sources"`
}

Summary is the whole own-scoped usage footprint roll-up over one window: the cost roll-up (spend) + the org's LLM usage totals (llm) + the caller's linked-account board (accounts). Spend/LLM are org-scoped; the account board is the caller's own (org+subject). One screen, one authoritative money source, one window.

type Total added in v1.801.60

type Total struct {
	Source     string
	Scope      string
	Provider   string
	Window     string
	Requests   int64
	Tokens     int64
	CostCents  int64
	UsedPct    float64
	Confidence string
	Windows    int64
}

Total is one provider's usage in the global view, from ONE source at ONE scope.

type TotalView added in v1.801.60

type TotalView struct {
	Source     string  `json:"source"` // account | hanzo
	Scope      string  `json:"scope"`  // user | org
	Provider   string  `json:"provider"`
	Window     string  `json:"window,omitempty"`
	Requests   int64   `json:"requests,omitempty"`
	Tokens     int64   `json:"tokens,omitempty"`
	CostCents  int64   `json:"costCents,omitempty"`
	UsedPct    float64 `json:"usedPct,omitempty"`
	Confidence string  `json:"confidence"`
	Windows    int64   `json:"windows,omitempty"`
}

TotalView is one row of the account-usage board (the summary's `accounts.rows`). Source and scope are what keep the board honest — see the Source/Scope consts.

Jump to

Keyboard shortcuts

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