feat: implement tool pricing settings UI and enhance tool call quota calculations
This commit is contained in:
@@ -1,21 +1,36 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tool call prices ($/1K calls, admin-configurable)
|
||||
// DB keys: tool_price_setting.prices
|
||||
// DB key: tool_price_setting.prices
|
||||
//
|
||||
// Key format:
|
||||
// - "tool_name" → default price for all models
|
||||
// - "tool_name:model_prefix*" → override for models matching the prefix
|
||||
//
|
||||
// Lookup order: longest prefix match → default → hardcoded fallback → 0
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var defaultToolPrices = map[string]float64{
|
||||
"web_search": 10.0,
|
||||
"web_search_high": 25.0,
|
||||
"claude_web_search": 10.0,
|
||||
"file_search": 2.5,
|
||||
"web_search": 10.0, // OpenAI web search (all models) / Claude web search
|
||||
"web_search_preview": 10.0, // OpenAI web search preview (default: reasoning models)
|
||||
"file_search": 2.5, // OpenAI file search (Responses API)
|
||||
"google_search": 14.0, // Gemini Grounding with Google Search
|
||||
}
|
||||
|
||||
var defaultToolPriceOverrides = map[string]float64{
|
||||
"web_search_preview:gpt-4o*": 25.0, // non-reasoning models
|
||||
"web_search_preview:gpt-4.1*": 25.0,
|
||||
"web_search_preview:gpt-4o-mini*": 25.0,
|
||||
"web_search_preview:gpt-4.1-mini*": 25.0,
|
||||
}
|
||||
|
||||
// ToolPriceSetting is managed by config.GlobalConfig.Register.
|
||||
@@ -25,30 +40,110 @@ type ToolPriceSetting struct {
|
||||
|
||||
var toolPriceSetting = ToolPriceSetting{
|
||||
Prices: func() map[string]float64 {
|
||||
m := make(map[string]float64, len(defaultToolPrices))
|
||||
m := make(map[string]float64, len(defaultToolPrices)+len(defaultToolPriceOverrides))
|
||||
for k, v := range defaultToolPrices {
|
||||
m[k] = v
|
||||
}
|
||||
for k, v := range defaultToolPriceOverrides {
|
||||
m[k] = v
|
||||
}
|
||||
return m
|
||||
}(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
config.GlobalConfig.Register("tool_price_setting", &toolPriceSetting)
|
||||
RebuildToolPriceIndex()
|
||||
}
|
||||
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
// Precomputed price index (atomic, lock-free on read path)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type prefixEntry struct {
|
||||
prefix string
|
||||
price float64
|
||||
}
|
||||
|
||||
type toolPriceIndex struct {
|
||||
defaults map[string]float64
|
||||
prefixes map[string][]prefixEntry
|
||||
}
|
||||
|
||||
var currentIndex atomic.Pointer[toolPriceIndex]
|
||||
|
||||
// RebuildToolPriceIndex rebuilds the lookup index from the current config.
|
||||
// Called on init and after config updates. Not on the billing hot path.
|
||||
func RebuildToolPriceIndex() {
|
||||
merged := make(map[string]float64, len(defaultToolPrices)+len(defaultToolPriceOverrides)+len(toolPriceSetting.Prices))
|
||||
for k, v := range defaultToolPrices {
|
||||
merged[k] = v
|
||||
}
|
||||
if v, ok := defaultToolPrices[key]; ok {
|
||||
return v
|
||||
for k, v := range defaultToolPriceOverrides {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range toolPriceSetting.Prices {
|
||||
merged[k] = v
|
||||
}
|
||||
|
||||
idx := &toolPriceIndex{
|
||||
defaults: make(map[string]float64),
|
||||
prefixes: make(map[string][]prefixEntry),
|
||||
}
|
||||
|
||||
for key, price := range merged {
|
||||
colonIdx := strings.IndexByte(key, ':')
|
||||
if colonIdx < 0 {
|
||||
idx.defaults[key] = price
|
||||
continue
|
||||
}
|
||||
toolName := key[:colonIdx]
|
||||
modelPart := key[colonIdx+1:]
|
||||
prefix := strings.TrimSuffix(modelPart, "*")
|
||||
idx.prefixes[toolName] = append(idx.prefixes[toolName], prefixEntry{prefix: prefix, price: price})
|
||||
}
|
||||
|
||||
for tool := range idx.prefixes {
|
||||
entries := idx.prefixes[tool]
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return len(entries[i].prefix) > len(entries[j].prefix)
|
||||
})
|
||||
idx.prefixes[tool] = entries
|
||||
}
|
||||
|
||||
currentIndex.Store(idx)
|
||||
}
|
||||
|
||||
// GetToolPriceForModel returns the price ($/1K calls) for a tool given a model name.
|
||||
// Lookup: longest prefix match → tool default → 0.
|
||||
func GetToolPriceForModel(toolName, modelName string) float64 {
|
||||
idx := currentIndex.Load()
|
||||
if idx == nil {
|
||||
if v, ok := defaultToolPrices[toolName]; ok {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if entries, ok := idx.prefixes[toolName]; ok && modelName != "" {
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(modelName, e.prefix) {
|
||||
return e.price
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p, ok := idx.defaults[toolName]; ok {
|
||||
return p
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetToolPrice is a convenience wrapper when no model name is needed.
|
||||
func GetToolPrice(toolName string) float64 {
|
||||
return GetToolPriceForModel(toolName, "")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GPT Image 1 per-call pricing (special: depends on quality + size)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user