Files
hunjing/shenshi/internal/metering/usage.go
T

89 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package metering 用量计量:每次尝试一条记录(stdout 结构化日志 + 进程内聚合)。
// 生产部署接 PG(shenshi-gateway.md §7);M4a 先以日志与内存聚合支撑 /me/usage 与对账抽样。
package metering
import (
"encoding/json"
"log"
"sync"
"time"
)
// Attempt 一次端点尝试的计量记录。
type Attempt struct {
RunID string `json:"run_id"`
CharacterID string `json:"character_id,omitempty"`
Model string `json:"model"`
Slot string `json:"slot"`
Endpoint string `json:"endpoint"`
Status string `json:"status"` // ok / error / first_byte_timeout
ErrorCode string `json:"error_code,omitempty"`
FirstTokenMS int64 `json:"first_token_ms"`
TotalMS int64 `json:"total_ms"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
At time.Time `json:"at"`
}
// UsageAgg /me/usage 聚合行。
type UsageAgg struct {
Model string `json:"model"`
Endpoint string `json:"endpoint"`
Requests int `json:"requests"`
OK int `json:"ok"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
}
// Log 进程内计量器(有界环形,避免无界增长)。
type Log struct {
mu sync.Mutex
entries []Attempt
limit int
}
// New 构造计量器(保留最近 N 条)。
func New(limit int) *Log {
return &Log{entries: make([]Attempt, 0, limit), limit: limit}
}
// Record 记录一次尝试:stdout JSON 日志 + 内存追加。
func (l *Log) Record(a Attempt) {
a.At = time.Now().UTC()
if raw, err := json.Marshal(a); err == nil {
log.Printf(`{"kind":"shenshi_usage",%s`, string(raw[1:]))
}
l.mu.Lock()
defer l.mu.Unlock()
if len(l.entries) >= l.limit {
l.entries = l.entries[1:]
}
l.entries = append(l.entries, a)
}
// Aggregate 按 模型×端点 聚合。
func (l *Log) Aggregate() []UsageAgg {
l.mu.Lock()
defer l.mu.Unlock()
agg := map[string]*UsageAgg{}
for _, e := range l.entries {
key := e.Model + "|" + e.Endpoint
row := agg[key]
if row == nil {
row = &UsageAgg{Model: e.Model, Endpoint: e.Endpoint}
agg[key] = row
}
row.Requests++
if e.Status == "ok" {
row.OK++
row.PromptTokens += e.PromptTokens
row.CompletionTokens += e.CompletionTokens
}
}
out := make([]UsageAgg, 0, len(agg))
for _, row := range agg {
out = append(out, *row)
}
return out
}