- 使用 SQLite 保存关联后的请求生命周期和用量记录 - 支持长上下文阶梯价格和可配置的 Fast 计费倍率 - 添加管理接口和可配置列的用量面板 - 保留失败、取消、重试和 compact 请求,便于计费核对
145 lines
3.8 KiB
Go
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
|
|
}
|