Documentation
¶
Overview ¶
Package usage defines usage tracking types and service.
Index ¶
- Constants
- Variables
- func BucketStart(t time.Time, b Bucket) time.Time
- func PeriodStart(period string, now time.Time) (time.Time, error)
- func ValidateMetadata(m map[string]string) error
- type Bucket
- type ModelUsage
- type Outcome
- type PricingStatus
- type ProviderUsage
- type QueryOptions
- type QueryResult
- type Record
- type SeriesOptions
- type SeriesPoint
- type Service
- type Store
- type Summary
- type SummaryRow
Constants ¶
const ( // MaxMetadataPairs bounds the labels one record, or one filter, carries. MaxMetadataPairs = 16 // MaxMetadataValueBytes bounds one label value. MaxMetadataValueBytes = 256 )
Variables ¶
var ErrInvalidMetadata = errors.New("nexus: invalid usage metadata")
ErrInvalidMetadata reports record metadata or a metadata filter that breaks the limits above or uses a key outside the allowed pattern.
var ErrInvalidPeriod = errors.New("nexus: period must be day, week or month")
ErrInvalidPeriod reports a period other than day, week or month.
var ErrInvalidSeries = errors.New("nexus: invalid series")
ErrInvalidSeries reports a range or bucket a series cannot be built for.
Functions ¶
func BucketStart ¶
BucketStart truncates t, in UTC, to the start of its bucket.
func PeriodStart ¶
PeriodStart is the UTC instant a period began as of now: midnight UTC for "day", seven days before now for "week", the first of the month at midnight UTC for "month". Every backend uses it, so none computes "now" in its own time zone.
func ValidateMetadata ¶
ValidateMetadata checks m as record metadata or as a filter: at most MaxMetadataPairs pairs, keys matching ^[A-Za-z0-9_-]{1,64}$ and values of at most MaxMetadataValueBytes bytes. A nil or empty map is valid.
Types ¶
type ModelUsage ¶
type ModelUsage struct {
Requests int `json:"requests"`
Tokens int `json:"tokens"`
CostUSD money.USD `json:"cost_usd"`
Unpriced int `json:"unpriced"`
}
ModelUsage is usage aggregated by model.
type Outcome ¶
type Outcome string
Outcome says what happened to a request.
const ( // OutcomeOK: a provider answered. OutcomeOK Outcome = "ok" // OutcomeCached: served from the response cache; no provider was called. OutcomeCached Outcome = "cached" // OutcomeBlocked: a guard refused the request; BlockedBy names the guard. OutcomeBlocked Outcome = "blocked" // OutcomeRefused: authentication or a quota refused the request; // RefusalCode says which. OutcomeRefused Outcome = "refused" // OutcomeError: the provider call failed. OutcomeError Outcome = "error" )
type PricingStatus ¶
type PricingStatus string
PricingStatus says whether a request's cost is known, and if not, why.
const ( // PricingPriced: the cost is the model's list price over the tokens the // provider reported. PricingPriced PricingStatus = "priced" // PricingUnpricedModel: the model has no price for the tokens used, so // the cost is unknown. It is never reported as $0. PricingUnpricedModel PricingStatus = "unpriced_model" // PricingCached: served from the response cache, so no provider was // called and the cost is exactly $0. PricingCached PricingStatus = "cached" // PricingNotCharged: no provider was called (the request was refused, // blocked before the call, or failed before reaching one), so the cost // is exactly $0. PricingNotCharged PricingStatus = "not_charged" // PricingUnknown: a provider was called but the request failed, so // whether and what it charged is unknown. The cost is nil, never $0. PricingUnknown PricingStatus = "unknown" )
func (PricingStatus) CostUnknown ¶
func (s PricingStatus) CostUnknown() bool
CostUnknown reports whether a record with this status has no known cost.
type ProviderUsage ¶
type ProviderUsage struct {
Requests int `json:"requests"`
Tokens int `json:"tokens"`
CostUSD money.USD `json:"cost_usd"`
Unpriced int `json:"unpriced"`
}
ProviderUsage is usage aggregated by provider.
type QueryOptions ¶
type QueryOptions struct {
TenantID string `json:"tenant_id,omitempty"`
KeyID string `json:"key_id,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
Outcome Outcome `json:"outcome,omitempty"`
StartTime time.Time `json:"start_time,omitzero"`
EndTime time.Time `json:"end_time,omitzero"`
Limit int `json:"limit,omitempty"`
Cursor string `json:"cursor,omitempty"`
// Metadata keeps records whose metadata holds every pair given, with an
// exactly equal value. An empty map filters nothing.
Metadata map[string]string `json:"metadata,omitempty"`
}
QueryOptions filters the request log. Every filter is applied by the store, never after reading a window, so an empty page means nothing matched. An empty TenantID means every tenant. EndTime is exclusive.
type QueryResult ¶
QueryResult is one page of records. NextCursor is "" on the last page.
type Record ¶
type Record struct {
ID id.UsageID `json:"id"`
// TenantID, KeyID and RequestID are id.Nil when the request was not
// attributed to them.
TenantID id.TenantID `json:"tenant_id"`
KeyID id.KeyID `json:"key_id"`
RequestID id.RequestID `json:"request_id"`
Provider string `json:"provider"`
Model string `json:"model"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
// CostUSD is nil when the request could not be priced. It is never $0
// standing in for "unknown"; PricingStatus says why it is missing.
CostUSD *money.USD `json:"cost_usd"`
PricingStatus PricingStatus `json:"pricing_status"`
Outcome Outcome `json:"outcome"`
BlockedBy string `json:"blocked_by,omitempty"`
RefusalCode string `json:"refusal_code,omitempty"`
Latency time.Duration `json:"latency"`
Cached bool `json:"cached"`
StatusCode int `json:"status_code"`
CreatedAt time.Time `json:"created_at"`
// Metadata is the caller's labels for the request (for example the
// project and run it belongs to). See ValidateMetadata for the limits.
Metadata map[string]string `json:"metadata,omitempty"`
}
Record captures a single API call's usage.
type SeriesOptions ¶
type SeriesOptions struct {
TenantID string `json:"tenant_id,omitempty"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Bucket Bucket `json:"bucket"`
// Metadata keeps records whose metadata holds every pair given, with an
// exactly equal value. An empty map filters nothing.
Metadata map[string]string `json:"metadata,omitempty"`
}
SeriesOptions asks for usage between Start and End (exclusive), one point per bucket. An empty TenantID means every tenant.
Start is rounded down to the start of its bucket, so every point covers its whole bucket and its label is true. End is used as given: when it is not on a bucket boundary the last bucket covers usage up to End only.
type SeriesPoint ¶
type SeriesPoint struct {
Start time.Time `json:"start"`
Requests int `json:"requests"`
Tokens int `json:"tokens"`
CostUSD money.USD `json:"cost_usd"`
Unpriced int `json:"unpriced"`
}
SeriesPoint is usage within one bucket. CostUSD sums priced requests; Unpriced counts the requests it leaves out.
func FillSeries ¶
func FillSeries(opts *SeriesOptions, points []SeriesPoint) ([]SeriesPoint, error)
FillSeries merges the points a store found (possibly one per record) into one point per bucket from Start to End, oldest first, with empty buckets present as zeros.
type Service ¶
type Service interface {
Record(ctx context.Context, rec *Record) error
MonthlySpend(ctx context.Context, tenantID string) (money.USD, error)
DailyRequests(ctx context.Context, tenantID string) (int, error)
Summary(ctx context.Context, tenantID string, period string) (*Summary, error)
Series(ctx context.Context, opts *SeriesOptions) ([]SeriesPoint, error)
Query(ctx context.Context, opts *QueryOptions) (*QueryResult, error)
}
Service tracks and queries usage data. Wherever a method takes a tenant id, "" means every tenant, including requests attributed to none. DailyRequests counts every request recorded since midnight UTC except refused ones, so refusals never use up the quota.
type Store ¶
type Store interface {
Insert(ctx context.Context, rec *Record) error
MonthlySpend(ctx context.Context, tenantID string) (money.USD, error)
DailyRequests(ctx context.Context, tenantID string) (int, error)
Summary(ctx context.Context, tenantID string, period string) (*Summary, error)
Series(ctx context.Context, opts *SeriesOptions) ([]SeriesPoint, error)
Query(ctx context.Context, opts *QueryOptions) (*QueryResult, error)
}
Store is the persistence interface for usage records. Wherever a method takes a tenant id, "" means every tenant, including requests attributed to none. DailyRequests counts every request recorded since midnight UTC except refused ones, so refusals never use up the quota. Summary returns ErrInvalidPeriod for a period other than day, week or month.
type Summary ¶
type Summary struct {
// TenantID is "" when the summary covers every tenant, unattributed
// requests included.
TenantID string `json:"tenant_id"`
Period string `json:"period"`
TotalRequests int `json:"total_requests"`
TotalTokens int `json:"total_tokens"`
// TotalCostUSD sums priced requests only. UnpricedRequests counts the
// requests it leaves out because their cost is unknown.
TotalCostUSD money.USD `json:"total_cost_usd"`
UnpricedRequests int `json:"unpriced_requests"`
CacheHitRate float64 `json:"cache_hit_rate"` // share of requests served from cache, 0 to 1
AvgLatency time.Duration `json:"avg_latency"`
ByProvider map[string]*ProviderUsage `json:"by_provider"`
ByModel map[string]*ModelUsage `json:"by_model"`
ByOutcome map[Outcome]int `json:"by_outcome"`
}
Summary aggregates usage over a period.
func BuildSummary ¶
func BuildSummary(tenantID, period string, rows []SummaryRow) *Summary
BuildSummary turns grouped rows into a Summary. Every backend calls it, so the rules for what counts as unpriced, cached or average live in one place.
type SummaryRow ¶
type SummaryRow struct {
Provider string
Model string
Outcome Outcome
PricingStatus PricingStatus
Cached bool
Requests int
Tokens int
LatencyNs int64
Cost money.USD
}
SummaryRow is one group of records a backend aggregated: requests sharing provider, model, outcome, pricing status and cache flag. A backend that cannot group (SQLite, memory) passes one row per record with Requests 1. Cost sums priced records only; it is zero for an unpriced group.