Documentation
¶
Overview ¶
Package usage owns per-request usage reports and billing adjustments. These records support activity results, exports, and reporting totals. They cannot grant required budget capacity.
Index ¶
- Constants
- Variables
- func Window(interval string, at time.Time) (time.Time, time.Time)
- type Adjustment
- type AdjustmentReader
- type AdjustmentWriter
- type BillingAdjustment
- type Cost
- type Extraction
- type Media
- type Options
- type Page
- type Query
- type Record
- type Repository
- type Scope
- type Sink
- type SinkOptions
- type Tokens
- type Totals
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" // CostReasonInvalidUsage reports inconsistent provider usage dimensions. CostReasonInvalidUsage = "invalid_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 ( // TimingGatewayService bounds LatencyMS: from the start of the proxy // service to the response, or to the end of a stream. TimingGatewayService = "gateway_service" // TimingGatewayAdded bounds OverheadMS: the gateway service time minus // the upstream provider waits inside it. TimingGatewayAdded = "gateway_added" )
Timing boundaries name what a record timing measures. Neither covers the complete gateway path: authentication, limits, budgets, request decoding, and the HTTP write before the proxy service starts are outside both, so a reader must label a value from them as a partial measurement.
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 ( // ErrRecordConflict refuses different contents for an existing usage identity. ErrRecordConflict = errors.New("usage identity has different contents") // ErrRecordExpired refuses replay after the record's retention deadline. ErrRecordExpired = errors.New("usage record retention deadline passed") )
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 Adjustment ¶ added in v1.3.0
type Adjustment struct {
ID string `json:"id"`
PreviousID string `json:"previous_id,omitempty"`
Original Record `json:"original"`
RecordedAt time.Time `json:"recorded_at"`
Cost Cost `json:"cost"`
Tokens int64 `json:"tokens"`
BillingDisposition string `json:"billing_disposition"`
}
Adjustment preserves the original usage and one corrected billing decision. ID and PreviousID order decisions. RecordedAt comes from the durable job audit. Private operator evidence belongs to that audit, not to usage reporting.
func (Adjustment) Validate ¶ added in v1.3.0
func (a Adjustment) Validate() error
Validate checks billing units, identity, and the original reporting record.
type AdjustmentReader ¶ added in v1.3.0
type AdjustmentReader interface {
InspectAdjustment(ctx context.Context, original Record, id string) (Adjustment, error)
}
AdjustmentReader reads a correction within its original usage identity.
type AdjustmentWriter ¶ added in v1.3.0
type AdjustmentWriter interface {
Adjust(context.Context, Adjustment) error
}
AdjustmentWriter changes reporting totals without changing required budgets. Exact retries preserve the accepted receipt and never add another request.
type BillingAdjustment ¶ added in v1.3.0
type BillingAdjustment struct {
OriginalTokensUnknown bool `json:"original_tokens_unknown,omitempty"`
OriginalTokensEstimated bool `json:"original_tokens_estimated,omitempty"`
ID string `json:"id"`
PreviousID string `json:"previous_id,omitempty"`
RecordedAt time.Time `json:"recorded_at"`
OriginalCost *Cost `json:"original_cost,omitempty"`
OriginalTokens Tokens `json:"original_tokens"`
OriginalBillingDisposition string `json:"original_billing_disposition,omitempty"`
}
BillingAdjustment identifies the revised billing fields in an activity result. Provider measurements remain in the original record and its media fields.
type Extraction ¶ added in v1.3.0
type Extraction struct {
ProcessedPages int64 `json:"processed_pages,omitzero"`
ProcessedPagesKnown bool `json:"processed_pages_known,omitzero"`
TokensUnknown bool `json:"tokens_unknown,omitempty"`
CacheReadTokensUnknown bool `json:"cache_read_tokens_unknown,omitempty"`
StartedAt time.Time `json:"started_at,omitzero"`
Offering string `json:"offering"`
GenerationID string `json:"generation_id,omitempty"`
Pages int64 `json:"pages"`
BillingBasis string `json:"billing_basis,omitempty"`
Tokens *Tokens `json:"tokens,omitempty"`
Cost *Cost `json:"cost,omitempty"`
}
Extraction records one fresh provider recognition call. Cache hits add no call.
func (Extraction) Clone ¶ added in v1.3.0
func (e Extraction) Clone() Extraction
Clone copies measurements and cost so asynchronous persistence owns its data.
type Media ¶
type Media struct {
// ImageSize retains the requested dimensions for image valuation.
ImageSize string `json:"image_size,omitempty"`
ImagesEdited bool `json:"images_edited,omitzero"`
GeneratedImages int64 `json:"generated_images,omitempty"`
// GeneratedVideos counts completed outputs. It does not establish billing units.
GeneratedVideos int64 `json:"generated_videos,omitempty"`
// VideoOutputSeconds preserves provider duration, including an explicit zero.
VideoOutputSeconds int64 `json:"video_output_seconds,omitempty"`
VideoOutputSecondsKnown bool `json:"video_output_seconds_known,omitzero"`
}
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 lifetime from the event timestamp for records and
// from each window end for counters. Zero selects DefaultRetention.
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 {
// BillingAdjustment identifies corrected billing and preserves the original values.
BillingAdjustment *BillingAdjustment `json:"billing_adjustment,omitempty"`
// BillingDisposition distinguishes administrator decisions from provider measurements.
BillingDisposition string `json:"billing_disposition,omitempty"`
// InputCharacters preserves the measured Unicode code-point count for speech.
InputCharacters int64 `json:"input_characters,omitempty"`
InputCharactersKnown bool `json:"input_characters_known,omitzero"`
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"`
// SearchUnitsKnown preserves explicit zero separately from missing usage.
SearchUnitsKnown bool `json:"search_units_known,omitzero"`
// TokensEstimated marks counts the gateway synthesized with a
// tokenizer because the provider reported none.
TokensEstimated bool `json:"tokens_estimated,omitempty"`
// TokensUnknown marks incomplete or invalid provider token evidence.
TokensUnknown bool `json:"tokens_unknown,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 reports document size independently of billing units.
// A cache hit adds no recognized pages.
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"`
// Extractions retain each recognition measurement and its selected offering.
Extractions []Extraction `json:"extractions,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 atomically persists one record and its aggregate changes.
// An exact retry is idempotent during retention. Conflicting reuse refuses.
// Preserve the event timestamp and all recorded values across retries.
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.