usage

package
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2026 License: AGPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package usage owns the canonical per-request usage record: what one inference request consumed, where it ran, and what it cost. Records are written best-effort after request completion and feed the activity API, the console usage page, and budget enforcement.

Index

Constants

View Source
const (
	// StatusOK reports a completed request.
	StatusOK = "ok"
	// StatusError reports a failed request.
	StatusError = "error"
	// StatusCancelled reports a client-cancelled request.
	StatusCancelled = "cancelled"
)

Statuses classify how a request finished.

View Source
const (
	// OperationChat reports a chat completion request.
	OperationChat = "chat"
	// OperationEmbeddings reports an embeddings request.
	OperationEmbeddings = "embeddings"
	// OperationRerank reports a rerank request. It is separate from embeddings
	// because the two are metered differently: an embedding is billed by token
	// and a rerank is billed by token at one provider and by search unit at
	// another.
	OperationRerank = "rerank"
	// OperationModerations reports a moderation request. The one compiled
	// moderation provider publishes it free, so a record that reports no
	// usage and no cost is the honest account of the turn.
	OperationModerations = "moderations"
	// OperationImages reports an image generation or image edit request.
	// One name covers both, because the two are metered the same way and a
	// spend report reads the meter rather than the path.
	OperationImages = "images"
	// OperationSpeech reports a text-to-speech request.
	OperationSpeech = "speech"
	// OperationTranscription reports a speech-to-text request, in the spoken
	// language or translated.
	OperationTranscription = "transcription"
	// OperationVideos reports a video generation. It is the one operation whose
	// record is written after the request that started it returned, because the
	// work outlives that request and only its end states what it cost.
	OperationVideos = "videos"
)

Operations name the inference surface a record measures.

View Source
const (
	// CostReasonNoPricing means the catalog offering had no pricing data.
	CostReasonNoPricing = "no_pricing"
	// CostReasonNoRoute means the request failed before a route was chosen.
	CostReasonNoRoute = "no_route"
	// CostReasonNoUsage means the provider returned no token counts.
	CostReasonNoUsage = "no_usage"
	// CostReasonMediaUnpriced means the turn carried a media unit the
	// offering does not price. The token half of such a turn does have a
	// price, so a cost is computable; it would omit the media half, which is
	// the expensive one. A silent understatement is worse than a named gap,
	// so the whole cost drops and this reason says why.
	CostReasonMediaUnpriced = "media_unpriced"
	// CostReasonRerankUnpriced means the turn billed a search unit the
	// offering publishes no price for. The catalog projection drops such an
	// offering, so this reason names a snapshot that reached accounting
	// without that guard rather than a gap an operator has to accept.
	CostReasonRerankUnpriced = "rerank_unpriced"
)

Cost unavailability reasons. A record without a cost carries one so the gap is loud, never a silent zero.

View Source
const (
	// StorageSchemaVersion identifies the only usage record schema.
	StorageSchemaVersion = 1
	// StoragePrefix is the usage v1 namespace.
	StoragePrefix = "usage:v1:"

	// DefaultRetention bounds how long records and counters survive.
	DefaultRetention = 30 * 24 * time.Hour
	// DefaultListLimit applies when a query gives no limit.
	DefaultListLimit = 50
	// MaxListLimit bounds one page.
	MaxListLimit = 1000
)
View Source
const (
	IntervalDay   = "day"
	IntervalWeek  = "week"
	IntervalMonth = "month"
)

Intervals for aggregate windows. Windows are fixed and UTC-aligned.

View Source
const NDJSONContentType = "application/x-ndjson"

NDJSONContentType is the media type both sinks and the activity export speak: one JSON-encoded record per line.

Variables

View Source
var (
	// ErrRepositoryRequired reports an absent usage storage adapter.
	ErrRepositoryRequired = errors.New("usage storage is required")
	// ErrInvalidQuery reports an unusable list query.
	ErrInvalidQuery = errors.New("invalid usage query")
	// ErrInvalidInterval reports an unknown aggregate interval.
	ErrInvalidInterval = errors.New("invalid usage interval")
	// ErrInvalidScope reports an aggregate scope that names no subject.
	ErrInvalidScope = errors.New("invalid usage scope")
	// ErrCorruptRecord reports invalid durable usage data.
	ErrCorruptRecord = errors.New("usage record is invalid")
)
View Source
var ErrInvalidRecord = errors.New("invalid usage record")

