metering

package
v1.801.350 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: Apache-2.0, MIT Imports: 17 Imported by: 0

README

metering — the one way to meter usage to commerce

github.com/hanzoai/commerce/metering is the single, reusable hook every Hanzo product uses to charge for usage. It is the DRY replacement for the balance-check and usage-record logic that was copy-pasted across the LLM/cloud path (ai/routers/filter_balance.go, ai/controllers/openai_api.go, gateway/auth_middleware.go). Any product — search, functions, storage, a CLI — imports this package and gets the proven, fail-closed billing gate plus usage recording, against commerce, the single billing source of truth.

What it does

Two operations, matching the proven cloud/gateway path:

Op Commerce endpoint Purpose
Authorize GET /v1/billing/balance (or GET /v1/billing/tier when tier-aware) Pre-request balance gate. Fail-closed by default.
Record POST /v1/billing/usage Post-request usage write (debits the user's balance ledger).

Auth is the commerce service token (admin-scoped S2S):

Authorization: Bearer ${COMMERCE_SERVICE_TOKEN}
X-IAM-Org-Id: <tenant org slug>

The token is a secret and must come from KMS (the operator wires it from a KMS-backed secret into COMMERCE_SERVICE_TOKEN). This package never reads it from disk.

Use it (middleware — the common case)

meter, _ := metering.FromEnv() // COMMERCE_URL + COMMERCE_SERVICE_TOKEN (KMS) + COMMERCE_SERVICE_ORG

mux.Use(meter.Middleware(metering.MiddlewareConfig{
    Provider: "search",
    Price: func(r *http.Request, status int, in metering.AuthInput) int64 {
        if status >= 200 && status < 300 { return 5 } // 5¢ per successful request
        return 0
    },
    Skip: func(r *http.Request) bool { return r.URL.Path == "/healthz" },
}))

Middleware is plain func(http.Handler) http.Handler, so it composes with the standard library, gorilla/mux (.Use), chi, and anything that speaks http.Handler — no per-framework variants.

It reads the caller identity from the gateway-minted X-User-Id / X-Org-Id headers (the trust boundary). The gateway strips client-supplied copies on ingress, so these are safe to trust downstream.

Use it (imperative — per-unit pricing)

in := metering.IdentityFromGatewayHeaders(r)
if err := meter.Authorize(ctx, in); err != nil {
    // ErrInsufficientBalance -> 402 ; other -> 503 (fail-closed)
}
// ... do work, measure cost ...
meter.Record(ctx, metering.Usage{User: in.User, Org: in.Org, AmountCents: cents, Provider: "functions"})

Configuration (env, operator-wired)

Var Default Notes
COMMERCE_URL http://commerce.hanzo.svc.cluster.local:8001 Commerce base (no /v1 suffix).
COMMERCE_SERVICE_TOKEN Admin-scoped S2S token. KMS-sourced.
COMMERCE_SERVICE_ORG hanzo Default tenant org (X-IAM-Org-Id).
METERING_TIER_AWARE false Gate on effectiveAvailable (prepaid + included plan allotment).
METERING_FAIL_OPEN false Allow-on-error. Leave false for paid products.
METERING_DISABLED false Force not-configured mode for local dev.

Fail-closed contract (aligned with the gateway)

Authorize returns:

  • nil → allow.
  • ErrInsufficientBalance → out of funds → map to 402.
  • any other error → balance unknown → fail-closed: deny → map to 503 (set FailOpen to allow instead).

When no COMMERCE_URL is configured the client is in "not configured" mode: Authorize allows and Record is a no-op, so a product can ship the wrap before its tenant billing is wired.

This is the same balance source and the same status mapping the gateway uses — no divergent logic.

Documentation

Overview

Package metering is how any product charges for usage: check the balance before, record the cost after.

It is the ONE way every Hanzo product meters usage to commerce — the single billing source of truth — so that every product (not only the LLM/cloud path) can be paid for.

It provides two operations, matching the proven cloud/gateway path:

  • Authorize: a pre-request balance gate. Fail-closed by default — if the balance cannot be determined the request is denied, exactly like the gateway's prepaid-balance gate (gateway/auth_middleware.go). With TierAware enabled it consults the tier-aware effective balance, which folds in the tenant's included plan allotment (e.g. the free-tier daily credit) so included usage is honored before prepaid funds.

  • Record: a post-request usage write. Records a usage event (cost in cents) against commerce, which debits the user's balance ledger.

The HTTP contract is commerce's canonical billing API, mounted under /v1 (commerce/api/billing/handlers.go):

GET  {BaseURL}/v1/billing/balance?user={user}&currency={cur}
GET  {BaseURL}/v1/billing/tier?user={user}            (tier-aware)
POST {BaseURL}/v1/billing/usage

Auth is the commerce service token (admin-scoped S2S), sent as

Authorization: Bearer {Token}

plus the tenant org as the X-Org-Id header. The token is a secret and MUST be sourced from KMS (never plaintext); this package never reads it from disk — the caller supplies it (typically from an env var the operator wires from a KMS-backed secret, e.g. COMMERCE_SERVICE_TOKEN).

Its only intra-repo dependency is the in-process finance seam (clients/finance): when a co-resident finance ledger is published, Authorize's balance read and Record's usage debit resolve it DIRECTLY (a typed in-proc call, no HTTP); otherwise both fall back to the commerce billing HTTP contract above. It pulls in NO commerce server internals, so any product — Go service, CLI, or job — can meter through it: it is the canonical client for commerce's billing API.

Example

Example shows the ONE way a non-LLM product opts into pay-for-everything: build the client from the operator-wired env vars and wrap the handler. Every request is then balance-gated (fail-closed) and metered to commerce.

package main

import (
	"net/http"

	"github.com/hanzoai/cloud/apps/metering"
)

func main() {
	// Token comes from a KMS-backed secret via COMMERCE_SERVICE_TOKEN — never
	// plaintext. COMMERCE_URL defaults to the in-cluster commerce address.
	meter, err := metering.FromEnv()
	if err != nil {
		panic(err)
	}

	// Price by outcome: charge a flat 5 cents per successful search request.
	priceSearch := func(r *http.Request, status int, in metering.AuthInput) int64 {
		if status >= 200 && status < 300 {
			return 5
		}
		return 0
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/search", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(`{"results":[]}`))
	})

	// One wrap meters the whole surface.
	handler := meter.Middleware(metering.MiddlewareConfig{
		Provider: "search",
		Price:    priceSearch,
		Skip:     func(r *http.Request) bool { return r.URL.Path == "/healthz" },
	})(mux)

	_ = handler // http.ListenAndServe(":8080", handler)
}
Example (Imperative)

