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 ¶
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.
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.
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.
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 )
const ( IntervalDay = "day" IntervalWeek = "week" IntervalMonth = "month" )
Intervals for aggregate windows. Windows are fixed and UTC-aligned.
const NDJSONContentType = "application/x-ndjson"
NDJSONContentType is the media type both sinks and the activity export speak: one JSON-encoded record per line.
Variables ¶
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") )
var ErrInvalidRecord = errors.New("invalid usage record")
ErrInvalidRecord reports a record that cannot be persisted.
Functions ¶
Types ¶
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"`
}
Record is one completed inference request.
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.
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 ¶
AccountScope addresses the counters for one account: every key it holds.
func GatewayScope ¶
func GatewayScope() Scope
GatewayScope addresses the counters for the whole deployment.
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.