feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
@@ -0,0 +1,45 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
|
||||
const LogMaxRangeDaysOptionKey = "log_setting.max_range_days"
|
||||
|
||||
// LogSetting limits how far regular users can reach into usage logs.
|
||||
type LogSetting struct {
|
||||
// UserExportEnabled lets regular users export their own usage logs.
|
||||
// Administrators can always export.
|
||||
UserExportEnabled bool `json:"user_export_enabled"`
|
||||
// MaxRangeDays is the longest time range a user can search in their logs
|
||||
// and dashboard, and the longest range any export can cover.
|
||||
MaxRangeDays int `json:"max_range_days"`
|
||||
}
|
||||
|
||||
var logSetting = LogSetting{
|
||||
UserExportEnabled: true,
|
||||
MaxRangeDays: 365,
|
||||
}
|
||||
|
||||
func init() {
|
||||
config.GlobalConfig.Register("log_setting", &logSetting)
|
||||
}
|
||||
|
||||
func GetLogSetting() *LogSetting {
|
||||
return &logSetting
|
||||
}
|
||||
|
||||
func (s *LogSetting) MaxRangeSeconds() int64 {
|
||||
return int64(max(s.MaxRangeDays, 1)) * 24 * 60 * 60
|
||||
}
|
||||
|
||||
func ValidateLogMaxRangeDays(value string) error {
|
||||
days, err := strconv.Atoi(value)
|
||||
if err != nil || days < 1 {
|
||||
return common.NewMessage("The longest log time range must be at least 1 day")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
|
||||
type QuotaSetting struct {
|
||||
EnableFreeModelPreConsume bool `json:"enable_free_model_pre_consume"` // 是否对免费模型启用预消耗
|
||||
EnableFreeModelPreConsume bool `json:"enable_free_model_pre_consume"` // 是否对免费模型启用预消耗
|
||||
TrustQuotaUSD float64 `json:"trust_quota_usd"` // 钱包免预扣门槛,0 表示禁用
|
||||
PreConsumeMultiplier float64 `json:"pre_consume_multiplier"` // 预计输入费用的预扣倍率,仅影响预留
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var quotaSetting = QuotaSetting{
|
||||
EnableFreeModelPreConsume: true,
|
||||
TrustQuotaUSD: 10,
|
||||
PreConsumeMultiplier: 1,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -19,3 +29,27 @@ func init() {
|
||||
func GetQuotaSetting() *QuotaSetting {
|
||||
return "aSetting
|
||||
}
|
||||
|
||||
// ValidateQuotaOption validates reservation settings before they are persisted.
|
||||
func ValidateQuotaOption(key, value string) error {
|
||||
if key != "quota_setting.trust_quota_usd" && key != "quota_setting.pre_consume_multiplier" {
|
||||
return nil
|
||||
}
|
||||
number, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil || math.IsNaN(number) || math.IsInf(number, 0) || number < 0 {
|
||||
return fmt.Errorf("%s must be a finite non-negative number", key)
|
||||
}
|
||||
if key == "quota_setting.pre_consume_multiplier" && number == 0 {
|
||||
return fmt.Errorf("%s must be greater than zero", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InputPreConsumeMultiplier rejects invalid runtime settings as well as invalid saves.
|
||||
func InputPreConsumeMultiplier() (float64, error) {
|
||||
multiplier := quotaSetting.PreConsumeMultiplier
|
||||
if multiplier <= 0 || math.IsNaN(multiplier) || math.IsInf(multiplier, 0) {
|
||||
return 0, fmt.Errorf("pre-consume multiplier must be a finite number greater than zero")
|
||||
}
|
||||
return multiplier, nil
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package operation_setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -23,6 +22,8 @@ import (
|
||||
// Effective index: hardcoded defaults → hardcoded model overrides → valid
|
||||
// operator values. Lookup uses the longest model prefix before the tool
|
||||
// default, and a matched numeric zero is terminal.
|
||||
//
|
||||
// Built-in keys are written in the vendor's list currency.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ToolPriceOptionKey = "tool_price_setting.prices"
|
||||
@@ -49,6 +50,49 @@ func seedHardcodedToolPrices(prices map[string]float64) {
|
||||
prices["web_search_preview:gpt-4.1*"] = defaultSearchPreviewModelPrice
|
||||
prices["web_search_preview:gpt-4o-mini*"] = defaultSearchPreviewModelPrice
|
||||
prices["web_search_preview:gpt-4.1-mini*"] = defaultSearchPreviewModelPrice
|
||||
|
||||
// Google Search grounding (USD per 1K, ai.google.dev/gemini-api/docs/pricing
|
||||
// and cloud.google.com/vertex-ai/generative-ai/pricing): Gemini 3 bills each
|
||||
// search query at $14 (the google_search default); Gemini 2.5 and older bill
|
||||
// $35 per grounded prompt, so their query count is free and the grounded
|
||||
// prompt carries the price.
|
||||
prices["google_search:gemini-2.5*"] = 0
|
||||
prices["google_search:gemini-2.0*"] = 0
|
||||
prices["google_search:gemini-1.5*"] = 0
|
||||
prices["google_search_grounded_prompt:gemini-2.5*"] = 35
|
||||
prices["google_search_grounded_prompt:gemini-2.0*"] = 35
|
||||
prices["google_search_grounded_prompt:gemini-1.5*"] = 35
|
||||
|
||||
// Vendor search tiers, verbatim in the vendor's list currency per 1K
|
||||
// calls. CNY: Zhipu search engines, 0.01/0.03/0.05 CNY per call
|
||||
// (https://docs.bigmodel.cn/cn/guide/tools/web-search), and Alibaba Model
|
||||
// Studio search strategies at Beijing prices
|
||||
// (https://help.aliyun.com/zh/model-studio/web-search); the Singapore
|
||||
// agent price (73.392381 CNY) is set by the operator. USD: Azure
|
||||
// Responses web search, billed as Bing transactions at $14 per 1,000
|
||||
// (https://www.microsoft.com/en-us/bing/apis), and xAI web search $5 per
|
||||
// 1K calls, x_search $5 per 1K posts and $10 per 1K profiles
|
||||
// (https://docs.x.ai/developers/pricing).
|
||||
prices["search_std"] = 10
|
||||
prices["search_pro"] = 30
|
||||
prices["search_pro_sogou"] = 50
|
||||
prices["search_pro_quark"] = 50
|
||||
prices["search_strategy_turbo"] = 3
|
||||
prices["search_strategy_max"] = 4
|
||||
prices["search_strategy_agent"] = 4
|
||||
prices["search_strategy_agent_max"] = 4
|
||||
prices["bing_web_search"] = 14
|
||||
prices["web_search:grok*"] = 5
|
||||
prices["x_search_posts"] = 5
|
||||
prices["x_search_profiles"] = 10
|
||||
|
||||
// DeepSeek Search and Fetch APIs, $0.75 per 1,000 successful requests
|
||||
// each (https://api-docs.deepseek.com/quick_start/pricing), for the
|
||||
// searches and page fetches DeepSeek runs server-side for Claude Code and
|
||||
// Codex traffic. /v1/tools/search and /v1/tools/fetch bill through the
|
||||
// deepseek-search and deepseek-fetch model prices instead.
|
||||
prices["deepseek_search"] = 0.75
|
||||
prices["deepseek_fetch"] = 0.75
|
||||
}
|
||||
|
||||
// ToolPriceSetting is managed by config.GlobalConfig.Register.
|
||||
@@ -61,11 +105,29 @@ var toolPriceSetting = ToolPriceSetting{
|
||||
Prices: make(map[string]float64),
|
||||
}
|
||||
|
||||
// builtInToolNames are the tool names (the part before ":") that
|
||||
// seedHardcodedToolPrices prices, built once from the constant seed and never
|
||||
// from operator configuration.
|
||||
var builtInToolNames = make(map[string]struct{})
|
||||
|
||||
func init() {
|
||||
config.GlobalConfig.Register("tool_price_setting", &toolPriceSetting)
|
||||
seed := make(map[string]float64)
|
||||
seedHardcodedToolPrices(seed)
|
||||
for key := range seed {
|
||||
name, _, _ := strings.Cut(key, ":")
|
||||
builtInToolNames[name] = struct{}{}
|
||||
}
|
||||
RebuildToolPriceIndex()
|
||||
}
|
||||
|
||||
// IsBuiltInToolPriceKey reports whether the built-in price seed prices the
|
||||
// tool name, as a default or for a model prefix. Operator prices never count.
|
||||
func IsBuiltInToolPriceKey(name string) bool {
|
||||
_, ok := builtInToolNames[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Precomputed price index (atomic, lock-free on read path)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -89,25 +151,25 @@ func isValidToolPrice(price float64) bool {
|
||||
func decodeToolPricesJSON(value string, ignoreInvalidEntries bool) (map[string]float64, error) {
|
||||
rawValue := json.RawMessage(strings.TrimSpace(value))
|
||||
if common.GetJsonType(rawValue) != "object" {
|
||||
return nil, fmt.Errorf("工具价格必须是 JSON 对象")
|
||||
return nil, common.NewMessage("Tool prices must be a JSON object")
|
||||
}
|
||||
|
||||
var rawPrices map[string]json.RawMessage
|
||||
if err := common.Unmarshal(rawValue, &rawPrices); err != nil {
|
||||
return nil, fmt.Errorf("解析工具价格失败: %w", err)
|
||||
return nil, common.NewMessage("Failed to parse tool prices: {{error}}", map[string]any{"error": err.Error()})
|
||||
}
|
||||
|
||||
prices := make(map[string]float64, len(rawPrices))
|
||||
for name, rawPrice := range rawPrices {
|
||||
var entryErr error
|
||||
if common.GetJsonType(rawPrice) != "number" {
|
||||
entryErr = fmt.Errorf("工具价格 %q 必须是非负数字", name)
|
||||
entryErr = common.NewMessage(`Tool price "{{name}}" must be a non-negative number`, map[string]any{"name": name})
|
||||
} else {
|
||||
var price float64
|
||||
if err := common.Unmarshal(rawPrice, &price); err != nil {
|
||||
entryErr = fmt.Errorf("解析工具价格 %q 失败: %w", name, err)
|
||||
entryErr = common.NewMessage(`Failed to parse tool price "{{name}}": {{error}}`, map[string]any{"name": name, "error": err.Error()})
|
||||
} else if !isValidToolPrice(price) {
|
||||
entryErr = fmt.Errorf("工具价格 %q 必须是有限的非负数字", name)
|
||||
entryErr = common.NewMessage(`Tool price "{{name}}" must be a finite non-negative number`, map[string]any{"name": name})
|
||||
} else {
|
||||
prices[name] = price
|
||||
}
|
||||
@@ -137,7 +199,7 @@ func ValidateToolPricesJSON(value string) error {
|
||||
func LoadToolPricesFromJSONString(value string) {
|
||||
prices, err := decodeToolPricesJSON(value, true)
|
||||
if err != nil {
|
||||
common.SysError("加载工具价格失败,将使用硬编码兜底: " + err.Error())
|
||||
common.SysError(common.LogText("failed to load tool prices, using hardcoded fallbacks: %s", err.Error()))
|
||||
prices = make(map[string]float64)
|
||||
}
|
||||
toolPriceSetting.Prices = prices
|
||||
|
||||
@@ -24,18 +24,46 @@ func TestToolPriceHardcodedFallbacksSurviveMissingOperatorConfig(t *testing.T) {
|
||||
toolPriceSetting.Prices = map[string]float64{}
|
||||
RebuildToolPriceIndex()
|
||||
|
||||
expectedDefaults := map[string]float64{
|
||||
"web_search": 10,
|
||||
"web_search_preview": 10,
|
||||
"file_search": 2.5,
|
||||
"google_search": 14,
|
||||
"image_generation": 150,
|
||||
tests := []struct {
|
||||
tool string
|
||||
model string
|
||||
want float64
|
||||
}{
|
||||
{"web_search", "", 10},
|
||||
{"web_search_preview", "", 10},
|
||||
{"file_search", "", 2.5},
|
||||
{"google_search", "", 14},
|
||||
{"image_generation", "", 150},
|
||||
{"web_search_preview", "gpt-4o-2024-11-20", 25},
|
||||
{"web_search_preview", "gpt-4.1-mini", 25},
|
||||
// Gemini 3 bills each search query; 2.5 and older bill the grounded prompt.
|
||||
{"google_search", "gemini-3.7-flash", 14},
|
||||
{"google_search_grounded_prompt", "gemini-3.7-flash", 0},
|
||||
{"google_search", "gemini-2.5-flash", 0},
|
||||
{"google_search_grounded_prompt", "gemini-2.5-flash", 35},
|
||||
// Vendor search tiers in the vendor's list currency.
|
||||
{"search_std", "", 10},
|
||||
{"search_pro", "", 30},
|
||||
{"search_pro_sogou", "", 50},
|
||||
{"search_pro_quark", "", 50},
|
||||
{"search_strategy_turbo", "", 3},
|
||||
{"search_strategy_max", "", 4},
|
||||
{"search_strategy_agent", "", 4},
|
||||
{"search_strategy_agent_max", "", 4},
|
||||
{"bing_web_search", "", 14},
|
||||
{"web_search", "grok-4", 5},
|
||||
{"x_search_posts", "", 5},
|
||||
{"x_search_profiles", "", 10},
|
||||
{"deepseek_search", "", 0.75},
|
||||
{"deepseek_fetch", "", 0.75},
|
||||
{"deepseek_search", "claude-sonnet-4-5", 0.75},
|
||||
}
|
||||
for name, expected := range expectedDefaults {
|
||||
assert.Equal(t, expected, GetToolPrice(name), name)
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.want, GetToolPriceForModel(tt.tool, tt.model), "%s for %q", tt.tool, tt.model)
|
||||
}
|
||||
assert.Equal(t, 25.0, GetToolPriceForModel("web_search_preview", "gpt-4o-2024-11-20"))
|
||||
assert.Equal(t, 25.0, GetToolPriceForModel("web_search_preview", "gpt-4.1-mini"))
|
||||
assert.True(t, IsBuiltInToolPriceKey("bing_web_search"))
|
||||
assert.True(t, IsBuiltInToolPriceKey("deepseek_fetch"))
|
||||
assert.True(t, IsBuiltInToolPriceKey("google_search_grounded_prompt"), "seeded only for model prefixes")
|
||||
}
|
||||
|
||||
func TestToolPriceOperatorOverridePrecedenceAndExplicitZero(t *testing.T) {
|
||||
@@ -77,6 +105,7 @@ func TestToolPriceCustomFunctionHasNoHardcodedFallback(t *testing.T) {
|
||||
toolPriceSetting.Prices["lookup_customer"] = 5
|
||||
RebuildToolPriceIndex()
|
||||
assert.Equal(t, 5.0, GetToolPrice("lookup_customer"))
|
||||
assert.False(t, IsBuiltInToolPriceKey("lookup_customer"), "an operator price is not a built-in key")
|
||||
|
||||
toolPriceSetting.Prices["lookup_customer"] = 0
|
||||
RebuildToolPriceIndex()
|
||||
|
||||
Reference in new issue
Block a user