Example_imperative shows direct use without middleware — for products that price per unit of work discovered during the request (e.g. functions billed by execution time), gating up front and recording the measured cost after.

package main

import (
	"net/http"

	"github.com/hanzoai/cloud/apps/metering"
)

func main() {
	meter, _ := metering.FromEnv()

	handle := func(w http.ResponseWriter, r *http.Request) {
		in := metering.IdentityFromGatewayHeaders(r)

		// Pre-request gate (fail-closed).
		if err := meter.Authorize(r.Context(), in); err != nil {
			if err == metering.ErrInsufficientBalance {
				http.Error(w, "insufficient balance", http.StatusPaymentRequired)
			} else {
				http.Error(w, "billing unavailable", http.StatusServiceUnavailable)
			}
			return
		}

		// ... do the work, measuring cost ...
		costCents := int64(12)

		w.WriteHeader(http.StatusOK)

		// Post-request record (best-effort).
		_, _ = meter.Record(r.Context(), metering.Usage{
			User:        in.User,
			Org:         in.Org,
			AmountCents: costCents,
			Provider:    "functions",
			RequestID:   r.Header.Get("X-Request-Id"),
			Status:      "success",
		})
	}
	_ = handle
}

Index

Examples

Constants

View Source
const (
	// EnvBaseURL is the commerce service base URL.
	// Default: http://commerce.hanzo.svc.cluster.local:8001
	EnvBaseURL = "COMMERCE_URL"

	// EnvToken is the commerce service token (admin-scoped S2S). The operator
	// wires this from a KMS-backed secret; it is NEVER stored in plaintext in
	// the repo or image.
	EnvToken = "COMMERCE_SERVICE_TOKEN"

	// EnvOrg is the default tenant org slug (X-Org-Id) for S2S calls when a
	// request carries no org. Default: hanzo.
	EnvOrg = "COMMERCE_SERVICE_ORG"

	// EnvTierAware ("true") gates on the tier-aware effective balance
	// (prepaid + included plan allotment) instead of bare prepaid balance.
	EnvTierAware = "METERING_TIER_AWARE"

	// EnvFailOpen ("true") flips the gate to allow-on-error. Default is
	// fail-closed (deny), matching the gateway. Set only where availability
	// outranks billing.
	EnvFailOpen = "METERING_FAIL_OPEN"

	// EnvDisabled ("true") forces "not configured" mode regardless of
	// COMMERCE_URL — Authorize allows, Record is a no-op. For local dev.
	EnvDisabled = "METERING_DISABLED"

	// EnvTest ("true") routes all calls to commerce's TEST ledger
	// (X-Hanzo-Test: true). For staging/sandbox so debits never hit real money.
	EnvTest = "METERING_TEST"
)

