Files
new-api/setting/operation_setting/tools.go
T

220 lines
6.5 KiB
Go

package operation_setting
import (
"sort"
"strings"
"sync/atomic"
"github.com/QuantumNous/new-api/setting/config"
)
// ---------------------------------------------------------------------------
// Tool call prices ($/1K calls, admin-configurable)
// 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, // 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.
type ToolPriceSetting struct {
Prices map[string]float64 `json:"prices"`
}
var toolPriceSetting = ToolPriceSetting{
Prices: func() map[string]float64 {
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()
}
// ---------------------------------------------------------------------------
// 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
}
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)
// ---------------------------------------------------------------------------
const (
GPTImage1Low1024x1024 = 0.011
GPTImage1Low1024x1536 = 0.016
GPTImage1Low1536x1024 = 0.016
GPTImage1Medium1024x1024 = 0.042
GPTImage1Medium1024x1536 = 0.063
GPTImage1Medium1536x1024 = 0.063
GPTImage1High1024x1024 = 0.167
GPTImage1High1024x1536 = 0.25
GPTImage1High1536x1024 = 0.25
)
func GetGPTImage1PriceOnceCall(quality string, size string) float64 {
prices := map[string]map[string]float64{
"low": {
"1024x1024": GPTImage1Low1024x1024,
"1024x1536": GPTImage1Low1024x1536,
"1536x1024": GPTImage1Low1536x1024,
},
"medium": {
"1024x1024": GPTImage1Medium1024x1024,
"1024x1536": GPTImage1Medium1024x1536,
"1536x1024": GPTImage1Medium1536x1024,
},
"high": {
"1024x1024": GPTImage1High1024x1024,
"1024x1536": GPTImage1High1024x1536,
"1536x1024": GPTImage1High1536x1024,
},
}
if qualityMap, exists := prices[quality]; exists {
if price, exists := qualityMap[size]; exists {
return price
}
}
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
}