Files
cpa-plugin/internal/data/usage.go
T
chuan 6a461bf1f3 feat: 添加持久化用量计费与管理面板
- 使用 SQLite 保存关联后的请求生命周期和用量记录
- 支持长上下文阶梯价格和可配置的 Fast 计费倍率
- 添加管理接口和可配置列的用量面板
- 保留失败、取消、重试和 compact 请求,便于计费核对
2026-08-14 21:08:02 +08:00

145 lines
3.8 KiB
Go

// Package data 定义 cpa-ext 各模块共享且与 Provider 无关的数据事实。
package data
import (
"errors"
"fmt"
"math"
"strings"
"time"
)
// UsageQuality 表示 Provider Token 数据经过规范化后的可信程度。
// 使用方必须检查该值,不能把所有零值都当成真实测量结果。
type UsageQuality string
const (
UsageQualityComplete UsageQuality = "complete"
UsageQualityNormalized UsageQuality = "normalized"
UsageQualityPartial UsageQuality = "partial"
UsageQualityInconsistent UsageQuality = "inconsistent"
UsageQualityUnclassified UsageQuality = "unclassified"
UsageQualityMissing UsageQuality = "missing"
)
// TokenUsage 保存互不重叠的 Token 分项。每个 Token 只能进入一个分项,
// 防止计价和统计重复计算。
type TokenUsage struct {
UncachedInputTokens int64
CacheReadTokens int64
CacheCreationTokens int64
NonReasoningTokens int64
ReasoningTokens int64
UnclassifiedTokens int64
TotalTokens int64
}
// Usage 是与 Provider 无关的最小用量事实。该类型不保存凭证、认证请求头、
// 请求体或响应内容。
type Usage struct {
Provider string
Model string
RequestedAt time.Time
Generated bool
Failed bool
Quality UsageQuality
Tokens TokenUsage
}
// Validate 检查 Usage 的结构约束,但不会修改观测到的数据。
// inconsistent 类型允许保留互相矛盾的总数和分项,供后续诊断使用。
func (u Usage) Validate() error {
if strings.TrimSpace(u.Provider) == "" {
return errors.New("usage provider is required")
}
if strings.TrimSpace(u.Model) == "" {
return errors.New("usage model is required")
}
if u.RequestedAt.IsZero() {
return errors.New("usage requested time is required")
}
if !u.Quality.valid() {
return fmt.Errorf("unknown usage quality %q", u.Quality)
}
return u.Tokens.validate(u.Quality)
}
func (q UsageQuality) valid() bool {
switch q {
case UsageQualityComplete,
UsageQualityNormalized,
UsageQualityPartial,
UsageQualityInconsistent,
UsageQualityUnclassified,
UsageQualityMissing:
return true
default:
return false
}
}
func (t TokenUsage) validate(quality UsageQuality) error {
counts := []struct {
name string
value int64
}{
{"uncached input tokens", t.UncachedInputTokens},
{"cache read tokens", t.CacheReadTokens},
{"cache creation tokens", t.CacheCreationTokens},
{"non-reasoning output tokens", t.NonReasoningTokens},
{"reasoning tokens", t.ReasoningTokens},
{"unclassified tokens", t.UnclassifiedTokens},
{"total tokens", t.TotalTokens},
}
for _, count := range counts {
if count.value < 0 {
return fmt.Errorf("%s cannot be negative", count.name)
}
}
sum, ok := t.bucketSum()
if !ok {
return errors.New("token bucket sum overflows int64")
}
switch quality {
case UsageQualityMissing:
if sum != 0 || t.TotalTokens != 0 {
return errors.New("missing usage cannot contain token counts")
}
case UsageQualityComplete, UsageQualityNormalized:
if t.UnclassifiedTokens != 0 {
return errors.New("classified usage cannot contain unclassified tokens")
}
if sum != t.TotalTokens {
return errors.New("classified token buckets do not equal total tokens")
}
case UsageQualityPartial, UsageQualityUnclassified:
if sum != t.TotalTokens {
return errors.New("token buckets do not equal total tokens")
}
case UsageQualityInconsistent:
// 矛盾的计数需要作为明确的诊断事实保留下来。
}
return nil
}
func (t TokenUsage) bucketSum() (int64, bool) {
values := [...]int64{
t.UncachedInputTokens,
t.CacheReadTokens,
t.CacheCreationTokens,
t.NonReasoningTokens,
t.ReasoningTokens,
t.UnclassifiedTokens,
}
var sum int64
for _, value := range values {
if value > math.MaxInt64-sum {
return 0, false
}
sum += value
}
return sum, true
}