From 41e322c53d7b4b796eb377d0df9c29ecd10ba431 Mon Sep 17 00:00:00 2001 From: Chia Date: Thu, 6 Aug 2026 09:29:41 +1200 Subject: feat: complete commercial control plane, billing, auth, and model catalog - add PostgreSQL control-plane persistence with Redis-degraded hot reload - implement prepaid balance, usage ledger, Stripe top-up and reconciliation - add registration, email verification, password reset, invitations and RBAC - support TOTP, Passkey MFA, device sessions, quotas and rate limits - add tenant billing profiles, audit logs and operational readiness checks - build authenticated admin console, Quickstart, Playground and usage analytics - add public model catalog with pricing, filtering and cost estimation - support OpenAI Responses providers and provider health failover - validate real upstream usage reporting and balance settlement --- internal/usage/observer.go | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 8 deletions(-) (limited to 'internal/usage/observer.go') diff --git a/internal/usage/observer.go b/internal/usage/observer.go index 808781f..71c1379 100644 --- a/internal/usage/observer.go +++ b/internal/usage/observer.go @@ -95,18 +95,28 @@ func (o *Observer) parseSSELine(line []byte) { o.parseJSON(payload) } +type tokenDetails struct { + CachedTokens *int64 `json:"cached_tokens"` + CacheWriteTokens *int64 `json:"cache_write_tokens"` +} + type usageFields struct { - PromptTokens *int64 `json:"prompt_tokens"` - CompletionTokens *int64 `json:"completion_tokens"` - TotalTokens *int64 `json:"total_tokens"` - InputTokens *int64 `json:"input_tokens"` - OutputTokens *int64 `json:"output_tokens"` - CacheCreationInputTokens *int64 `json:"cache_creation_input_tokens"` - CacheReadInputTokens *int64 `json:"cache_read_input_tokens"` + PromptTokens *int64 `json:"prompt_tokens"` + CompletionTokens *int64 `json:"completion_tokens"` + TotalTokens *int64 `json:"total_tokens"` + InputTokens *int64 `json:"input_tokens"` + OutputTokens *int64 `json:"output_tokens"` + CacheCreationInputTokens *int64 `json:"cache_creation_input_tokens"` + CacheReadInputTokens *int64 `json:"cache_read_input_tokens"` + PromptTokensDetails *tokenDetails `json:"prompt_tokens_details"` + InputTokensDetails *tokenDetails `json:"input_tokens_details"` } type responseEnvelope struct { - Usage *usageFields `json:"usage"` + Usage *usageFields `json:"usage"` + Response *struct { + Usage *usageFields `json:"usage"` + } `json:"response"` Message *struct { Usage *usageFields `json:"usage"` } `json:"message"` @@ -131,16 +141,22 @@ func (o *Observer) parseJSON(payload []byte) { if envelope.Message != nil && envelope.Message.Usage != nil { o.apply(envelope.Message.Usage) } + if envelope.Response != nil && envelope.Response.Usage != nil { + o.apply(envelope.Response.Usage) + } } func (o *Observer) apply(fields *usageFields) { + inputReported := false if fields.PromptTokens != nil { o.usage.InputTokens = *fields.PromptTokens o.found = true + inputReported = true } if fields.InputTokens != nil { o.usage.InputTokens = *fields.InputTokens o.found = true + inputReported = true } if fields.CompletionTokens != nil { o.usage.OutputTokens = *fields.CompletionTokens @@ -163,6 +179,26 @@ func (o *Observer) apply(fields *usageFields) { o.usage.CacheReadInputTokens = *fields.CacheReadInputTokens o.found = true } + details := fields.InputTokensDetails + if details == nil { + details = fields.PromptTokensDetails + } + if details != nil { + if details.CachedTokens != nil { + o.usage.CacheReadInputTokens = *details.CachedTokens + o.found = true + } + if details.CacheWriteTokens != nil { + o.usage.CacheCreationInputTokens = *details.CacheWriteTokens + o.found = true + } + // OpenAI reports cached token details as subsets of prompt/input_tokens. + // Normalize them into mutually exclusive buckets before billing. Anthropic + // reports its top-level cache fields separately, so they are not adjusted. + if inputReported && (o.protocol == domain.ProtocolOpenAI || o.protocol == domain.ProtocolOpenAIResponses) { + o.usage.InputTokens = max(0, o.usage.InputTokens-o.usage.CacheReadInputTokens-o.usage.CacheCreationInputTokens) + } + } if !o.explicitTotal && o.found { o.usage.TotalTokens = o.usage.InputTokens + o.usage.OutputTokens } -- cgit v1.2.3