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.
84 lines
3.0 KiB
Go
84 lines
3.0 KiB
Go
// 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
|
|
}
|