ErrInvalidRecord reports a record that cannot be persisted.

Functions

func Window

func Window(interval string, at time.Time) (time.Time, time.Time)

Window returns the UTC-aligned [start, end) of the interval containing at. Callers use the end to report when a fixed budget window resets.

Types

type Cost

type Cost struct {
	NanoUSD  int64  `json:"nano_usd"`
	Currency string `json:"currency"`
}

Cost is the Starmap-derived cost of one request in integer nano-USD.

type Media

type Media struct {
	GeneratedImages int64 `json:"generated_images,omitempty"`
	// GeneratedVideos counts finished videos. A provider prices a video per
	// video, not per second and not per token, so this is the whole meter for
	// the operation rather than a share of another one.
	GeneratedVideos int64 `json:"generated_videos,omitempty"`
}

Media counts the non-token units one request produced. A generated image carries no token count at all, so a token total cannot describe it, and a spend budget that reads tokens alone would meter such a turn as free.

type Options

type Options struct {
	// Retention bounds record and counter lifetime; DefaultRetention
	// when zero.
	Retention time.Duration
}

Options configure a repository.

type Page

type Page struct {
	Records []Record
	// NextCursor continues the listing; empty when the page is the last.
	NextCursor string
}

Page is one newest-first result page.

type Query

type Query struct {
	KeyID string
	// AccountID selects one account's records across every key it holds.
	// Records are keyed by gateway API key, so a query that names only an
	// account scans the record namespace and filters. That cost belongs to
	// reporting: budget enforcement reads the aggregate counters instead.
	AccountID string
	Model     string
	Provider  string
	Status    string
	// RequestID selects the one record a request left, so an audit record
	// or a log line reaches its usage row.
	RequestID string
	// GuardrailVerdict keeps only records a guardrail closed with this
	// verdict: `refuse` or `redact`. Empty places no filter.
	GuardrailVerdict string
	Since            time.Time
	Until            time.Time
	Limit            int
	// Cursor continues a previous page: the opaque NextCursor value.
	Cursor string
}

Query selects records. An empty KeyID selects every key and an empty AccountID every account (admin scope); callers own that authorization decision.

type Record

