feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+45
View File
@@ -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
}
+36 -2
View File
@@ -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 &quotaSetting
}
// 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
}
+69 -7
View File
@@ -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
+39 -10
View File
@@ -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()