feat: add billing expression system documentation and enhance tiered billing logic

- Introduced a new rule for the Billing Expression System, emphasizing the importance of reading `pkg/billingexpr/expr.md` for dynamic billing.
- Updated the billing expression logic to support new variables and improved handling of image and audio tokens.
- Enhanced the tiered billing functionality with versioning support for expressions and refined quota calculations.
- Added tests to validate the new billing expression features and ensure correctness in pricing calculations.
This commit is contained in:
CaIon
2026-03-17 15:29:43 +08:00
parent 5b03b39db2
commit c5405b2a12
27 changed files with 894 additions and 578 deletions
+80 -66
View File
@@ -1,15 +1,58 @@
package operation_setting
import "strings"
import (
"strings"
const (
// Web search
WebSearchPriceHigh = 25.00
WebSearchPrice = 10.00
// File search
FileSearchPrice = 2.5
"github.com/QuantumNous/new-api/setting/config"
)
// ---------------------------------------------------------------------------
// Tool call prices ($/1K calls, admin-configurable)
// DB keys: tool_price_setting.prices
// ---------------------------------------------------------------------------
var defaultToolPrices = map[string]float64{
"web_search": 10.0,
"web_search_high": 25.0,
"claude_web_search": 10.0,
"file_search": 2.5,
}
// ToolPriceSetting is managed by config.GlobalConfig.Register.
type ToolPriceSetting struct {
Prices map[string]float64 `json:"prices"`
}
var toolPriceSetting = ToolPriceSetting{
Prices: func() map[string]float64 {
m := make(map[string]float64, len(defaultToolPrices))
for k, v := range defaultToolPrices {
m[k] = v
}
return m
}(),
}
func init() {
config.GlobalConfig.Register("tool_price_setting", &toolPriceSetting)
}
// GetToolPrice returns the configured price for a tool key ($/1K calls),
// falling back to hardcoded default if not overridden.
func GetToolPrice(key string) float64 {
if v, ok := toolPriceSetting.Prices[key]; ok {
return v
}
if v, ok := defaultToolPrices[key]; ok {
return v
}
return 0
}
// ---------------------------------------------------------------------------
// GPT Image 1 per-call pricing (special: depends on quality + size)
// ---------------------------------------------------------------------------
const (
GPTImage1Low1024x1024 = 0.011
GPTImage1Low1024x1536 = 0.016
@@ -22,65 +65,6 @@ const (
GPTImage1High1536x1024 = 0.25
)
const (
// Gemini Audio Input Price
Gemini25FlashPreviewInputAudioPrice = 1.00
Gemini25FlashProductionInputAudioPrice = 1.00 // for `gemini-2.5-flash`
Gemini25FlashLitePreviewInputAudioPrice = 0.50
Gemini25FlashNativeAudioInputAudioPrice = 3.00
Gemini20FlashInputAudioPrice = 0.70
GeminiRoboticsER15InputAudioPrice = 1.00
)
const (
// Claude Web search
ClaudeWebSearchPrice = 10.00
)
func GetClaudeWebSearchPricePerThousand() float64 {
return ClaudeWebSearchPrice
}
func GetWebSearchPricePerThousand(modelName string, contextSize string) float64 {
// 确定模型类型
// https://platform.openai.com/docs/pricing Web search 价格按模型类型收费
// 新版计费规则不再关联 search context size,故在const区域将各size的价格设为一致。
// gpt-5, gpt-5-mini, gpt-5-nano 和 o 系列模型价格为 10.00 美元/千次调用,产生额外 token 计入 input_tokens
// gpt-4o, gpt-4.1, gpt-4o-mini 和 gpt-4.1-mini 价格为 25.00 美元/千次调用,不产生额外 token
isNormalPriceModel :=
strings.HasPrefix(modelName, "o3") ||
strings.HasPrefix(modelName, "o4") ||
strings.HasPrefix(modelName, "gpt-5")
var priceWebSearchPerThousandCalls float64
if isNormalPriceModel {
priceWebSearchPerThousandCalls = WebSearchPrice
} else {
priceWebSearchPerThousandCalls = WebSearchPriceHigh
}
return priceWebSearchPerThousandCalls
}
func GetFileSearchPricePerThousand() float64 {
return FileSearchPrice
}
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
return Gemini25FlashNativeAudioInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-lite") {
return Gemini25FlashLitePreviewInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
return Gemini25FlashPreviewInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash") {
return Gemini25FlashProductionInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
return Gemini20FlashInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-robotics-er-1.5") {
return GeminiRoboticsER15InputAudioPrice
}
return 0
}
func GetGPTImage1PriceOnceCall(quality string, size string) float64 {
prices := map[string]map[string]float64{
"low": {
@@ -108,3 +92,33 @@ func GetGPTImage1PriceOnceCall(quality string, size string) float64 {
return GPTImage1High1024x1024
}
// ---------------------------------------------------------------------------
// Gemini audio input pricing (per-million tokens, model-specific)
// ---------------------------------------------------------------------------
const (
Gemini25FlashPreviewInputAudioPrice = 1.00
Gemini25FlashProductionInputAudioPrice = 1.00
Gemini25FlashLitePreviewInputAudioPrice = 0.50
Gemini25FlashNativeAudioInputAudioPrice = 3.00
Gemini20FlashInputAudioPrice = 0.70
GeminiRoboticsER15InputAudioPrice = 1.00
)
func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-native-audio") {
return Gemini25FlashNativeAudioInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview-lite") {
return Gemini25FlashLitePreviewInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash-preview") {
return Gemini25FlashPreviewInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.5-flash") {
return Gemini25FlashProductionInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-2.0-flash") {
return Gemini20FlashInputAudioPrice
} else if strings.HasPrefix(modelName, "gemini-robotics-er-1.5") {
return GeminiRoboticsER15InputAudioPrice
}
return 0
}