Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions database/billing.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ var (

modelPricingRules = []modelPricingRule{
{model: "gpt-5.5", pricing: ModelPricing{
InputPricePerMToken: 5.0,
InputPricePerMTokenPriority: 12.5,
OutputPricePerMToken: 30.0,
OutputPricePerMTokenPriority: 75.0,
CacheReadPricePerMToken: 0.5,
CacheReadPricePerMTokenPriority: 1.25,
InputPricePerMToken: 5.0,
InputPricePerMTokenPriority: 12.5,
OutputPricePerMToken: 30.0,
OutputPricePerMTokenPriority: 75.0,
CacheReadPricePerMToken: 0.5,
CacheReadPricePerMTokenPriority: 1.25,
LongInputPricePerMToken: 10.0,
LongInputPricePerMTokenPriority: 25.0,
LongOutputPricePerMToken: 45.0,
Expand All @@ -55,10 +55,10 @@ var (
LongCacheReadPricePerMTokenPriority: 2.5,
}},
{model: "gpt-5.5-pro", pricing: ModelPricing{
InputPricePerMToken: 30.0,
InputPricePerMTokenPriority: 75.0,
OutputPricePerMToken: 180.0,
OutputPricePerMTokenPriority: 450.0,
InputPricePerMToken: 30.0,
InputPricePerMTokenPriority: 75.0,
OutputPricePerMToken: 180.0,
OutputPricePerMTokenPriority: 450.0,
LongInputPricePerMToken: 60.0,
LongInputPricePerMTokenPriority: 150.0,
LongOutputPricePerMToken: 270.0,
Expand All @@ -67,12 +67,12 @@ var (
{model: "gpt-5.4-mini", pricing: ModelPricing{InputPricePerMToken: 0.75, OutputPricePerMToken: 4.5, CacheReadPricePerMToken: 0.075}},
{model: "gpt-5.4-nano", pricing: ModelPricing{InputPricePerMToken: 0.2, OutputPricePerMToken: 1.25, CacheReadPricePerMToken: 0.02}},
{model: "gpt-5.4", pricing: ModelPricing{
InputPricePerMToken: 2.5,
InputPricePerMTokenPriority: 5.0,
OutputPricePerMToken: 15.0,
OutputPricePerMTokenPriority: 30.0,
CacheReadPricePerMToken: 0.25,
CacheReadPricePerMTokenPriority: 0.5,
InputPricePerMToken: 2.5,
InputPricePerMTokenPriority: 5.0,
OutputPricePerMToken: 15.0,
OutputPricePerMTokenPriority: 30.0,
CacheReadPricePerMToken: 0.25,
CacheReadPricePerMTokenPriority: 0.5,
LongInputPricePerMToken: 5.0,
LongInputPricePerMTokenPriority: 10.0,
LongOutputPricePerMToken: 22.5,
Expand All @@ -81,10 +81,10 @@ var (
LongCacheReadPricePerMTokenPriority: 1.0,
}},
{model: "gpt-5.4-pro", pricing: ModelPricing{
InputPricePerMToken: 30.0,
InputPricePerMTokenPriority: 75.0,
OutputPricePerMToken: 180.0,
OutputPricePerMTokenPriority: 450.0,
InputPricePerMToken: 30.0,
InputPricePerMTokenPriority: 75.0,
OutputPricePerMToken: 180.0,
OutputPricePerMTokenPriority: 450.0,
LongInputPricePerMToken: 60.0,
LongInputPricePerMTokenPriority: 150.0,
LongOutputPricePerMToken: 270.0,
Expand Down Expand Up @@ -326,7 +326,8 @@ func geminiFamilyPricing(model string) *ModelPricing {
}

func usePriorityPricing(serviceTier string, pricing *ModelPricing) bool {
if normalizeServiceTier(serviceTier) != "priority" {
tier := normalizeServiceTier(serviceTier)
if tier != "priority" && tier != "fast" {
return false
}
return pricing.InputPricePerMTokenPriority > 0 ||
Expand All @@ -336,8 +337,6 @@ func usePriorityPricing(serviceTier string, pricing *ModelPricing) bool {

func serviceTierCostMultiplier(serviceTier string) float64 {
switch normalizeServiceTier(serviceTier) {
case "priority":
return 2.0
case "flex":
return 0.5
default:
Expand Down
29 changes: 28 additions & 1 deletion database/billing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ func TestCalculateCostHandlesCachedTokensAndServiceTier(t *testing.T) {
cachedTokens: 200,
want: 0.0191,
},
{
name: "uses priority prices for fast tier",
model: "gpt-5.4",
serviceTier: "fast",
inputTokens: 1000,
outputTokens: 500,
cachedTokens: 200,
want: 0.0191,
},
{
name: "does not invent priority multiplier when priority price is unknown",
model: "gpt-4o",
serviceTier: "priority",
inputTokens: 1000,
outputTokens: 500,
cachedTokens: 200,
want: 0.0075,
},
{
name: "fast tier falls back to standard pricing when priority price is unknown",
model: "gpt-4o",
serviceTier: "fast",
inputTokens: 1000,
outputTokens: 500,
cachedTokens: 200,
want: 0.0075,
},
{
name: "applies flex multiplier",
model: "gpt-5.4",
Expand Down Expand Up @@ -277,7 +304,7 @@ func TestCodexAutoReviewModelNormalizesToGPT54(t *testing.T) {
func TestCodexAutoReviewLongContextPricing(t *testing.T) {
// codex-auto-review maps to gpt-5.4 which has long context pricing.
long := CalculateCostBreakdown(300000, 500, 100, "codex-auto-review", "")
assertFloatEqual(t, long.InputPricePerMToken, 5.0) // long input price
assertFloatEqual(t, long.InputPricePerMToken, 5.0) // long input price
assertFloatEqual(t, long.OutputPricePerMToken, 22.5) // long output price
assertFloatEqual(t, long.CacheReadPricePerMToken, 0.5) // long cache read price
}
Expand Down
74 changes: 40 additions & 34 deletions database/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -1543,8 +1543,13 @@ func (db *DB) InsertUsageLog(ctx context.Context, log *UsageLogInput) error {
billingModel = log.Model
}

// 计算账号计费金额(标准费用)
accountBilled := calculateCost(log.InputTokens, log.OutputTokens, log.CachedTokens, billingModel, log.ServiceTier)
billingServiceTier := log.BillingServiceTier
if billingServiceTier == "" {
billingServiceTier = log.ServiceTier
}

// 计算账号计费金额(基于上游实际 service tier)
accountBilled := calculateCost(log.InputTokens, log.OutputTokens, log.CachedTokens, billingModel, billingServiceTier)

// 用户计费金额与账号计费金额相同(简化版,未来可支持倍率)
userBilled := accountBilled
Expand Down Expand Up @@ -1598,38 +1603,39 @@ func (db *DB) InsertUsageLog(ctx context.Context, log *UsageLogInput) error {

// UsageLogInput 日志写入参数
type UsageLogInput struct {
AccountID int64
Endpoint string
Model string
EffectiveModel string
PromptTokens int
CompletionTokens int
TotalTokens int
StatusCode int
DurationMs int
InputTokens int
OutputTokens int
ReasoningTokens int
FirstTokenMs int
ReasoningEffort string
InboundEndpoint string
UpstreamEndpoint string
Stream bool
CachedTokens int
ServiceTier string
APIKeyID int64
APIKeyName string
APIKeyMasked string
ImageCount int
ImageWidth int
ImageHeight int
ImageBytes int
ImageFormat string
ImageSize string
IsRetryAttempt bool
AttemptIndex int
UpstreamErrorKind string
ErrorMessage string
AccountID int64
Endpoint string
Model string
EffectiveModel string
PromptTokens int
CompletionTokens int
TotalTokens int
StatusCode int
DurationMs int
InputTokens int
OutputTokens int
ReasoningTokens int
FirstTokenMs int
ReasoningEffort string
InboundEndpoint string
UpstreamEndpoint string
Stream bool
CachedTokens int
ServiceTier string
BillingServiceTier string
APIKeyID int64
APIKeyName string
APIKeyMasked string
ImageCount int
ImageWidth int
ImageHeight int
ImageBytes int
ImageFormat string
ImageSize string
IsRetryAttempt bool
AttemptIndex int
UpstreamErrorKind string
ErrorMessage string
}

func (l *UsageLog) populateBillingBreakdown() {
Expand Down
68 changes: 68 additions & 0 deletions database/sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,74 @@ func TestUsageLogsReturnBillingFields(t *testing.T) {
}
}

func TestUsageLogsBillFastByActualServiceTier(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "codex2api.db")

db, err := New("sqlite", dbPath)
if err != nil {
t.Fatalf("New(sqlite) 返回错误: %v", err)
}
defer db.Close()

ctx := context.Background()
if err := db.InsertUsageLog(ctx, &UsageLogInput{
AccountID: 1,
Endpoint: "/v1/responses",
Model: "gpt-5.4",
StatusCode: 200,
InputTokens: 1000,
OutputTokens: 500,
CachedTokens: 200,
ServiceTier: "fast",
BillingServiceTier: "default",
}); err != nil {
t.Fatalf("InsertUsageLog 返回错误: %v", err)
}
if err := db.InsertUsageLog(ctx, &UsageLogInput{
AccountID: 1,
Endpoint: "/v1/responses",
Model: "gpt-5.4",
StatusCode: 200,
InputTokens: 1000,
OutputTokens: 500,
CachedTokens: 200,
ServiceTier: "fast",
BillingServiceTier: "priority",
}); err != nil {
t.Fatalf("InsertUsageLog 返回错误: %v", err)
}
db.flushLogs()

logs, err := db.ListRecentUsageLogs(ctx, 10)
if err != nil {
t.Fatalf("ListRecentUsageLogs 返回错误: %v", err)
}
if len(logs) != 2 {
t.Fatalf("len(logs) = %d, want 2", len(logs))
}

wantPriority := calculateCost(1000, 500, 200, "gpt-5.4", "priority")
wantDefault := calculateCost(1000, 500, 200, "gpt-5.4", "default")
seenPriority := false
seenDefault := false
for _, log := range logs {
if log.ServiceTier != "fast" {
t.Fatalf("log tier = %q, want fast", log.ServiceTier)
}
switch log.AccountBilled {
case wantPriority:
seenPriority = true
case wantDefault:
seenDefault = true
default:
t.Fatalf("unexpected billed amount %.12f, want %.12f or %.12f", log.AccountBilled, wantPriority, wantDefault)
}
}
if !seenPriority || !seenDefault {
t.Fatalf("billing tiers seen priority=%v default=%v, want both", seenPriority, seenDefault)
}
}

func TestUsageLogsReturnErrorMessage(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "codex2api.db")

Expand Down
Loading
Loading