Environment variables a product reads to wire the metering Client. These are the canonical names the operator injects (the token from a KMS-backed secret); products MUST NOT invent their own. See gateway/auth_middleware.go (DefaultAuthConfig) for the same names on the gateway side.

View Source
const (
	HeaderUserID  = "X-User-Id"
	HeaderOrgID   = "X-Org-Id"
	HeaderAccount = "X-Billing-Account-Id"
)

Identity headers minted by the Hanzo gateway (the trust boundary). A product behind the gateway reads the caller identity from these — it never trusts a client-supplied value (the gateway strips them on ingress). See commerce/CLAUDE.md "Gateway Trust Headers".

View Source
const DefaultBaseURL = "http://commerce.hanzo.svc.cluster.local:8001"

DefaultBaseURL is the in-cluster commerce address. Matches the gateway's AUTH_BILLING_URL default so both gate on the same balance source.

Variables

View Source
var ErrInsufficientBalance = errors.New("metering: insufficient balance")

ErrInsufficientBalance is returned by Authorize when commerce confirms the user's available balance is non-positive. It is distinct from a connectivity failure so callers can map it to HTTP 402 (vs 503 for "unknown").

View Source
var ErrSpendCapExceeded = errors.New("metering: spend cap exceeded")

ErrSpendCapExceeded is returned by Authorize when the caller is FUNDED but a configured per-scope spend cap (issue #70) would be exceeded by this request. It is DISTINCT from ErrInsufficientBalance: the balance is fine, the tenant's own policy ceiling is not — callers map it to a 402 spend_cap_exceeded, not the out-of-funds insufficient_balance.

View Source
var OnCapError func(error)

OnCapError, when set, is called (best-effort) whenever the cap check FAILS OPEN — a timeout or any error on the authorize call. It lets the host log/alert on a degraded cap without this leaf package taking a logger dependency. nil = no-op.

Functions

This section is empty.

Types

type AuthInput

type AuthInput struct {
	User     string
	Actor    string
	Org      string
	Currency string
	// Amount, when non-zero, gates on available >= Amount instead of the bare
	// available > 0. Use it to authorize a known up-front charge (e.g. the first
	// hour of a machine) so a 1-cent balance cannot green-light an arbitrarily
	// expensive request. Zero preserves the "any positive balance" gate.
	//
	// It is the exact, typed value — the same one Usage.Amount carries, at the
	// ledger's own 18-decimal precision — so the gate and the debit that follows
	// it weigh the SAME number. A cents-rounded gate admitted a charge the debit
	// then wrote in full, which is how a sub-cent price gets authorized against a
	// figure nobody spent.
	Amount money.Amount

	// AmountCents is the same charge in whole cents, for the HTTP path to
	// commerce and for callers that have not got a typed value. Amount wins when
	// both are set.
	AmountCents int64

	// Project and Service scope the per-scope spend cap + rate limit (issue #70).
	// Service is server-derived (route/provider). Empty = the org-wide default
	// scope. Forwarded to commerce so the right scope cap is resolved; they never
	// change which BALANCE is gated — that is the address (Org, User), always.
	Project string
	Service string

	// ProjectValidated reports whether Project is bound to a VALIDATED identity
	// claim. When false, commerce DEGRADES a project-scoped hard cap to a soft warn
	// (records + warns, never 402) so a forgeable X-Project-Id can neither hard-stop
	// nor be evaded. The org and service axes are always validated. Today IAM mints
	// no project claim, so cloud sends false; when it does, cloud sends true and
	// project caps auto-harden.
	ProjectValidated bool
}

AuthInput identifies who to authorize.

(Org, User) IS THE MONEY'S ADDRESS: Org names the LEDGER that holds the balance, User the ACCOUNT within it. Both halves are resolved by the ONE rule — principal.WalletOf, which is hanzoai/account.Payer — and a caller passes what it resolved, never a re-derivation of its own.

User is therefore the payer's SUBJECT, not "the org slug". For a pooled tenant the two coincide, because Payer answers the org itself and finance reads the org pool from a bare slug — which is why "User is always the org" held for years and why it was wrong: in the shared signup org, whose members are strangers to each other, Payer answers "<org>/<name>" and the pool is a balance that member neither owns nor can spend. A gate keyed on the org there checks a pool while the debit spends a person, and clients/principal/wallet.go catalogues what that costs.

A caller that legitimately holds only an org — a resource meter billing an org's build minutes, say — passes the org and gets the pool; that is the same rule, answered for an org credential, not an exception to it.

Actor is the full "org/sub" identity (e.g. "hanzo/alice") recorded on the usage transaction for the audit trail. It is ATTRIBUTION ONLY: for a machine key the payer is the org while the actor is the key, so the two axes are never each other.

Currency defaults to "usd".

func IdentityFromGatewayHeaders

func IdentityFromGatewayHeaders(r *http.Request) AuthInput

IdentityFromGatewayHeaders builds an AuthInput from the gateway-minted identity headers. User — the account this request pays from — is resolved by the ONE rule every layer that touches money shares (hanzoai/account.Payer), so this client cannot key a different account than the gate that authorizes the request or the ledger that records it. The full "{org}/{sub}" identity is recorded as Actor for the audit trail only; it never decides which balance is gated.

When there is no org (anonymous / org-less token) User falls back to the bare sub so a per-user balance can still gate; without either, User is empty and the fail-closed gate denies (anonymous traffic must be bypassed via Skip).

type Client

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

Client meters usage to commerce. It is safe for concurrent use.

func FromEnv

func FromEnv() (*Client, error)

FromEnv builds a Client from the canonical environment variables. This is the one-liner products use at startup:

meter, _ := metering.FromEnv()
mux.Use(meter.Middleware(metering.MiddlewareConfig{Provider: "search", Price: priceSearch}))

func New

func New(cfg Config) (*Client, error)

New builds a metering Client from cfg. It returns an error only for an unparseable BaseURL; an empty BaseURL is valid ("not configured" mode).

func (*Client) Authorize

func (c *Client) Authorize(ctx context.Context, in AuthInput) error

Authorize is the pre-request gate. It is the thin error-mapping wrapper over AuthorizeVerdict, preserving the proven three-outcome contract:

(nil)                       -> allow.
(ErrInsufficientBalance)    -> deny: out of funds          (map to HTTP 402).
(ErrSpendCapExceeded)       -> deny: funded but over a per-scope cap (HTTP 402
                               spend_cap_exceeded — distinct from out-of-funds).
(other error)               -> balance unknown; with the default fail-closed
                               posture this denies          (map to HTTP 503).
                               With FailOpen it returns nil (allow).

When the client is not configured (no BaseURL) it always allows.

func (*Client) AuthorizeVerdict

func (c *Client) AuthorizeVerdict(ctx context.Context, in AuthInput) (Verdict, error)

AuthorizeVerdict is the full pre-request gate: it checks FUNDS first (the money-safety backstop, honoring the fail-open/closed posture on a connectivity error) and, only when funded, layers the per-scope SPEND CAP verdict.

Spend caps are a POLICY OVERLAY, not a funds check: the balance gate already prevents overspending real money, so a cap-endpoint failure FAILS OPEN (degrades to funds-only gating) regardless of the funds fail posture — a commerce limits blip must never take down all paid traffic. An older commerce without the endpoint (404) is likewise treated as "no cap configured".

The returned WarnPct (>0 when at/over a covering cap's soft threshold) lets the caller emit X-Spend-Warn from this one round trip.

func (*Client) Enabled

func (c *Client) Enabled() bool

Enabled reports whether a commerce BaseURL is configured. When false, Authorize always allows and Record is a no-op.

func (*Client) Middleware

func (c *Client) Middleware(cfg MiddlewareConfig) func(http.Handler) http.Handler

Middleware returns net/http middleware that gates every request on the caller's commerce balance (fail-closed by default) and records usage after a successful response. It is the ONE way a non-LLM product opts into pay-for-everything: wrap the handler once and every request is metered.

It is plain net/http middleware (func(http.Handler) http.Handler), so it composes with the standard library, gorilla/mux (.Use), chi, and anything that speaks http.Handler — no per-framework variants.

func (*Client) Record

func (c *Client) Record(ctx context.Context, u Usage) (*RecordResult, error)

Record writes a usage event to commerce, debiting the user's balance.

It is a no-op (nil, nil) when the client is not configured or when AmountCents <= 0 (commerce treats zero-cost usage as "skipped"). Usage recording is deliberately decoupled from gating: the work already happened and must be recorded, so balance is NOT re-checked here — exactly as commerce's RecordUsage documents.

Provider is the service name doing the metering when no model/provider is natural (e.g. "search", "functions"); set it on Usage.Provider.

func (*Client) ScopeRules

func (c *Client) ScopeRules(ctx context.Context, org string) ([]ScopeRule, error)

ScopeRules lists the org's per-scope rate-limit rules (the rate-limited subset of its spend-alert rows). It is the config source for the cloud ScopeRateLimit middleware, which caches it with a short TTL and fails open on error. Org is sent as X-Org-Id so the rules are the caller org's own — never another tenant's.

func (*Client) Tier

func (c *Client) Tier(ctx context.Context, subject, org string) (string, error)

Tier resolves the subject's commerce subscription-plan NAME (free | starter | pro | enterprise) via GET /v1/billing/tier?user=<subject>, scoped to org (X-Org-Id). This is the in-process (co-resident) — or S2S HTTP — read the embedded ai module's per-tier SKU gate consumes (via aiobject.SetTierReader) INSTEAD of an authed self-call to the cloud edge: the edge 401/403s a service call to /v1/billing/*, so the ai module's own HTTP path always returned "" in-cluster and the gate failed OPEN. This rides the SAME transport and service token the metering gate already bills over, so it reaches commerce's OWN service-token middleware (which reads the tenant from X-Org-Id), never the cloud edge.

Empty subject or a not-configured client returns ("", nil): the gate treats an unknown tier as ALLOW (fail-safe), so a commerce hiccup never locks out a paying caller. Unlike fetchAvailable this does NOT short-circuit to the finance ledger — the plan tier is a commerce subscription fact, not a wallet balance.

type Config

type Config struct {
	// BaseURL is the commerce service base, e.g.
	// "http://commerce.hanzo.svc.cluster.local:8001". No trailing /v1 — the
	// client appends the canonical billing paths itself.
	BaseURL string

	// Token is the commerce service token (admin-scoped). MUST come from KMS;
	// never hard-code or read from a file. Sent as "Authorization: Bearer".
	Token string

	// Org is the tenant org slug (e.g. "hanzo") sent as X-Org-Id so
	// commerce resolves the right tenant namespace. Per-request Org on the
	// Usage/AuthInput overrides this default.
	Org string

	// TierAware, when true, makes Authorize consult GET /v1/billing/tier and
	// gate on the effective balance (prepaid + included plan allotment such as
	// the free-tier daily credit) instead of the bare prepaid balance. This is
	// the same effectiveAvailable commerce computes in GetTier.
	TierAware bool

	// FailOpen inverts the default fail-closed posture: when commerce cannot be
	// reached, Authorize allows the request instead of denying it. Leave false
	// for paid products; set true only where availability outranks billing
	// (and accept the revenue leak). Mirrors the gateway, which is fail-closed.
	FailOpen bool

	// Test routes every call to commerce's TEST ledger (X-Hanzo-Test: true) so
	// balances and debits hit the sandbox books, not real money. Production
	// metering leaves this false. Used for end-to-end proofs and staging.
	Test bool

	// Timeout bounds each commerce HTTP call. Default 5s (the gateway's value).
	Timeout time.Duration

	// HTTPClient overrides the underlying HTTP client. When nil a client with
	// Timeout is created.
	HTTPClient HTTPDoer
}

Config configures a Client. Only BaseURL is conceptually required; an empty BaseURL puts the client in "not configured" mode where Authorize allows and Record is a no-op — matching the gateway's behavior when no billing URL is set, so a product can adopt metering before its tenant billing is wired.

func ConfigFromEnv

func ConfigFromEnv() Config

ConfigFromEnv builds a Config from the canonical environment variables. It applies the in-cluster commerce default and the fail-closed/usd defaults.

type HTTPDoer

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer is the minimal HTTP surface the client needs. *http.Client satisfies it; tests and instrumented transports can substitute their own.

type MiddlewareConfig

type MiddlewareConfig struct {
	// Price computes the per-request cost in cents. Required — without it the
	// middleware would gate but never charge, which is not metering.
	Price PriceFunc

	// Provider labels the recorded usage (e.g. "search", "functions"). It is
	// stored on the commerce transaction so spend can be attributed per product.
	Provider string

	// Identify extracts the billing identity (IAM user + org) from a request.
	// Defaults to IdentityFromGatewayHeaders, which reads the gateway-minted
	// X-User-Id / X-Org-Id headers.
	Identify func(*http.Request) AuthInput

	// Skip lets a request bypass metering entirely (health checks, public
	// paths). Returning true means: no gate, no record. Optional.
	Skip func(*http.Request) bool

	// OnDenied renders the response when Authorize denies. Defaults to a JSON
	// 402 (insufficient balance) / 503 (balance unknown, fail-closed) — the
	// same status mapping the gateway uses.
	OnDenied func(w http.ResponseWriter, r *http.Request, err error)

	// OnRecordError is invoked (best-effort, async) if recording usage fails.
	// Optional — typically wired to the product's logger/metrics. The request
	// has already succeeded; recording failure must not affect the response.
	OnRecordError func(r *http.Request, u Usage, err error)
}

MiddlewareConfig configures Middleware.

type PriceFunc

type PriceFunc func(r *http.Request, status int, in AuthInput) int64

PriceFunc computes the cost (in cents) to record for a completed request. It is called AFTER the wrapped handler runs, with the captured status code and the per-request context, so it can price by outcome (e.g. charge only on success) and by work done (bytes, rows, units recorded on the context by the handler). Return 0 to record nothing.

type RecordResult

type RecordResult struct {
	TransactionID string `json:"transactionId"`
	User          string `json:"user"`
	Amount        int64  `json:"amount"`
	Currency      string `json:"currency"`
	Type          string `json:"type"`
}

RecordResult is the commerce response to a usage write.

type ScopeRule

type ScopeRule struct {
	Project      string
	Service      string
	RateLimitRpm int
}

ScopeRule is one scope's rate-limit config, consumed by the cloud ScopeRateLimit middleware. Only rows with a positive RateLimitRpm are returned.

type Usage

type Usage struct {
	User     string `json:"user"`            // the ACCOUNT half of the debit's address (see AuthInput.User) — a pooled org's slug, or the payer subject the gate authorized.
	Actor    string `json:"actor,omitempty"` // org/sub identity for the audit trail (commerce ignores unknown fields today; forward-compatible).
	Org      string `json:"-"`               // routed via X-Org-Id, not the body.
	Currency string `json:"currency,omitempty"`

	// Amount is the exact debit, typed. Not serialized: the co-resident finance
	// path reads it directly; the HTTP path derives the wire fields below from it.
	Amount money.Amount `json:"-"`

	// AmountCents is the debit in whole cents (legacy wire field). Set by older
	// callers and by Record when serializing a typed Amount for commerce. When
	// Amount is set, this is ignored on the co-resident path.
	AmountCents int64 `json:"amount"`
	// AmountMicros is the debit in micro-USD (1e6 = $1), sub-cent precision for the
	// HTTP path so a tiny per-call cost is not lost to cent rounding. Commerce
	// prefers it over AmountCents (usage.go: effMicros); when set, AmountCents may
	// be 0. Zero/absent → commerce falls back to AmountCents*10000. Ignored on the
	// co-resident path when Amount is set.
	AmountMicros int64 `json:"amountMicros,omitempty"`

	Model    string `json:"model,omitempty"`
	Provider string `json:"provider,omitempty"`
	// Project and Service attribute this debit to a scope so commerce records the
	// dimensions the per-scope spend cap sums over (issue #70). Empty = the
	// org-wide default scope.
	Project          string `json:"project,omitempty"`
	Service          string `json:"service,omitempty"`
	PromptTokens     int    `json:"promptTokens,omitempty"`
	CompletionTokens int    `json:"completionTokens,omitempty"`
	TotalTokens      int    `json:"totalTokens,omitempty"`
	RequestID        string `json:"requestId,omitempty"`
	Premium          bool   `json:"premium,omitempty"`
	Stream           bool   `json:"stream,omitempty"`
	Status           string `json:"status,omitempty"`
	ClientIP         string `json:"clientIp,omitempty"`
}

Usage is one usage event to record. The amount (the cost to debit) is the essential beside the billing key (User); the rest is descriptive metadata commerce stores on the transaction.

Amount is the debit as an exact money.Amount — the canonical, typed value, native 18-decimal USD (the co-resident finance ledger's precision). One typed value, no precedence rules: a caller that has the exact cost (zen, which prices per token at 18-dp) sets Amount directly and the co-resident path debits it with NO rounding. The legacy int64 wire fields (AmountCents, AmountMicros) remain only for the HTTP path to commerce and for older callers that build a Usage without a money.Amount; from them Record reconstructs the same money.Amount. Amount, when non-zero, always wins.

func (Usage) Money added in v1.801.350

func (u Usage) Money() money.Amount

amountMoney returns the canonical typed debit. Amount wins; otherwise the int64 wire fields are reconstructed (micros preferred, then cents) so a legacy Usage without a typed Amount still debits. The result is zero when no amount is set, which Record treats as "skip". Money is the debit this Usage carries, as the one exact value — resolving the precedence the type documents: the typed Amount when set, else micro-USD, else whole cents.

It is EXPORTED because "is there anything to bill here?" is the same question wherever it is asked, and asking it any other way gets a different answer. The resource meter asked it as `AmountCents <= 0 && AmountMicros <= 0` and so dropped, silently and before Record ever saw it, every usage priced only as a typed Amount — which is exactly the shape a per-token 18-dp caller sends. Money billed nobody and appeared nowhere: not an error, not a log, no row.

One question, one answer, one place. A caller that needs the value and a caller that only needs to know whether there IS one both read this.

type Verdict

type Verdict struct {
	Allow      bool
	Reason     string // "", "insufficient_balance", "spend_cap"
	WarnPct    int
	CapCents   int64
	SpentCents int64
}

Verdict is the full gate outcome AuthorizeVerdict returns, so a gate can render the distinct denial shapes AND emit the soft-warn header from ONE round trip.

Allow=true,  Reason="",                     WarnPct=p  -> allow; if p>0 emit X-Spend-Warn.
Allow=false, Reason="insufficient_balance"             -> 402 out of funds.
Allow=false, Reason="spend_cap", Cap/Spent            -> 402 spend cap exceeded.

Jump to

Keyboard shortcuts

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