Skip to content

Commit 29b1c94

Browse files
committed
feat: show billing costs in usage stats
1 parent 9ce475f commit 29b1c94

9 files changed

Lines changed: 523 additions & 62 deletions

File tree

database/billing.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,30 @@ type modelPricingRule struct {
1717
pricing ModelPricing
1818
}
1919

20+
type costBreakdown struct {
21+
InputCost float64
22+
OutputCost float64
23+
CacheReadCost float64
24+
TotalCost float64
25+
InputPricePerMToken float64
26+
OutputPricePerMToken float64
27+
CacheReadPricePerMToken float64
28+
ServiceTierCostMultiplier float64
29+
}
30+
2031
var (
2132
defaultModelPricing = &ModelPricing{InputPricePerMToken: 1.0, OutputPricePerMToken: 2.0}
2233

2334
modelPricingRules = []modelPricingRule{
2435
// Codex/GPT-5 系列,参考 sub2api 的本地 fallback 定价。
36+
{model: "gpt-5.5", pricing: ModelPricing{
37+
InputPricePerMToken: 5.0,
38+
InputPricePerMTokenPriority: 10.0,
39+
OutputPricePerMToken: 30.0,
40+
OutputPricePerMTokenPriority: 60.0,
41+
CacheReadPricePerMToken: 0.5,
42+
CacheReadPricePerMTokenPriority: 1.0,
43+
}},
2544
{model: "gpt-5.4-mini", pricing: ModelPricing{InputPricePerMToken: 0.75, OutputPricePerMToken: 4.5, CacheReadPricePerMToken: 0.075}},
2645
{model: "gpt-5.4-nano", pricing: ModelPricing{InputPricePerMToken: 0.2, OutputPricePerMToken: 1.25, CacheReadPricePerMToken: 0.02}},
2746
{model: "gpt-5.4", pricing: ModelPricing{
@@ -91,6 +110,10 @@ func getModelPricing(model string) *ModelPricing {
91110
// model: 模型名称
92111
// 返回:账号计费金额(美元)
93112
func calculateCost(inputTokens, outputTokens, cachedTokens int, model string, serviceTier string) float64 {
113+
return calculateCostBreakdown(inputTokens, outputTokens, cachedTokens, model, serviceTier).TotalCost
114+
}
115+
116+
func calculateCostBreakdown(inputTokens, outputTokens, cachedTokens int, model string, serviceTier string) costBreakdown {
94117
pricing := getModelPricing(model)
95118
inputPrice := pricing.InputPricePerMToken
96119
outputPrice := pricing.OutputPricePerMToken
@@ -126,7 +149,16 @@ func calculateCost(inputTokens, outputTokens, cachedTokens int, model string, se
126149
cacheReadCost := float64(cachedTokens) / 1000000.0 * cacheReadPrice
127150
outputCost := float64(outputTokens) / 1000000.0 * outputPrice
128151

129-
return (inputCost + cacheReadCost + outputCost) * tierMultiplier
152+
return costBreakdown{
153+
InputCost: inputCost * tierMultiplier,
154+
OutputCost: outputCost * tierMultiplier,
155+
CacheReadCost: cacheReadCost * tierMultiplier,
156+
TotalCost: (inputCost + cacheReadCost + outputCost) * tierMultiplier,
157+
InputPricePerMToken: inputPrice * tierMultiplier,
158+
OutputPricePerMToken: outputPrice * tierMultiplier,
159+
CacheReadPricePerMToken: cacheReadPrice * tierMultiplier,
160+
ServiceTierCostMultiplier: tierMultiplier,
161+
}
130162
}
131163

132164
func normalizeBillingModelName(model string) string {
@@ -148,6 +180,8 @@ func normalizeBillingModelName(model string) string {
148180
func normalizeCodexBillingModel(model string) (string, bool) {
149181
compact := strings.NewReplacer(" ", "-", "_", "-").Replace(model)
150182
switch {
183+
case strings.Contains(compact, "gpt-5.5") || strings.Contains(compact, "gpt5-5") || strings.Contains(compact, "gpt5.5"):
184+
return "gpt-5.5", true
151185
case strings.Contains(compact, "gpt-5.4-mini") || strings.Contains(compact, "gpt5-4-mini") || strings.Contains(compact, "gpt5.4-mini"):
152186
return "gpt-5.4-mini", true
153187
case strings.Contains(compact, "gpt-5.4-nano") || strings.Contains(compact, "gpt5-4-nano") || strings.Contains(compact, "gpt5.4-nano"):

database/billing_test.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestGetModelPricingUsesSub2APICodexFallbacks(t *testing.T) {
3232
}{
3333
{model: "gpt-5.4-mini-20260401", wantInput: 0.75, wantOutput: 4.5},
3434
{model: "gpt-5.3-codex-spark", wantInput: 1.5, wantOutput: 12.0},
35-
{model: "gpt-5.5", wantInput: 2.5, wantOutput: 15.0},
35+
{model: "gpt-5.5", wantInput: 5.0, wantOutput: 30.0},
3636
}
3737

3838
for _, tt := range tests {
@@ -120,6 +120,47 @@ func TestCalculateCostHandlesCachedTokensAndServiceTier(t *testing.T) {
120120
}
121121
}
122122

123+
func TestCalculateCostBreakdownExposesDisplayFields(t *testing.T) {
124+
got := calculateCostBreakdown(1000, 500, 200, "gpt-5.4", "flex")
125+
126+
assertFloatEqual(t, got.InputCost, 0.001)
127+
assertFloatEqual(t, got.CacheReadCost, 0.000025)
128+
assertFloatEqual(t, got.OutputCost, 0.00375)
129+
assertFloatEqual(t, got.TotalCost, 0.004775)
130+
assertFloatEqual(t, got.InputPricePerMToken, 1.25)
131+
assertFloatEqual(t, got.CacheReadPricePerMToken, 0.125)
132+
assertFloatEqual(t, got.OutputPricePerMToken, 7.5)
133+
assertFloatEqual(t, got.ServiceTierCostMultiplier, 0.5)
134+
}
135+
136+
func TestGPT55PricingIsDoubleGPT54(t *testing.T) {
137+
gpt54 := getModelPricing("gpt-5.4")
138+
gpt55 := getModelPricing("gpt-5.5")
139+
140+
assertFloatEqual(t, gpt55.InputPricePerMToken, gpt54.InputPricePerMToken*2)
141+
assertFloatEqual(t, gpt55.OutputPricePerMToken, gpt54.OutputPricePerMToken*2)
142+
assertFloatEqual(t, gpt55.CacheReadPricePerMToken, gpt54.CacheReadPricePerMToken*2)
143+
assertFloatEqual(t, gpt55.InputPricePerMTokenPriority, gpt54.InputPricePerMTokenPriority*2)
144+
assertFloatEqual(t, gpt55.OutputPricePerMTokenPriority, gpt54.OutputPricePerMTokenPriority*2)
145+
assertFloatEqual(t, gpt55.CacheReadPricePerMTokenPriority, gpt54.CacheReadPricePerMTokenPriority*2)
146+
}
147+
148+
func TestUsageLogBreakdownScalesToStoredBilledTotal(t *testing.T) {
149+
log := &UsageLog{
150+
Model: "gpt-5.5",
151+
InputTokens: 1000,
152+
StatusCode: 200,
153+
AccountBilled: 0.0025,
154+
UserBilled: 0.0025,
155+
}
156+
157+
log.populateBillingBreakdown()
158+
159+
assertFloatEqual(t, log.TotalCost, 0.0025)
160+
assertFloatEqual(t, log.InputCost, 0.0025)
161+
assertFloatEqual(t, log.InputPrice, 2.5)
162+
}
163+
123164
func assertPricing(t *testing.T, got *ModelPricing, wantInput, wantOutput float64) {
124165
t.Helper()
125166
if got == nil {
@@ -130,3 +171,10 @@ func assertPricing(t *testing.T, got *ModelPricing, wantInput, wantOutput float6
130171
got.InputPricePerMToken, got.OutputPricePerMToken, wantInput, wantOutput)
131172
}
132173
}
174+
175+
func assertFloatEqual(t *testing.T, got, want float64) {
176+
t.Helper()
177+
if math.Abs(got-want) > 1e-12 {
178+
t.Fatalf("got %.12f, want %.12f", got, want)
179+
}
180+
}

0 commit comments

Comments
 (0)