type Record struct {
	RequestID string `json:"request_id"`
	KeyID     string `json:"key_id"`
	// AccountID is the account the key belongs to. It is what an account-wide
	// spend cap counts, and a key ID cannot stand in for it because an account
	// holds many keys. It is optional so that a record written before account
	// attribution stays readable; such a record counts toward no account.
	AccountID string `json:"account_id,omitempty"`
	// TeamID is the team the serving key is attributed to, or empty for a
	// teamless key. It is what a team-wide spend cap counts: a team meters
	// every key attributed to it, across every account the team reaches, so
	// neither the key ID nor the account ID can stand in for it.
	TeamID string `json:"team_id,omitempty"`
	// BatchID names the batch this request ran inside, or is empty for an
	// online request. Every line of a batch draws its own record, and this
	// field is what sums them back into one bill.
	BatchID        string    `json:"batch_id,omitempty"`
	Timestamp      time.Time `json:"timestamp"`
	Protocol       string    `json:"protocol,omitempty"`
	Operation      string    `json:"operation"`
	ModelRequested string    `json:"model_requested,omitempty"`
	ModelUsed      string    `json:"model_used,omitempty"`
	Provider       string    `json:"provider,omitempty"`
	// CredentialSource names which credential plane paid for the request:
	// `environment`, `shared`, `byok`, or `anonymous` for a provider that
	// accepted the call without one. It is what lets an operator see an
	// account drawing on the deployment's credential rather than its own. It
	// is empty on a record written before a route was chosen, and on one
	// written before the gateway recorded the plane.
	CredentialSource string `json:"credential_source,omitempty"`
	Streaming        bool   `json:"streaming,omitempty"`
	Status           string `json:"status"`
	StatusCode       int    `json:"status_code,omitempty"`
	ErrorClass       string `json:"error_class,omitempty"`
	Tokens           Tokens `json:"tokens"`
	// Media is nil on a text turn, which is what every record written before
	// media accounting existed reads back as.
	Media *Media `json:"media,omitempty"`
	// SearchUnits counts what a rerank provider that bills by search unit
	// billed. It sits beside Media rather than inside it because reranking
	// produces no media, and it sits outside Tokens because no token total
	// converts into it: a Cohere turn reports a search unit and no tokens at
	// all, and a record that read tokens alone would meter it as free.
	SearchUnits int64 `json:"search_units,omitempty"`
	// TokensEstimated marks counts the gateway synthesized with a
	// tokenizer because the provider reported none.
	TokensEstimated bool  `json:"tokens_estimated,omitempty"`
	LatencyMS       int64 `json:"latency_ms"`
	RoutingMS       int64 `json:"routing_ms,omitempty"`
	// OverheadMS is the gateway-added latency: total handling time
	// minus upstream provider waits.
	OverheadMS int64 `json:"overhead_ms,omitempty"`
	// TTFTMS is the time from request start to the first stream event.
	// Only streamed requests carry it.
	TTFTMS      int64  `json:"ttft_ms,omitempty"`
	Attempts    int    `json:"attempts,omitempty"`
	CacheStatus string `json:"cache_status,omitempty"`
	// CacheSimilarity is the cosine similarity a semantic cache hit served
	// under. It is zero for an exact hit and for a miss.
	CacheSimilarity float64 `json:"cache_similarity,omitempty"`
	// CacheSemantic reports that the hit came from the semantic layer, so
	// a reader separates a near-duplicate answer from an exact replay
	// without a similarity threshold of its own.
	CacheSemantic bool `json:"cache_semantic,omitempty"`

	// ParserEngine names which engine read the documents this turn attached:
	// `native` for the in-process reader, `recognition` for a catalogued model.
	// It is empty on a turn that attached none.
	ParserEngine string `json:"parser_engine,omitempty"`
	// DocumentPages is how many pages those attachments held, whether this turn
	// read them or the cache answered for them.
	DocumentPages int64 `json:"document_pages,omitempty"`
	// RecognizedPages is how many of those pages this turn sent to a
	// recognition model. It is what ExtractionCost is charged for, and it is
	// zero on a cached read: the pages were recognized once, on an earlier turn
	// that paid for them.
	RecognizedPages int64 `json:"recognized_pages,omitempty"`
	// NativePages is how many pages this turn read in process. They cost
	// nothing: no provider saw them.
	NativePages int64 `json:"native_pages,omitempty"`
	// ExtractionCached reports that every attachment came back from the
	// extraction cache. A cached read and a native read both record no cost,
	// and only this field separates a page an earlier turn already paid for
	// from a page no provider ever charged for.
	ExtractionCached bool `json:"extraction_cached,omitempty"`
	// ExtractionMillis is how long the document reads took. A recognition read
	// is a provider call inside a provider call, so it is latency an operator
	// cannot find anywhere else in this record.
	ExtractionMillis int64 `json:"extraction_millis,omitempty"`
	// ExtractionCost is the recognized share of Cost below, reported on its own
	// so an operator can see what reading a document cost apart from what
	// answering about it cost. It is nil when the turn recognized nothing.
	ExtractionCost *Cost `json:"extraction_cost,omitempty"`

	// GuardrailVerdict is the strongest guardrail verdict of the turn:
	// `allow`, `redact`, or `refuse`. It is empty on a turn no guardrail
	// inspected, which is what every record written before the guardrail
	// seam existed reads back as.
	GuardrailVerdict string `json:"guardrail_verdict,omitempty"`
	// GuardrailCheck names the check behind a refusal, so an operator can
	// see which policy stopped the turn. It is empty unless the verdict
	// is `refuse`.
	GuardrailCheck string `json:"guardrail_check,omitempty"`

	// Cost is nil when no cost could be computed; CostUnavailableReason
	// then names why. It covers the whole request, the document reads
	// included.
	Cost                  *Cost  `json:"cost,omitempty"`
	CostUnavailableReason string `json:"cost_unavailable_reason,omitempty"`
}

