Add token usage stats and consolidate credentials in SQLite
build / build (push) Successful in 2m34s
build / build (push) Successful in 2m34s
- New internal/stats (types) and internal/store (SQLite owner: requests + credentials tables, WAL); store implements stats.Recorder. - Stream (SSE tee) and non-stream chat paths parse upstream usage incl. cached_tokens and record per-request; add /api/stats, /api/stats/reset, /admin/stats HTML with cache hit rate. - Drop credentials.json: remove auth file I/O and ZHANLU_CREDENTIALS_FILE; credential precedence is env vars > db row.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Package stats defines the types and helpers for OpenAI-compatible token
|
||||
// usage statistics recorded by the zhanlu proxy. The concrete SQLite-backed
|
||||
// recorder lives in internal/store; this package is dependency-free so it can
|
||||
// be referenced by both store and server without import cycles.
|
||||
package stats
|
||||
|
||||
import "time"
|
||||
|
||||
// Record is a single chat-completion usage observation.
|
||||
type Record struct {
|
||||
Ts time.Time `json:"ts"`
|
||||
Model string `json:"model"`
|
||||
Stream bool `json:"stream"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
ReasoningTokens int `json:"reasoning_tokens"`
|
||||
CachedTokens int `json:"cached_tokens"`
|
||||
Status string `json:"status"` // "success" | "upstream_error"
|
||||
LatencyMs int64 `json:"latency_ms"`
|
||||
}
|
||||
|
||||
// Query filters the recorded stats. Zero-value time fields mean unbounded on
|
||||
// that end; empty Model means all models. Limit caps the recent-records list
|
||||
// (0 = default). Stream filters by streaming mode (nil = both).
|
||||
type Query struct {
|
||||
Since time.Time
|
||||
Until time.Time
|
||||
Model string
|
||||
Limit int
|
||||
Stream *bool
|
||||
}
|
||||
|
||||
// Totals aggregates request counts and token sums over a filtered set.
|
||||
type Totals struct {
|
||||
Requests int `json:"requests"`
|
||||
SuccessRequests int `json:"success_requests"`
|
||||
ErrorRequests int `json:"error_requests"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
ReasoningTokens int64 `json:"reasoning_tokens"`
|
||||
CachedTokens int64 `json:"cached_tokens"`
|
||||
CacheRate float64 `json:"cache_rate"` // cached_tokens / prompt_tokens, 0..1
|
||||
}
|
||||
|
||||
// ModelStat is a per-model aggregation row.
|
||||
type ModelStat struct {
|
||||
Model string `json:"model"`
|
||||
Requests int `json:"requests"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
CachedTokens int64 `json:"cached_tokens"`
|
||||
}
|
||||
|
||||
// DayStat is a per-day aggregation row (server-local time, YYYY-MM-DD).
|
||||
type DayStat struct {
|
||||
Day string `json:"day"`
|
||||
Requests int `json:"requests"`
|
||||
PromptTokens int64 `json:"prompt_tokens"`
|
||||
CompletionTokens int64 `json:"completion_tokens"`
|
||||
TotalTokens int64 `json:"total_tokens"`
|
||||
CachedTokens int64 `json:"cached_tokens"`
|
||||
}
|
||||
|
||||
// Summary is the full result returned by a Recorder's Stats query.
|
||||
type Summary struct {
|
||||
Totals Totals `json:"totals"`
|
||||
PerModel []ModelStat `json:"per_model"`
|
||||
Daily []DayStat `json:"daily"`
|
||||
Recent []Record `json:"recent"`
|
||||
}
|
||||
|
||||
// Recorder persists and queries usage statistics. The concrete implementation
|
||||
// lives in internal/store; the interface is declared here so server code can
|
||||
// depend on the contract and tests can inject fakes.
|
||||
type Recorder interface {
|
||||
Record(r Record) error
|
||||
Stats(q Query) (*Summary, error)
|
||||
Reset() error
|
||||
Close() error
|
||||
}
|
||||
Reference in New Issue
Block a user