Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,396 @@
package usage
import "strings"
// TokenAccountingSchemaVersion identifies the canonical token accounting contract.
const TokenAccountingSchemaVersion = 2
// TokenAccountingQuality describes how confidently a token total can be classified.
type TokenAccountingQuality string
const (
TokenAccountingQualityComplete TokenAccountingQuality = "complete"
TokenAccountingQualityInconsistent TokenAccountingQuality = "inconsistent"
TokenAccountingQualityUnclassified TokenAccountingQuality = "unclassified"
)
type tokenAccountingSemantics uint8
const (
tokenAccountingSemanticsUnknown tokenAccountingSemantics = iota
tokenAccountingSemanticsSubset
tokenAccountingSemanticsIndependent
tokenAccountingSemanticsSeparateReasoning
)
// TokenInputBreakdown contains mutually exclusive input token buckets.
type TokenInputBreakdown struct {
TotalTokens int64 `json:"total_tokens"`
UncachedTokens int64 `json:"uncached_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheWriteTokens int64 `json:"cache_write_tokens"`
}
// TokenOutputBreakdown contains mutually exclusive output token buckets.
type TokenOutputBreakdown struct {
TotalTokens int64 `json:"total_tokens"`
NonReasoningTokens int64 `json:"non_reasoning_tokens"`
ReasoningTokens int64 `json:"reasoning_tokens"`
}
// TokenBreakdown is the canonical, non-overlapping token accounting contract.
type TokenBreakdown struct {
SchemaVersion int `json:"schema_version"`
Quality TokenAccountingQuality `json:"quality"`
TotalTokens int64 `json:"total_tokens"`
Input TokenInputBreakdown `json:"input"`
Output TokenOutputBreakdown `json:"output"`
UnclassifiedTokens int64 `json:"unclassified_tokens"`
}
// Valid reports whether the breakdown satisfies the v2 accounting invariants.
func (b TokenBreakdown) Valid() bool {
if b.SchemaVersion != TokenAccountingSchemaVersion || !validTokenAccountingQuality(b.Quality) {
return false
}
if b.TotalTokens < 0 || b.UnclassifiedTokens < 0 ||
b.Input.TotalTokens < 0 || b.Input.UncachedTokens < 0 ||
b.Input.CacheReadTokens < 0 || b.Input.CacheWriteTokens < 0 ||
b.Output.TotalTokens < 0 || b.Output.NonReasoningTokens < 0 ||
b.Output.ReasoningTokens < 0 {
return false
}
if b.Input.TotalTokens != b.Input.UncachedTokens+b.Input.CacheReadTokens+b.Input.CacheWriteTokens {
return false
}
if b.Output.TotalTokens != b.Output.NonReasoningTokens+b.Output.ReasoningTokens {
return false
}
if b.TotalTokens != b.Input.TotalTokens+b.Output.TotalTokens+b.UnclassifiedTokens {
return false
}
if b.Quality == TokenAccountingQualityComplete && b.UnclassifiedTokens != 0 {
return false
}
return true
}
func validTokenAccountingQuality(quality TokenAccountingQuality) bool {
switch quality {
case TokenAccountingQualityComplete, TokenAccountingQualityInconsistent, TokenAccountingQualityUnclassified:
return true
default:
return false
}
}
// NewSubsetTokenBreakdown normalizes protocols where cache tokens are included
// in input totals and reasoning tokens are included in output totals.
func NewSubsetTokenBreakdown(inputTotal, cacheRead, cacheWrite, outputTotal, reasoning, total int64) TokenBreakdown {
expectedTotal, okExpected := nonNegativeSum(inputTotal, outputTotal)
if !okExpected || cacheRead < 0 || cacheWrite < 0 || reasoning < 0 ||
cacheRead+cacheWrite > inputTotal || reasoning > outputTotal {
return inconsistentTokenBreakdown(total, expectedTotal)
}
resolvedTotal, okTotal := resolveAccountingTotal(total, expectedTotal)
if !okTotal {
return inconsistentTokenBreakdown(total, expectedTotal)
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: TokenAccountingQualityComplete,
TotalTokens: resolvedTotal,
Input: TokenInputBreakdown{
TotalTokens: inputTotal,
UncachedTokens: inputTotal - cacheRead - cacheWrite,
CacheReadTokens: cacheRead,
CacheWriteTokens: cacheWrite,
},
Output: TokenOutputBreakdown{
TotalTokens: outputTotal,
NonReasoningTokens: outputTotal - reasoning,
ReasoningTokens: reasoning,
},
}
}
// NewPartialSubsetTokenBreakdown preserves known subset buckets while assigning
// an authoritative remainder to the unclassified bucket.
func NewPartialSubsetTokenBreakdown(inputTotal, cacheRead, cacheWrite, outputTotal, reasoning, total int64) TokenBreakdown {
cacheTotal, okCache := nonNegativeSum(cacheRead, cacheWrite)
expectedTotal, okExpected := nonNegativeSum(inputTotal, outputTotal)
if !okCache || !okExpected || inputTotal < 0 || outputTotal < 0 || reasoning < 0 ||
cacheTotal > inputTotal || reasoning > outputTotal || total < 0 {
return inconsistentTokenBreakdown(total, expectedTotal)
}
resolvedTotal := total
if resolvedTotal == 0 {
resolvedTotal = expectedTotal
}
if resolvedTotal < expectedTotal {
return inconsistentTokenBreakdown(total, expectedTotal)
}
unclassified := resolvedTotal - expectedTotal
quality := TokenAccountingQualityComplete
if unclassified > 0 {
quality = TokenAccountingQualityUnclassified
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: quality,
TotalTokens: resolvedTotal,
Input: TokenInputBreakdown{
TotalTokens: inputTotal,
UncachedTokens: inputTotal - cacheTotal,
CacheReadTokens: cacheRead,
CacheWriteTokens: cacheWrite,
},
Output: TokenOutputBreakdown{
TotalTokens: outputTotal,
NonReasoningTokens: outputTotal - reasoning,
ReasoningTokens: reasoning,
},
UnclassifiedTokens: unclassified,
}
}
// NewIndependentTokenBreakdown normalizes protocols where uncached input,
// cache reads, cache writes, non-reasoning output, and reasoning are separate.
func NewIndependentTokenBreakdown(uncachedInput, cacheRead, cacheWrite, nonReasoningOutput, reasoning, total int64) TokenBreakdown {
inputTotal, okInput := nonNegativeSum(uncachedInput, cacheRead, cacheWrite)
outputTotal, okOutput := nonNegativeSum(nonReasoningOutput, reasoning)
expectedTotal, okExpected := nonNegativeSum(inputTotal, outputTotal)
if !okInput || !okOutput || !okExpected {
return inconsistentTokenBreakdown(total, expectedTotal)
}
resolvedTotal, okTotal := resolveAccountingTotal(total, expectedTotal)
if !okTotal {
return inconsistentTokenBreakdown(total, expectedTotal)
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: TokenAccountingQualityComplete,
TotalTokens: resolvedTotal,
Input: TokenInputBreakdown{
TotalTokens: inputTotal,
UncachedTokens: uncachedInput,
CacheReadTokens: cacheRead,
CacheWriteTokens: cacheWrite,
},
Output: TokenOutputBreakdown{
TotalTokens: outputTotal,
NonReasoningTokens: nonReasoningOutput,
ReasoningTokens: reasoning,
},
}
}
// NewSeparateReasoningTokenBreakdown normalizes protocols where cache tokens
// are included in input totals while reasoning is separate from ordinary output.
func NewSeparateReasoningTokenBreakdown(inputTotal, cacheRead, cacheWrite, nonReasoningOutput, reasoning, total int64) TokenBreakdown {
if inputTotal < 0 || cacheRead < 0 || cacheWrite < 0 || cacheRead+cacheWrite > inputTotal {
return inconsistentTokenBreakdown(total, 0)
}
outputTotal, okOutput := nonNegativeSum(nonReasoningOutput, reasoning)
expectedTotal, okExpected := nonNegativeSum(inputTotal, outputTotal)
if !okOutput || !okExpected {
return inconsistentTokenBreakdown(total, expectedTotal)
}
resolvedTotal, okTotal := resolveAccountingTotal(total, expectedTotal)
if !okTotal {
return inconsistentTokenBreakdown(total, expectedTotal)
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: TokenAccountingQualityComplete,
TotalTokens: resolvedTotal,
Input: TokenInputBreakdown{
TotalTokens: inputTotal,
UncachedTokens: inputTotal - cacheRead - cacheWrite,
CacheReadTokens: cacheRead,
CacheWriteTokens: cacheWrite,
},
Output: TokenOutputBreakdown{
TotalTokens: outputTotal,
NonReasoningTokens: nonReasoningOutput,
ReasoningTokens: reasoning,
},
}
}
// NewUnclassifiedTokenBreakdown preserves an authoritative total without
// guessing how an unknown protocol partitions it.
func NewUnclassifiedTokenBreakdown(total int64) TokenBreakdown {
if total <= 0 {
quality := TokenAccountingQualityComplete
if total < 0 {
quality = TokenAccountingQualityInconsistent
}
return TokenBreakdown{SchemaVersion: TokenAccountingSchemaVersion, Quality: quality}
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: TokenAccountingQualityUnclassified,
TotalTokens: total,
UnclassifiedTokens: total,
}
}
// EnsureTokenBreakdown attaches a valid v2 breakdown to legacy or direct SDK
// usage details without guessing whether reasoning is already inside output.
func EnsureTokenBreakdown(detail Detail) Detail {
return EnsureTokenBreakdownForProvider(detail, "", "")
}
// EnsureTokenBreakdownForProvider attaches a valid v2 breakdown to legacy or
// direct SDK usage details using the known provider's token semantics. Unknown
// providers remain unclassified instead of guessing how their buckets overlap.
func EnsureTokenBreakdownForProvider(detail Detail, provider, executorType string) Detail {
if !detail.TokenBreakdown.Valid() {
semantics := tokenAccountingSemanticsFor(provider, executorType)
if detail.CacheReadTokens == 0 && detail.CachedTokens > 0 && detail.InputTokens == 0 &&
detail.OutputTokens == 0 && detail.ReasoningTokens == 0 && detail.CacheCreationTokens == 0 && detail.TotalTokens == 0 &&
(semantics == tokenAccountingSemanticsSubset || semantics == tokenAccountingSemanticsSeparateReasoning) {
detail.CacheReadTokens = detail.CachedTokens
}
detail.TokenBreakdown = tokenBreakdownForSemantics(detail, semantics)
}
if detail.TotalTokens == 0 {
detail.TotalTokens = detail.TokenBreakdown.TotalTokens
}
return detail
}
func tokenBreakdownForSemantics(detail Detail, semantics tokenAccountingSemantics) TokenBreakdown {
if detail.TotalTokens == 0 && detail.InputTokens == 0 && detail.OutputTokens == 0 {
if total, okTotal := unclassifiedTokenLowerBound(detail); !okTotal {
return inconsistentTokenBreakdown(detail.TotalTokens, 0)
} else if total > 0 && (semantics == tokenAccountingSemanticsUnknown ||
semantics == tokenAccountingSemanticsSubset ||
(semantics == tokenAccountingSemanticsSeparateReasoning &&
(detail.CacheReadTokens > 0 || detail.CacheCreationTokens > 0 || detail.CachedTokens > 0))) {
return NewUnclassifiedTokenBreakdown(total)
}
}
switch semantics {
case tokenAccountingSemanticsSubset:
return NewSubsetTokenBreakdown(
detail.InputTokens,
detail.CacheReadTokens,
detail.CacheCreationTokens,
detail.OutputTokens,
detail.ReasoningTokens,
detail.TotalTokens,
)
case tokenAccountingSemanticsIndependent:
return NewIndependentTokenBreakdown(
detail.InputTokens,
detail.CacheReadTokens,
detail.CacheCreationTokens,
detail.OutputTokens,
detail.ReasoningTokens,
detail.TotalTokens,
)
case tokenAccountingSemanticsSeparateReasoning:
return NewSeparateReasoningTokenBreakdown(
detail.InputTokens,
detail.CacheReadTokens,
detail.CacheCreationTokens,
detail.OutputTokens,
detail.ReasoningTokens,
detail.TotalTokens,
)
default:
total := detail.TotalTokens
if total == 0 {
var okTotal bool
total, okTotal = unclassifiedTokenLowerBound(detail)
if !okTotal {
return inconsistentTokenBreakdown(detail.TotalTokens, 0)
}
}
return NewUnclassifiedTokenBreakdown(total)
}
}
func unclassifiedTokenLowerBound(detail Detail) (int64, bool) {
cacheTokens, okCache := nonNegativeSum(detail.CacheReadTokens, detail.CacheCreationTokens)
if !okCache || detail.InputTokens < 0 || detail.OutputTokens < 0 || detail.ReasoningTokens < 0 || detail.CachedTokens < 0 {
return 0, false
}
inputTotal := detail.InputTokens
if cacheTokens > inputTotal {
inputTotal = cacheTokens
}
if detail.CachedTokens > inputTotal {
inputTotal = detail.CachedTokens
}
outputTotal := detail.OutputTokens
if detail.ReasoningTokens > outputTotal {
outputTotal = detail.ReasoningTokens
}
return nonNegativeSum(inputTotal, outputTotal)
}
func tokenAccountingSemanticsFor(provider, executorType string) tokenAccountingSemantics {
normalizedProvider := strings.ToLower(strings.TrimSpace(provider))
normalizedExecutor := strings.ToLower(strings.TrimSpace(executorType))
value := strings.TrimSpace(normalizedProvider + " " + normalizedExecutor)
if value == "" || value == "unknown" || value == "unknown unknown" {
return tokenAccountingSemanticsUnknown
}
if normalizedExecutor == "openaicompatexecutor" || normalizedProvider == "openai-compatibility" || strings.HasPrefix(normalizedProvider, "openai-compatible-") {
return tokenAccountingSemanticsSubset
}
if strings.Contains(value, "claude") || strings.Contains(value, "anthropic") {
return tokenAccountingSemanticsIndependent
}
for _, marker := range []string{"gemini", "aistudio", "antigravity", "vertex", "interaction"} {
if strings.Contains(value, marker) {
return tokenAccountingSemanticsSeparateReasoning
}
}
for _, marker := range []string{"openai", "codex", "xai", "grok", "kimi", "qwen", "deepseek", "openrouter"} {
if strings.Contains(value, marker) {
return tokenAccountingSemanticsSubset
}
}
return tokenAccountingSemanticsUnknown
}
func inconsistentTokenBreakdown(total, fallback int64) TokenBreakdown {
resolved := total
if resolved <= 0 {
resolved = fallback
}
if resolved < 0 {
resolved = 0
}
return TokenBreakdown{
SchemaVersion: TokenAccountingSchemaVersion,
Quality: TokenAccountingQualityInconsistent,
TotalTokens: resolved,
UnclassifiedTokens: resolved,
}
}
func resolveAccountingTotal(total, expected int64) (int64, bool) {
if total < 0 || expected < 0 {
return 0, false
}
if total == 0 {
return expected, true
}
return total, total == expected
}
func nonNegativeSum(values ...int64) (int64, bool) {
var total int64
for _, value := range values {
if value < 0 || total > int64(^uint64(0)>>1)-value {
return 0, false
}
total += value
}
return total, true
}

View file

@ -0,0 +1,162 @@
package usage
import "testing"
func TestNewSubsetTokenBreakdownAvoidsCacheAndReasoningDoubleCount(t *testing.T) {
breakdown := NewSubsetTokenBreakdown(100, 40, 10, 30, 12, 130)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Input.UncachedTokens != 50 || breakdown.Output.NonReasoningTokens != 18 {
t.Fatalf("breakdown = %+v", breakdown)
}
if breakdown.TotalTokens != 130 {
t.Fatalf("total = %d, want 130", breakdown.TotalTokens)
}
}
func TestNewPartialSubsetTokenBreakdownPreservesKnownBuckets(t *testing.T) {
breakdown := NewPartialSubsetTokenBreakdown(10, 4, 0, 0, 0, 15)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityUnclassified || breakdown.Input.TotalTokens != 10 ||
breakdown.UnclassifiedTokens != 5 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewIndependentTokenBreakdownKeepsClaudeCacheBucketsIndependent(t *testing.T) {
breakdown := NewIndependentTokenBreakdown(30, 7, 13, 5, 0, 55)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Input.TotalTokens != 50 || breakdown.TotalTokens != 55 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewSeparateReasoningTokenBreakdownAddsReasoningToOutput(t *testing.T) {
breakdown := NewSeparateReasoningTokenBreakdown(20, 5, 0, 7, 3, 30)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Output.TotalTokens != 10 || breakdown.TotalTokens != 30 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestTokenBreakdownMarksContradictoryParentsInconsistent(t *testing.T) {
breakdown := NewSubsetTokenBreakdown(10, 4, 0, 3, 1, 20)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityInconsistent || breakdown.UnclassifiedTokens != 20 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestNewUnclassifiedTokenBreakdownDoesNotGuessBuckets(t *testing.T) {
breakdown := NewUnclassifiedTokenBreakdown(42)
if !breakdown.Valid() {
t.Fatalf("breakdown is invalid: %+v", breakdown)
}
if breakdown.Quality != TokenAccountingQualityUnclassified || breakdown.UnclassifiedTokens != 42 {
t.Fatalf("breakdown = %+v", breakdown)
}
}
func TestEnsureTokenBreakdownForProviderUsesKnownSemantics(t *testing.T) {
tests := []struct {
name string
provider string
executorType string
detail Detail
wantTotal int64
wantInput int64
wantOutput int64
}{
{
name: "OpenAI subsets cache and reasoning",
provider: "openai",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 130,
wantInput: 100,
wantOutput: 30,
},
{
name: "OpenAI compatible executor takes precedence",
provider: "anthropic",
executorType: "OpenAICompatExecutor",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 130,
wantInput: 100,
wantOutput: 30,
},
{
name: "Gemini keeps reasoning separate",
provider: "gemini",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 142,
wantInput: 100,
wantOutput: 42,
},
{
name: "Claude keeps cache and reasoning independent",
provider: "anthropic",
detail: Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12, CacheReadTokens: 40, CacheCreationTokens: 10},
wantTotal: 192,
wantInput: 150,
wantOutput: 42,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(tt.detail, tt.provider, tt.executorType)
if !detail.TokenBreakdown.Valid() || detail.TokenBreakdown.Quality != TokenAccountingQualityComplete {
t.Fatalf("token breakdown = %+v", detail.TokenBreakdown)
}
if detail.TotalTokens != tt.wantTotal || detail.TokenBreakdown.TotalTokens != tt.wantTotal ||
detail.TokenBreakdown.Input.TotalTokens != tt.wantInput || detail.TokenBreakdown.Output.TotalTokens != tt.wantOutput {
t.Fatalf("detail = %+v, want total=%d input=%d output=%d", detail, tt.wantTotal, tt.wantInput, tt.wantOutput)
}
})
}
}
func TestEnsureTokenBreakdownForUnknownProviderDoesNotGuessReasoning(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{InputTokens: 100, OutputTokens: 30, ReasoningTokens: 12}, "plugin-provider", "")
if detail.TotalTokens != 130 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified || detail.TokenBreakdown.UnclassifiedTokens != 130 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownForUnknownProviderPreservesAuxiliaryOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{ReasoningTokens: 12, CacheReadTokens: 7}, "plugin-provider", "")
if detail.TotalTokens != 19 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified || detail.TokenBreakdown.UnclassifiedTokens != 19 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownForGeminiClassifiesReasoningOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{ReasoningTokens: 12}, "gemini", "")
if detail.TotalTokens != 12 || detail.TokenBreakdown.Quality != TokenAccountingQualityComplete ||
detail.TokenBreakdown.Output.ReasoningTokens != 12 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownPreservesLegacyCachedOnlyUsage(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{CachedTokens: 13}, "openai", "")
if detail.TotalTokens != 13 || detail.CacheReadTokens != 13 || detail.TokenBreakdown.Quality != TokenAccountingQualityUnclassified ||
detail.TokenBreakdown.UnclassifiedTokens != 13 {
t.Fatalf("detail = %+v", detail)
}
}
func TestEnsureTokenBreakdownDoesNotOverrideCanonicalZeroCacheRead(t *testing.T) {
detail := EnsureTokenBreakdownForProvider(Detail{CachedTokens: 13, CacheCreationTokens: 13}, "openai", "")
if detail.CacheReadTokens != 0 {
t.Fatalf("detail = %+v", detail)
}
}

View file

@ -0,0 +1,388 @@
package usage
import (
"context"
"net/http"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
// DefaultServiceTier is retained for direct SDK and non-OpenAI usage callers.
const DefaultServiceTier = "default"
// AutoServiceTier is the OpenAI request semantics when service_tier is omitted.
// OpenAI HTTP handlers set it explicitly, without changing other providers'
// historical direct-SDK default.
const AutoServiceTier = "auto"
// Record contains the usage statistics captured for a single provider request.
type Record struct {
Provider string
// ExecutorType stores the concrete executor type that handled the request.
ExecutorType string
Model string
Alias string
APIKey string
AuthID string
AuthIndex string
// AccessTokenSHA256 identifies the OAuth token version without exposing the token.
AccessTokenSHA256 string
AuthType string
Source string
// ReasoningEffort stores the translated upstream thinking level for request event logs.
ReasoningEffort string
// ServiceTier stores the client-requested service tier.
ServiceTier string
// RequestServiceTier is a deprecated input-only alias retained for existing
// plugin callers. It is normalized into ServiceTier and never emitted.
RequestServiceTier string
// ResponseServiceTier stores the final tier reported by the upstream response.
ResponseServiceTier string
// Generate reports whether the client requested actual generation.
// nil or true means generation is enabled; only an explicit false disables generation.
// Use GenerateFlag to set the value and GenerateEnabled to read it with the default.
Generate *bool
RequestedAt time.Time
Latency time.Duration
TTFT time.Duration
Failed bool
Fail Failure
Detail Detail
// ResponseHeaders stores a snapshot of upstream response headers for usage sinks.
ResponseHeaders http.Header
}
// Failure holds HTTP failure metadata for an upstream request attempt.
type Failure struct {
StatusCode int
Body string
}
// Detail holds the token usage breakdown.
type Detail struct {
InputTokens int64
OutputTokens int64
ReasoningTokens int64
CachedTokens int64
CacheReadTokens int64
CacheCreationTokens int64
TotalTokens int64
TokenBreakdown TokenBreakdown
ResponseServiceTier string
}
type requestedModelAliasContextKey struct{}
type reasoningEffortContextKey struct{}
type serviceTierContextKey struct{}
type generateContextKey struct{}
// WithRequestedModelAlias stores the client-requested model name for usage sinks.
func WithRequestedModelAlias(ctx context.Context, alias string) context.Context {
if ctx == nil {
ctx = context.Background()
}
alias = strings.TrimSpace(alias)
if alias == "" {
return ctx
}
return context.WithValue(ctx, requestedModelAliasContextKey{}, alias)
}
// RequestedModelAliasFromContext returns the client-requested model name stored in ctx.
func RequestedModelAliasFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
raw := ctx.Value(requestedModelAliasContextKey{})
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
case []byte:
return strings.TrimSpace(string(value))
default:
return ""
}
}
// WithReasoningEffort stores the client-requested reasoning effort for usage sinks.
func WithReasoningEffort(ctx context.Context, effort string) context.Context {
if ctx == nil {
ctx = context.Background()
}
effort = strings.TrimSpace(effort)
if effort == "" {
return ctx
}
return context.WithValue(ctx, reasoningEffortContextKey{}, effort)
}
// ReasoningEffortFromContext returns the client-requested reasoning effort stored in ctx.
func ReasoningEffortFromContext(ctx context.Context) string {
if ctx == nil {
return ""
}
raw := ctx.Value(reasoningEffortContextKey{})
switch value := raw.(type) {
case string:
return strings.TrimSpace(value)
case []byte:
return strings.TrimSpace(string(value))
default:
return ""
}
}
// WithServiceTier stores the client-requested service tier for usage sinks.
func WithServiceTier(ctx context.Context, tier string) context.Context {
if ctx == nil {
ctx = context.Background()
}
tier = strings.TrimSpace(tier)
if tier == "" {
tier = DefaultServiceTier
}
return context.WithValue(ctx, serviceTierContextKey{}, tier)
}
// ServiceTierFromContext returns the client-requested service tier stored in ctx.
func ServiceTierFromContext(ctx context.Context) string {
if ctx == nil {
return DefaultServiceTier
}
raw := ctx.Value(serviceTierContextKey{})
switch value := raw.(type) {
case string:
tier := strings.TrimSpace(value)
if tier == "" {
return DefaultServiceTier
}
return tier
case []byte:
tier := strings.TrimSpace(string(value))
if tier == "" {
return DefaultServiceTier
}
return tier
default:
return DefaultServiceTier
}
}
// WithGenerate stores whether the client requested actual generation for usage sinks.
// Missing context values default to true; only an explicit false disables generation.
func WithGenerate(ctx context.Context, generate bool) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, generateContextKey{}, generate)
}
// GenerateFromContext returns whether the client requested actual generation.
// Missing values default to true.
func GenerateFromContext(ctx context.Context) bool {
if ctx == nil {
return true
}
raw := ctx.Value(generateContextKey{})
switch value := raw.(type) {
case bool:
return value
default:
return true
}
}
// GenerateFlag returns a pointer suitable for Record.Generate.
func GenerateFlag(generate bool) *bool {
return &generate
}
// GenerateEnabled reports whether generation is enabled for the record field.
// A nil value defaults to true so legacy callers that omit Generate keep the historical behavior.
func GenerateEnabled(generate *bool) bool {
if generate == nil {
return true
}
return *generate
}
// Plugin consumes usage records emitted by the proxy runtime.
type Plugin interface {
HandleUsage(ctx context.Context, record Record)
}
type queueItem struct {
ctx context.Context
record Record
}
// Manager maintains a queue of usage records and delivers them to registered plugins.
type Manager struct {
once sync.Once
stopOnce sync.Once
cancel context.CancelFunc
mu sync.Mutex
cond *sync.Cond
queue []queueItem
closed bool
pluginsMu sync.RWMutex
plugins []Plugin
named map[string]int
}
// NewManager constructs a manager with a buffered queue.
func NewManager(buffer int) *Manager {
m := &Manager{}
m.cond = sync.NewCond(&m.mu)
return m
}
// Start launches the background dispatcher. Calling Start multiple times is safe.
func (m *Manager) Start(ctx context.Context) {
if m == nil {
return
}
m.once.Do(func() {
if ctx == nil {
ctx = context.Background()
}
var workerCtx context.Context
workerCtx, m.cancel = context.WithCancel(ctx)
go m.run(workerCtx)
})
}
// Stop stops the dispatcher and drains the queue.
func (m *Manager) Stop() {
if m == nil {
return
}
m.stopOnce.Do(func() {
if m.cancel != nil {
m.cancel()
}
m.mu.Lock()
m.closed = true
m.mu.Unlock()
m.cond.Broadcast()
})
}
// Register appends a plugin to the delivery list.
func (m *Manager) Register(plugin Plugin) {
if m == nil || plugin == nil {
return
}
m.pluginsMu.Lock()
m.plugins = append(m.plugins, plugin)
m.pluginsMu.Unlock()
}
// RegisterNamed registers or replaces a plugin by name.
func (m *Manager) RegisterNamed(name string, plugin Plugin) {
if m == nil || plugin == nil {
return
}
name = strings.TrimSpace(name)
if name == "" {
return
}
m.pluginsMu.Lock()
if m.named == nil {
m.named = make(map[string]int)
}
if index, exists := m.named[name]; exists && index >= 0 && index < len(m.plugins) {
m.plugins[index] = plugin
m.pluginsMu.Unlock()
return
}
m.named[name] = len(m.plugins)
m.plugins = append(m.plugins, plugin)
m.pluginsMu.Unlock()
}
// Publish enqueues a usage record for processing. If no plugin is registered
// the record will be discarded downstream.
func (m *Manager) Publish(ctx context.Context, record Record) {
if m == nil {
return
}
// ensure worker is running even if Start was not called explicitly
m.Start(context.Background())
m.mu.Lock()
if m.closed {
m.mu.Unlock()
return
}
m.queue = append(m.queue, queueItem{ctx: ctx, record: record})
m.mu.Unlock()
m.cond.Signal()
}
func (m *Manager) run(ctx context.Context) {
for {
m.mu.Lock()
for !m.closed && len(m.queue) == 0 {
m.cond.Wait()
}
if len(m.queue) == 0 && m.closed {
m.mu.Unlock()
return
}
item := m.queue[0]
m.queue = m.queue[1:]
m.mu.Unlock()
m.dispatch(item)
}
}
func (m *Manager) dispatch(item queueItem) {
m.pluginsMu.RLock()
plugins := make([]Plugin, len(m.plugins))
copy(plugins, m.plugins)
m.pluginsMu.RUnlock()
if len(plugins) == 0 {
return
}
for _, plugin := range plugins {
if plugin == nil {
continue
}
safeInvoke(plugin, item.ctx, item.record)
}
}
func safeInvoke(plugin Plugin, ctx context.Context, record Record) {
defer func() {
if r := recover(); r != nil {
log.Errorf("usage: plugin panic recovered: %v", r)
}
}()
plugin.HandleUsage(ctx, record)
}
var defaultManager = NewManager(512)
// DefaultManager returns the global usage manager instance.
func DefaultManager() *Manager { return defaultManager }
// RegisterPlugin registers a plugin on the default manager.
func RegisterPlugin(plugin Plugin) { DefaultManager().Register(plugin) }
// RegisterNamedPlugin registers or replaces a named plugin on the default manager.
func RegisterNamedPlugin(name string, plugin Plugin) { DefaultManager().RegisterNamed(name, plugin) }
// PublishRecord publishes a record using the default manager.
func PublishRecord(ctx context.Context, record Record) { DefaultManager().Publish(ctx, record) }
// StartDefault starts the default manager's dispatcher.
func StartDefault(ctx context.Context) { DefaultManager().Start(ctx) }
// StopDefault stops the default manager's dispatcher.
func StopDefault() { DefaultManager().Stop() }

View file

@ -0,0 +1,52 @@
package usage
import (
"context"
"testing"
)
func TestGenerateEnabledDefaultsNilToTrue(t *testing.T) {
if !GenerateEnabled(nil) {
t.Fatalf("GenerateEnabled(nil) = false, want true")
}
}
func TestGenerateEnabledHonorsExplicitFalse(t *testing.T) {
if GenerateEnabled(GenerateFlag(false)) {
t.Fatalf("GenerateEnabled(false) = true, want false")
}
}
func TestGenerateEnabledHonorsExplicitTrue(t *testing.T) {
if !GenerateEnabled(GenerateFlag(true)) {
t.Fatalf("GenerateEnabled(true) = false, want true")
}
}
func TestGenerateFromContextDefaultsMissingToTrue(t *testing.T) {
if !GenerateFromContext(context.Background()) {
t.Fatalf("GenerateFromContext(background) = false, want true")
}
}
func TestGenerateFromContextHonorsExplicitFalse(t *testing.T) {
ctx := WithGenerate(context.Background(), false)
if GenerateFromContext(ctx) {
t.Fatalf("GenerateFromContext(false) = true, want false")
}
}
func TestRecordOmittedGenerateIsEnabled(t *testing.T) {
// Existing callers construct Record without setting Generate.
// Omission must remain distinguishable from explicit false and default to true.
record := Record{
Provider: "openai",
Model: "gpt-5.4",
}
if record.Generate != nil {
t.Fatalf("Record.Generate = %v, want nil for omitted field", record.Generate)
}
if !GenerateEnabled(record.Generate) {
t.Fatalf("GenerateEnabled(omitted) = false, want true")
}
}