Record is one completed inference request.

func (Record) Validate

func (r Record) Validate() error

Validate reports whether the record can be persisted.

type Repository

type Repository interface {
	// Put persists one record and advances the aggregate counters.
	Put(context.Context, Record) error
	// List returns records newest-first.
	List(context.Context, Query) (Page, error)
	// Totals reads the aggregate counters for one scope in the window
	// containing at.
	Totals(ctx context.Context, scope Scope, interval string, at time.Time) (Totals, error)
}

Repository is the durable usage contract.

func Open

func Open(store storage.KVStore, options Options) (Repository, error)

Open returns a storage-backed usage repository.

type Scope

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

Scope selects which counter set an aggregate query reads. The gateway keeps one set per key, one per account, and one for the whole deployment, because an account cap and a key cap meter different populations: the account total is the sum over every key it holds, so neither can be derived from the other.

It is comparable, so a caller may use it as a map key.

func AccountScope

func AccountScope(accountID string) Scope

AccountScope addresses the counters for one account: every key it holds.

func GatewayScope

func GatewayScope() Scope

GatewayScope addresses the counters for the whole deployment.

func KeyScope

func KeyScope(keyID string) Scope

KeyScope addresses the counters for one gateway API key.

func TeamScope added in v1.2.0

func TeamScope(teamID string) Scope

TeamScope addresses the counters for one team: every key attributed to it, across every account the team reaches.

func (Scope) String

func (s Scope) String() string

String names the scope for a log field.

type Sink added in v1.2.0

type Sink interface {
	Receive(record Record)
	Close(ctx context.Context) error
	// Dropped counts the records this sink could not deliver since it
	// opened. The admin surface reports it beside the target kind.
	Dropped() int64
}

Sink receives each finalized usage record and streams it out of the gateway. A sink batches internally and flushes on an interval and at Close. Receive must return quickly: it runs on the request completion path, before the asynchronous store write.

func NewFileSink added in v1.2.0

func NewFileSink(path string, options SinkOptions) (Sink, error)

NewFileSink appends each record as one NDJSON line to path. The file opens once and holds its handle until Close.

func NewHTTPSink added in v1.2.0

func NewHTTPSink(url string, options SinkOptions) Sink

NewHTTPSink posts each batch as an NDJSON body to url. A failed post retries a bounded number of times; a batch that never lands drops and OnDrop counts it.

type SinkOptions added in v1.2.0

type SinkOptions struct {
	// FlushInterval is how often buffered records flush without waiting for
	// Close.
	FlushInterval time.Duration
	// MaxPending bounds the buffer. When a flush target stalls long enough to
	// fill it, the oldest records drop and OnDrop counts them.
	MaxPending int
	// OnDrop observes every dropped record count. Nil observes nothing.
	OnDrop func(count int)
}

SinkOptions tune a sink. The zero value selects the defaults.

type Tokens

type Tokens struct {
	Input      int64 `json:"input"`
	Output     int64 `json:"output"`
	Total      int64 `json:"total"`
	Reasoning  int64 `json:"reasoning,omitempty"`
	CacheRead  int64 `json:"cache_read,omitempty"`
	CacheWrite int64 `json:"cache_write,omitempty"`
	// AudioInput and AudioOutput are the audio shares of Input and Output,
	// not additions to them. A provider meters audio at its own rate, so a
	// cost reclassifies these out of the plain rates rather than adding them.
	AudioInput  int64 `json:"audio_input,omitempty"`
	AudioOutput int64 `json:"audio_output,omitempty"`
}

Tokens holds provider-reported token counts for one request.

type Totals

type Totals struct {
	Requests     int64
	Tokens       int64
	SpendNanoUSD int64
}

Totals are the aggregate counters for one scope and window.

Jump to

Keyboard shortcuts

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