// 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 }