feat(model):claude模型5m/1h缓存收费
This commit is contained in:
1 parent
f56301560e
commit
922003e758
3 files changed
+123
-53
No files matched your search
@@ -45,8 +45,10 @@ type PricingResult struct {
|
||||
Currency string
|
||||
|
||||
// Cache 直接定价
|
||||
CacheReadPrice float64 // 缓存读取每 1M token 价格
|
||||
CacheCreationPrice float64 // 缓存创建每 1M token 价格
|
||||
CacheReadPrice float64 // 缓存读取每 1M token 价格
|
||||
CacheCreationPrice float64 // 缓存创建每 1M token 价格(5m TTL 基础价)
|
||||
CacheCreation5mPrice float64 // 5 分钟缓存创建价格(1.25× 基础价)
|
||||
CacheCreation1hPrice float64 // 1 小时缓存创建价格(2× 基础价)
|
||||
|
||||
// 模型最大输出 token 数(随定价一起缓存,供预扣估算使用,
|
||||
// 避免 EstimatePreDeductAmount 每请求单独查一次 mdl_models)。
|
||||
@@ -252,21 +254,23 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric
|
||||
modelMultiplier := 1.0
|
||||
|
||||
result := &PricingResult{
|
||||
InputPrice: inputPrice,
|
||||
OutputPrice: outputPrice,
|
||||
BaseInputPrice: baseInputPrice,
|
||||
BaseOutputPrice: baseOutputPrice,
|
||||
BillingMode: billingMode,
|
||||
BillingSource: billingSource,
|
||||
PerRequestPrice: perRequestPrice,
|
||||
DiscountRatio: discountRatio,
|
||||
TenantMultiplier: tenantMultiplier,
|
||||
ModelMultiplier: modelMultiplier,
|
||||
Currency: "USD",
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
MaxOutputTokens: model.MaxOutputTokens,
|
||||
CustomTiers: customTiers,
|
||||
InputPrice: inputPrice,
|
||||
OutputPrice: outputPrice,
|
||||
BaseInputPrice: baseInputPrice,
|
||||
BaseOutputPrice: baseOutputPrice,
|
||||
BillingMode: billingMode,
|
||||
BillingSource: billingSource,
|
||||
PerRequestPrice: perRequestPrice,
|
||||
DiscountRatio: discountRatio,
|
||||
TenantMultiplier: tenantMultiplier,
|
||||
ModelMultiplier: modelMultiplier,
|
||||
Currency: "USD",
|
||||
CacheReadPrice: cacheReadPrice,
|
||||
CacheCreationPrice: cacheCreationPrice,
|
||||
CacheCreation5mPrice: cacheCreationPrice, // 5m TTL = cache_creation_price 基础价(对应官方 1.25× 输入价)
|
||||
CacheCreation1hPrice: cacheCreationPrice * 1.6, // 1h TTL = 1.6× 基础价(因为 2.0÷1.25=1.6,对应官方 2× 输入价)
|
||||
MaxOutputTokens: model.MaxOutputTokens,
|
||||
CustomTiers: customTiers,
|
||||
}
|
||||
|
||||
modelPriceCache.Set(ctx, cacheKey, result)
|
||||
@@ -287,27 +291,27 @@ func CalculateCost(ctx context.Context, tenantID int64, modelName string, inputT
|
||||
// computeCost 纯计算:根据定价结果和 token 用量计算费用明细。
|
||||
// 提取为独立函数以便单元测试,不依赖数据库或缓存。
|
||||
func computeCost(pricing *PricingResult, inputTokens, outputTokens int, usage *rcommon.Usage) *CostBreakdown {
|
||||
inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens := resolveTokenCounts(pricing, inputTokens, outputTokens, usage)
|
||||
baseInputTokens, outputTokens, cacheReadTokens, cacheCreation5mTokens, cacheCreation1hTokens := resolveTokenCounts(pricing, inputTokens, outputTokens, usage)
|
||||
|
||||
// 按次计费:直接返回单价
|
||||
if pricing.BillingMode == "per_request" {
|
||||
return &CostBreakdown{
|
||||
BaseCost: pricing.PerRequestPrice,
|
||||
TotalCost: pricing.PerRequestPrice,
|
||||
InputTokens: inputTokens,
|
||||
InputTokens: baseInputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
BillingMode: pricing.BillingMode,
|
||||
PerRequestPrice: pricing.PerRequestPrice,
|
||||
DiscountRatio: pricing.DiscountRatio,
|
||||
TenantMultiplier: pricing.TenantMultiplier,
|
||||
Currency: pricing.Currency,
|
||||
CacheCreationTokens: cacheCreationTokens,
|
||||
CacheCreationTokens: cacheCreation5mTokens + cacheCreation1hTokens,
|
||||
CacheReadTokens: cacheReadTokens,
|
||||
}
|
||||
}
|
||||
|
||||
// 基础输入费用(已改为 decimal)
|
||||
baseInputCostD := computeInputCost(pricing, inputTokens)
|
||||
baseInputCostD := computeInputCost(pricing, baseInputTokens)
|
||||
|
||||
// 输出费用(已改为 decimal)
|
||||
outputCostD := computeOutputCost(pricing, outputTokens)
|
||||
@@ -318,7 +322,11 @@ func computeCost(pricing *PricingResult, inputTokens, outputTokens int, usage *r
|
||||
mul := NewFromFloat(pricing.TenantMultiplier)
|
||||
|
||||
cacheReadCostD := decimal.NewFromInt(int64(cacheReadTokens)).Div(million).Mul(NewFromFloat(pricing.CacheReadPrice))
|
||||
cacheCreationCostD := decimal.NewFromInt(int64(cacheCreationTokens)).Div(million).Mul(NewFromFloat(pricing.CacheCreationPrice))
|
||||
|
||||
// 缓存创建按 TTL 分别计价:5m 按 1.25×,1h 按 2×
|
||||
cacheCreation5mCostD := decimal.NewFromInt(int64(cacheCreation5mTokens)).Div(million).Mul(NewFromFloat(pricing.CacheCreation5mPrice))
|
||||
cacheCreation1hCostD := decimal.NewFromInt(int64(cacheCreation1hTokens)).Div(million).Mul(NewFromFloat(pricing.CacheCreation1hPrice))
|
||||
cacheCreationCostD := cacheCreation5mCostD.Add(cacheCreation1hCostD)
|
||||
|
||||
// 总费用 = (基础输入 + 输出 + cache各项) × 租户倍率
|
||||
subtotalD := baseInputCostD.Add(outputCostD).Add(cacheReadCostD).Add(cacheCreationCostD)
|
||||
@@ -329,14 +337,14 @@ func computeCost(pricing *PricingResult, inputTokens, outputTokens int, usage *r
|
||||
InputCost: InexactFloat64(RoundMoney(baseInputCostD.Mul(mul))),
|
||||
OutputCost: InexactFloat64(RoundMoney(outputCostD.Mul(mul))),
|
||||
TotalCost: InexactFloat64(RoundMoney(totalCostD)),
|
||||
InputTokens: inputTokens,
|
||||
InputTokens: baseInputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
BillingMode: pricing.BillingMode,
|
||||
PerRequestPrice: pricing.PerRequestPrice,
|
||||
DiscountRatio: pricing.DiscountRatio,
|
||||
TenantMultiplier: pricing.TenantMultiplier,
|
||||
Currency: pricing.Currency,
|
||||
CacheCreationTokens: cacheCreationTokens,
|
||||
CacheCreationTokens: cacheCreation5mTokens + cacheCreation1hTokens,
|
||||
CacheReadTokens: cacheReadTokens,
|
||||
CacheCreationCost: InexactFloat64(RoundMoney(cacheCreationCostD.Mul(mul))),
|
||||
CacheReadCost: InexactFloat64(RoundMoney(cacheReadCostD.Mul(mul))),
|
||||
@@ -345,17 +353,25 @@ func computeCost(pricing *PricingResult, inputTokens, outputTokens int, usage *r
|
||||
|
||||
// resolveTokenCounts 根据 usage 信息解析最终的 token 计数。
|
||||
// 处理 cacheIncludedInPrompt 逻辑:如果 PromptTokens 包含 cache tokens,则扣减以避免重复计费。
|
||||
func resolveTokenCounts(pricing *PricingResult, inputTokens, outputTokens int, usage *rcommon.Usage) (baseInput, output, cacheRead, cacheCreation int) {
|
||||
// 返回:baseInput, output, cacheRead, cacheCreation5m, cacheCreation1h
|
||||
func resolveTokenCounts(pricing *PricingResult, inputTokens, outputTokens int, usage *rcommon.Usage) (baseInput, output, cacheRead, cacheCreation5m, cacheCreation1h int) {
|
||||
baseInput = inputTokens
|
||||
output = outputTokens
|
||||
|
||||
if usage != nil {
|
||||
if usage.PromptTokensDetails != nil {
|
||||
cacheRead = usage.PromptTokensDetails.CachedTokens
|
||||
cacheCreation = usage.PromptTokensDetails.CachedCreationTokens
|
||||
// 优先使用细分的 5m/1h token(Claude 新协议)
|
||||
if usage.PromptTokensDetails.CachedCreation5mTokens > 0 || usage.PromptTokensDetails.CachedCreation1hTokens > 0 {
|
||||
cacheCreation5m = usage.PromptTokensDetails.CachedCreation5mTokens
|
||||
cacheCreation1h = usage.PromptTokensDetails.CachedCreation1hTokens
|
||||
} else if usage.PromptTokensDetails.CachedCreationTokens > 0 {
|
||||
// 旧协议无 TTL 细分:全部按 5m 兜底(保守计费)
|
||||
cacheCreation5m = usage.PromptTokensDetails.CachedCreationTokens
|
||||
}
|
||||
}
|
||||
if usage.CacheIncludedInPrompt {
|
||||
baseInput = inputTokens - cacheRead - cacheCreation
|
||||
baseInput = inputTokens - cacheRead - cacheCreation5m - cacheCreation1h
|
||||
if baseInput < 0 {
|
||||
baseInput = 0
|
||||
}
|
||||
|
||||
@@ -556,13 +556,15 @@ func TestComputeCost_ZeroTokens(t *testing.T) {
|
||||
|
||||
func TestComputeCost_WithCacheTokens(t *testing.T) {
|
||||
pricing := &PricingResult{
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 30.0,
|
||||
CacheReadPrice: 1.0, // $1/1M cache read
|
||||
CacheCreationPrice: 2.5, // $2.5/1M cache creation
|
||||
BillingMode: "token",
|
||||
TenantMultiplier: 1.0,
|
||||
Currency: "USD",
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 30.0,
|
||||
CacheReadPrice: 1.0, // $1/1M cache read
|
||||
CacheCreationPrice: 2.5, // $2.5/1M cache creation(5m 基础价)
|
||||
CacheCreation5mPrice: 2.5, // 5m TTL 价格
|
||||
CacheCreation1hPrice: 4.0, // 1h TTL 价格(2.5 × 1.6)
|
||||
BillingMode: "token",
|
||||
TenantMultiplier: 1.0,
|
||||
Currency: "USD",
|
||||
}
|
||||
|
||||
usage := &rcommon.Usage{
|
||||
@@ -599,13 +601,15 @@ func TestComputeCost_WithCacheTokens(t *testing.T) {
|
||||
|
||||
func TestComputeCost_CacheNotIncludedInPrompt(t *testing.T) {
|
||||
pricing := &PricingResult{
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 30.0,
|
||||
CacheReadPrice: 1.0,
|
||||
CacheCreationPrice: 2.5,
|
||||
BillingMode: "token",
|
||||
TenantMultiplier: 1.0,
|
||||
Currency: "USD",
|
||||
InputPrice: 10.0,
|
||||
OutputPrice: 30.0,
|
||||
CacheReadPrice: 1.0,
|
||||
CacheCreationPrice: 2.5,
|
||||
CacheCreation5mPrice: 2.5,
|
||||
CacheCreation1hPrice: 4.0,
|
||||
BillingMode: "token",
|
||||
TenantMultiplier: 1.0,
|
||||
Currency: "USD",
|
||||
}
|
||||
|
||||
usage := &rcommon.Usage{
|
||||
@@ -660,9 +664,9 @@ func TestComputeCost_CacheWithTenantMultiplier(t *testing.T) {
|
||||
// ─── resolveTokenCounts ─────────────────────────────────────────────
|
||||
|
||||
func TestResolveTokenCounts_NoUsage(t *testing.T) {
|
||||
baseIn, out, cr, cc := resolveTokenCounts(&PricingResult{}, 1000, 500, nil)
|
||||
if baseIn != 1000 || out != 500 || cr != 0 || cc != 0 {
|
||||
t.Errorf("expected (1000,500,0,0), got (%d,%d,%d,%d)", baseIn, out, cr, cc)
|
||||
baseIn, out, cr, cc5m, cc1h := resolveTokenCounts(&PricingResult{}, 1000, 500, nil)
|
||||
if baseIn != 1000 || out != 500 || cr != 0 || cc5m != 0 || cc1h != 0 {
|
||||
t.Errorf("expected (1000,500,0,0,0), got (%d,%d,%d,%d,%d)", baseIn, out, cr, cc5m, cc1h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,12 +679,31 @@ func TestResolveTokenCounts_CacheIncluded(t *testing.T) {
|
||||
},
|
||||
CacheIncludedInPrompt: true,
|
||||
}
|
||||
baseIn, _, cr, cc := resolveTokenCounts(&PricingResult{}, 10_000, 500, usage)
|
||||
baseIn, _, cr, cc5m, cc1h := resolveTokenCounts(&PricingResult{}, 10_000, 500, usage)
|
||||
if baseIn != 5_000 {
|
||||
t.Errorf("baseInput = %d, want 5000 (10000 - 3000 - 2000)", baseIn)
|
||||
}
|
||||
if cr != 3_000 || cc != 2_000 {
|
||||
t.Errorf("cache: read=%d creation=%d, want 3000, 2000", cr, cc)
|
||||
if cr != 3_000 || cc5m != 2_000 || cc1h != 0 {
|
||||
t.Errorf("cache: read=%d creation5m=%d creation1h=%d, want 3000, 2000, 0", cr, cc5m, cc1h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTokenCounts_CacheIncluded_5m1hSplit(t *testing.T) {
|
||||
usage := &rcommon.Usage{
|
||||
PromptTokens: 10_000,
|
||||
PromptTokensDetails: &rcommon.TokenDetails{
|
||||
CachedTokens: 3_000,
|
||||
CachedCreation5mTokens: 1_200,
|
||||
CachedCreation1hTokens: 800,
|
||||
},
|
||||
CacheIncludedInPrompt: true,
|
||||
}
|
||||
baseIn, _, cr, cc5m, cc1h := resolveTokenCounts(&PricingResult{}, 10_000, 500, usage)
|
||||
if baseIn != 5_000 {
|
||||
t.Errorf("baseInput = %d, want 5000 (10000 - 3000 - 1200 - 800)", baseIn)
|
||||
}
|
||||
if cr != 3_000 || cc5m != 1_200 || cc1h != 800 {
|
||||
t.Errorf("cache: read=%d creation5m=%d creation1h=%d, want 3000, 1200, 800", cr, cc5m, cc1h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,7 +716,7 @@ func TestResolveTokenCounts_CacheExceedsInput(t *testing.T) {
|
||||
},
|
||||
CacheIncludedInPrompt: true,
|
||||
}
|
||||
baseIn, _, _, _ := resolveTokenCounts(&PricingResult{}, 100, 50, usage)
|
||||
baseIn, _, _, _, _ := resolveTokenCounts(&PricingResult{}, 100, 50, usage)
|
||||
// 100 - 200 - 50 = -150 → clamped to 0
|
||||
if baseIn != 0 {
|
||||
t.Errorf("baseInput = %d, want 0 (clamped)", baseIn)
|
||||
@@ -708,7 +731,7 @@ func TestResolveTokenCounts_CacheNotIncluded(t *testing.T) {
|
||||
},
|
||||
CacheIncludedInPrompt: false,
|
||||
}
|
||||
baseIn, _, cr, _ := resolveTokenCounts(&PricingResult{}, 10_000, 500, usage)
|
||||
baseIn, _, cr, _, _ := resolveTokenCounts(&PricingResult{}, 10_000, 500, usage)
|
||||
// No deduction when cache not included
|
||||
if baseIn != 10_000 {
|
||||
t.Errorf("baseInput = %d, want 10000 (no deduction)", baseIn)
|
||||
@@ -725,8 +748,8 @@ func TestResolveTokenCounts_NilTokenDetails(t *testing.T) {
|
||||
PromptTokensDetails: nil,
|
||||
CacheIncludedInPrompt: true,
|
||||
}
|
||||
baseIn, out, cr, cc := resolveTokenCounts(&PricingResult{}, 1000, 500, usage)
|
||||
if baseIn != 1000 || out != 500 || cr != 0 || cc != 0 {
|
||||
t.Errorf("expected (1000,500,0,0), got (%d,%d,%d,%d)", baseIn, out, cr, cc)
|
||||
baseIn, out, cr, cc5m, cc1h := resolveTokenCounts(&PricingResult{}, 1000, 500, usage)
|
||||
if baseIn != 1000 || out != 500 || cr != 0 || cc5m != 0 || cc1h != 0 {
|
||||
t.Errorf("expected (1000,500,0,0,0), got (%d,%d,%d,%d,%d)", baseIn, out, cr, cc5m, cc1h)
|
||||
}
|
||||
}
|
||||
@@ -693,7 +693,7 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He
|
||||
billing.IncrApiKeyQuotaUsed(settleCtx, rc.ApiKeyID, settleResult.ActualCost)
|
||||
}
|
||||
}
|
||||
recordFailedUsage(provider, rc, selection, v.modelName, v.relayMode, v.isStream, err)
|
||||
recordFailedUsageWithTokens(provider, rc, selection, v.modelName, v.relayMode, v.isStream, err, streamUsage)
|
||||
finalizeTrace(trace, rc, hop, false, attempt, selection, err.Error(), info.LatencyMs())
|
||||
return usage, v.billingResult, err
|
||||
}
|
||||
@@ -1002,6 +1002,37 @@ func recordFailedUsage(provider common.DataProvider, rc *RelayContext, selection
|
||||
provider.RecordUsage(context.Background(), record)
|
||||
}
|
||||
|
||||
// recordFailedUsageWithTokens 记录失败用量(含 token 明细,用于流中断等已有部分 usage 的场景)。
|
||||
// 与 recordFailedUsage 的区别:此函数会填充 token 字段,避免报表中流中断记录的 token 全为 0。
|
||||
func recordFailedUsageWithTokens(provider common.DataProvider, rc *RelayContext, selection *common.ChannelSelection, modelName string, relayMode constant.RelayMode, isStream bool, err error, usage *common.Usage) {
|
||||
record := &common.UsageRecord{
|
||||
TenantID: rc.TenantID,
|
||||
UserID: rc.UserID,
|
||||
ApiKeyID: rc.ApiKeyID,
|
||||
ProjectID: rc.ProjectID,
|
||||
ModelName: modelName,
|
||||
RequestedModel: modelName,
|
||||
RelayMode: int(relayMode),
|
||||
IsStream: isStream,
|
||||
Success: false,
|
||||
RequestID: rc.RequestID,
|
||||
Status: "error",
|
||||
ErrorMessage: err.Error(),
|
||||
}
|
||||
if selection != nil {
|
||||
record.ChannelID = selection.ChannelID
|
||||
record.ChannelName = selection.ChannelName
|
||||
record.ChannelType = selection.ChannelType
|
||||
record.UpstreamModel = selection.UpstreamModelName
|
||||
}
|
||||
if usage != nil {
|
||||
record.PromptTokens = usage.PromptTokens
|
||||
record.CompletionTokens = usage.CompletionTokens
|
||||
record.TotalTokens = usage.TotalTokens
|
||||
}
|
||||
provider.RecordUsage(context.Background(), record)
|
||||
}
|
||||
|
||||
// recordChannelError 记录渠道错误事件到 chn_error_events(异步,不阻塞请求)
|
||||
func recordChannelError(rc *RelayContext, selection *common.ChannelSelection, modelName string, attempt int, isFinal bool, err error, latencyMs float64) {
|
||||
if commonlogic.DefaultChannelErrorWriter == nil {
|
||||
|
||||
Reference in new issue
Block a user