初始化导入 new-api 源码
This commit is contained in:
120
setting/operation_setting/channel_affinity_setting.go
Normal file
120
setting/operation_setting/channel_affinity_setting.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
type ChannelAffinityKeySource struct {
|
||||
Type string `json:"type"` // context_int, context_string, gjson
|
||||
Key string `json:"key,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type ChannelAffinityRule struct {
|
||||
Name string `json:"name"`
|
||||
ModelRegex []string `json:"model_regex"`
|
||||
PathRegex []string `json:"path_regex"`
|
||||
UserAgentInclude []string `json:"user_agent_include,omitempty"`
|
||||
KeySources []ChannelAffinityKeySource `json:"key_sources"`
|
||||
|
||||
ValueRegex string `json:"value_regex"`
|
||||
TTLSeconds int `json:"ttl_seconds"`
|
||||
|
||||
ParamOverrideTemplate map[string]interface{} `json:"param_override_template,omitempty"`
|
||||
|
||||
SkipRetryOnFailure bool `json:"skip_retry_on_failure,omitempty"`
|
||||
|
||||
IncludeUsingGroup bool `json:"include_using_group"`
|
||||
IncludeRuleName bool `json:"include_rule_name"`
|
||||
}
|
||||
|
||||
type ChannelAffinitySetting struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
SwitchOnSuccess bool `json:"switch_on_success"`
|
||||
MaxEntries int `json:"max_entries"`
|
||||
DefaultTTLSeconds int `json:"default_ttl_seconds"`
|
||||
Rules []ChannelAffinityRule `json:"rules"`
|
||||
}
|
||||
|
||||
var codexCliPassThroughHeaders = []string{
|
||||
"Originator",
|
||||
"Session_id",
|
||||
"User-Agent",
|
||||
"X-Codex-Beta-Features",
|
||||
"X-Codex-Turn-Metadata",
|
||||
}
|
||||
|
||||
var claudeCliPassThroughHeaders = []string{
|
||||
"X-Stainless-Arch",
|
||||
"X-Stainless-Lang",
|
||||
"X-Stainless-Os",
|
||||
"X-Stainless-Package-Version",
|
||||
"X-Stainless-Retry-Count",
|
||||
"X-Stainless-Runtime",
|
||||
"X-Stainless-Runtime-Version",
|
||||
"X-Stainless-Timeout",
|
||||
"User-Agent",
|
||||
"X-App",
|
||||
"Anthropic-Beta",
|
||||
"Anthropic-Dangerous-Direct-Browser-Access",
|
||||
"Anthropic-Version",
|
||||
}
|
||||
|
||||
func buildPassHeaderTemplate(headers []string) map[string]interface{} {
|
||||
clonedHeaders := make([]string, 0, len(headers))
|
||||
clonedHeaders = append(clonedHeaders, headers...)
|
||||
return map[string]interface{}{
|
||||
"operations": []map[string]interface{}{
|
||||
{
|
||||
"mode": "pass_headers",
|
||||
"value": clonedHeaders,
|
||||
"keep_origin": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var channelAffinitySetting = ChannelAffinitySetting{
|
||||
Enabled: true,
|
||||
SwitchOnSuccess: true,
|
||||
MaxEntries: 100_000,
|
||||
DefaultTTLSeconds: 3600,
|
||||
Rules: []ChannelAffinityRule{
|
||||
{
|
||||
Name: "codex cli trace",
|
||||
ModelRegex: []string{"^gpt-.*$"},
|
||||
PathRegex: []string{"/v1/responses"},
|
||||
KeySources: []ChannelAffinityKeySource{
|
||||
{Type: "gjson", Path: "prompt_cache_key"},
|
||||
},
|
||||
ValueRegex: "",
|
||||
TTLSeconds: 0,
|
||||
ParamOverrideTemplate: buildPassHeaderTemplate(codexCliPassThroughHeaders),
|
||||
SkipRetryOnFailure: true,
|
||||
IncludeUsingGroup: true,
|
||||
IncludeRuleName: true,
|
||||
UserAgentInclude: nil,
|
||||
},
|
||||
{
|
||||
Name: "claude cli trace",
|
||||
ModelRegex: []string{"^claude-.*$"},
|
||||
PathRegex: []string{"/v1/messages"},
|
||||
KeySources: []ChannelAffinityKeySource{
|
||||
{Type: "gjson", Path: "metadata.user_id"},
|
||||
},
|
||||
ValueRegex: "",
|
||||
TTLSeconds: 0,
|
||||
ParamOverrideTemplate: buildPassHeaderTemplate(claudeCliPassThroughHeaders),
|
||||
SkipRetryOnFailure: true,
|
||||
IncludeUsingGroup: true,
|
||||
IncludeRuleName: true,
|
||||
UserAgentInclude: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
config.GlobalConfig.Register("channel_affinity_setting", &channelAffinitySetting)
|
||||
}
|
||||
|
||||
func GetChannelAffinitySetting() *ChannelAffinitySetting {
|
||||
return &channelAffinitySetting
|
||||
}
|
||||
37
setting/operation_setting/checkin_setting.go
Normal file
37
setting/operation_setting/checkin_setting.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
// CheckinSetting 签到功能配置
|
||||
type CheckinSetting struct {
|
||||
Enabled bool `json:"enabled"` // 是否启用签到功能
|
||||
MinQuota int `json:"min_quota"` // 签到最小额度奖励
|
||||
MaxQuota int `json:"max_quota"` // 签到最大额度奖励
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var checkinSetting = CheckinSetting{
|
||||
Enabled: false, // 默认关闭
|
||||
MinQuota: 1000, // 默认最小额度 1000 (约 0.002 USD)
|
||||
MaxQuota: 10000, // 默认最大额度 10000 (约 0.02 USD)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("checkin_setting", &checkinSetting)
|
||||
}
|
||||
|
||||
// GetCheckinSetting 获取签到配置
|
||||
func GetCheckinSetting() *CheckinSetting {
|
||||
return &checkinSetting
|
||||
}
|
||||
|
||||
// IsCheckinEnabled 是否启用签到功能
|
||||
func IsCheckinEnabled() bool {
|
||||
return checkinSetting.Enabled
|
||||
}
|
||||
|
||||
// GetCheckinQuotaRange 获取签到额度范围
|
||||
func GetCheckinQuotaRange() (min, max int) {
|
||||
return checkinSetting.MinQuota, checkinSetting.MaxQuota
|
||||
}
|
||||
91
setting/operation_setting/general_setting.go
Normal file
91
setting/operation_setting/general_setting.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
// 额度展示类型
|
||||
const (
|
||||
QuotaDisplayTypeUSD = "USD"
|
||||
QuotaDisplayTypeCNY = "CNY"
|
||||
QuotaDisplayTypeTokens = "TOKENS"
|
||||
QuotaDisplayTypeCustom = "CUSTOM"
|
||||
)
|
||||
|
||||
type GeneralSetting struct {
|
||||
DocsLink string `json:"docs_link"`
|
||||
PingIntervalEnabled bool `json:"ping_interval_enabled"`
|
||||
PingIntervalSeconds int `json:"ping_interval_seconds"`
|
||||
// 当前站点额度展示类型:USD / CNY / TOKENS
|
||||
QuotaDisplayType string `json:"quota_display_type"`
|
||||
// 自定义货币符号,用于 CUSTOM 展示类型
|
||||
CustomCurrencySymbol string `json:"custom_currency_symbol"`
|
||||
// 自定义货币与美元汇率(1 USD = X Custom)
|
||||
CustomCurrencyExchangeRate float64 `json:"custom_currency_exchange_rate"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var generalSetting = GeneralSetting{
|
||||
DocsLink: "https://docs.newapi.pro",
|
||||
PingIntervalEnabled: false,
|
||||
PingIntervalSeconds: 60,
|
||||
QuotaDisplayType: QuotaDisplayTypeUSD,
|
||||
CustomCurrencySymbol: "¤",
|
||||
CustomCurrencyExchangeRate: 1.0,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("general_setting", &generalSetting)
|
||||
}
|
||||
|
||||
func GetGeneralSetting() *GeneralSetting {
|
||||
return &generalSetting
|
||||
}
|
||||
|
||||
// IsCurrencyDisplay 是否以货币形式展示(美元或人民币)
|
||||
func IsCurrencyDisplay() bool {
|
||||
return generalSetting.QuotaDisplayType != QuotaDisplayTypeTokens
|
||||
}
|
||||
|
||||
// IsCNYDisplay 是否以人民币展示
|
||||
func IsCNYDisplay() bool {
|
||||
return generalSetting.QuotaDisplayType == QuotaDisplayTypeCNY
|
||||
}
|
||||
|
||||
// GetQuotaDisplayType 返回额度展示类型
|
||||
func GetQuotaDisplayType() string {
|
||||
return generalSetting.QuotaDisplayType
|
||||
}
|
||||
|
||||
// GetCurrencySymbol 返回当前展示类型对应符号
|
||||
func GetCurrencySymbol() string {
|
||||
switch generalSetting.QuotaDisplayType {
|
||||
case QuotaDisplayTypeUSD:
|
||||
return "$"
|
||||
case QuotaDisplayTypeCNY:
|
||||
return "¥"
|
||||
case QuotaDisplayTypeCustom:
|
||||
if generalSetting.CustomCurrencySymbol != "" {
|
||||
return generalSetting.CustomCurrencySymbol
|
||||
}
|
||||
return "¤"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// GetUsdToCurrencyRate 返回 1 USD = X <currency> 的 X(TOKENS 不适用)
|
||||
func GetUsdToCurrencyRate(usdToCny float64) float64 {
|
||||
switch generalSetting.QuotaDisplayType {
|
||||
case QuotaDisplayTypeUSD:
|
||||
return 1
|
||||
case QuotaDisplayTypeCNY:
|
||||
return usdToCny
|
||||
case QuotaDisplayTypeCustom:
|
||||
if generalSetting.CustomCurrencyExchangeRate > 0 {
|
||||
return generalSetting.CustomCurrencyExchangeRate
|
||||
}
|
||||
return 1
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
35
setting/operation_setting/monitor_setting.go
Normal file
35
setting/operation_setting/monitor_setting.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/QuantumNous/new-api/setting/config"
|
||||
)
|
||||
|
||||
type MonitorSetting struct {
|
||||
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
||||
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var monitorSetting = MonitorSetting{
|
||||
AutoTestChannelEnabled: false,
|
||||
AutoTestChannelMinutes: 10,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("monitor_setting", &monitorSetting)
|
||||
}
|
||||
|
||||
func GetMonitorSetting() *MonitorSetting {
|
||||
if os.Getenv("CHANNEL_TEST_FREQUENCY") != "" {
|
||||
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_TEST_FREQUENCY"))
|
||||
if err == nil && frequency > 0 {
|
||||
monitorSetting.AutoTestChannelEnabled = true
|
||||
monitorSetting.AutoTestChannelMinutes = float64(frequency)
|
||||
}
|
||||
}
|
||||
return &monitorSetting
|
||||
}
|
||||
32
setting/operation_setting/operation_setting.go
Normal file
32
setting/operation_setting/operation_setting.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package operation_setting
|
||||
|
||||
import "strings"
|
||||
|
||||
var DemoSiteEnabled = false
|
||||
var SelfUseModeEnabled = false
|
||||
|
||||
var AutomaticDisableKeywords = []string{
|
||||
"Your credit balance is too low",
|
||||
"This organization has been disabled.",
|
||||
"You exceeded your current quota",
|
||||
"Permission denied",
|
||||
"The security token included in the request is invalid",
|
||||
"Operation not allowed",
|
||||
"Your account is not authorized",
|
||||
}
|
||||
|
||||
func AutomaticDisableKeywordsToString() string {
|
||||
return strings.Join(AutomaticDisableKeywords, "\n")
|
||||
}
|
||||
|
||||
func AutomaticDisableKeywordsFromString(s string) {
|
||||
AutomaticDisableKeywords = []string{}
|
||||
ak := strings.Split(s, "\n")
|
||||
for _, k := range ak {
|
||||
k = strings.TrimSpace(k)
|
||||
k = strings.ToLower(k)
|
||||
if k != "" {
|
||||
AutomaticDisableKeywords = append(AutomaticDisableKeywords, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
setting/operation_setting/payment_setting.go
Normal file
23
setting/operation_setting/payment_setting.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
type PaymentSetting struct {
|
||||
AmountOptions []int `json:"amount_options"`
|
||||
AmountDiscount map[int]float64 `json:"amount_discount"` // 充值金额对应的折扣,例如 100 元 0.9 表示 100 元充值享受 9 折优惠
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var paymentSetting = PaymentSetting{
|
||||
AmountOptions: []int{10, 20, 50, 100, 200, 500},
|
||||
AmountDiscount: map[int]float64{},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("payment_setting", &paymentSetting)
|
||||
}
|
||||
|
||||
func GetPaymentSetting() *PaymentSetting {
|
||||
return &paymentSetting
|
||||
}
|
||||
59
setting/operation_setting/payment_setting_old.go
Normal file
59
setting/operation_setting/payment_setting_old.go
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
此文件为旧版支付设置文件,如需增加新的参数、变量等,请在 payment_setting.go 中添加
|
||||
This file is the old version of the payment settings file. If you need to add new parameters, variables, etc., please add them in payment_setting.go
|
||||
*/
|
||||
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
var PayAddress = ""
|
||||
var CustomCallbackAddress = ""
|
||||
var EpayId = ""
|
||||
var EpayKey = ""
|
||||
var Price = 7.3
|
||||
var MinTopUp = 1
|
||||
var USDExchangeRate = 7.3
|
||||
|
||||
var PayMethods = []map[string]string{
|
||||
{
|
||||
"name": "支付宝",
|
||||
"color": "rgba(var(--semi-blue-5), 1)",
|
||||
"type": "alipay",
|
||||
},
|
||||
{
|
||||
"name": "微信",
|
||||
"color": "rgba(var(--semi-green-5), 1)",
|
||||
"type": "wxpay",
|
||||
},
|
||||
{
|
||||
"name": "自定义1",
|
||||
"color": "black",
|
||||
"type": "custom1",
|
||||
"min_topup": "50",
|
||||
},
|
||||
}
|
||||
|
||||
func UpdatePayMethodsByJsonString(jsonString string) error {
|
||||
PayMethods = make([]map[string]string, 0)
|
||||
return common.Unmarshal([]byte(jsonString), &PayMethods)
|
||||
}
|
||||
|
||||
func PayMethods2JsonString() string {
|
||||
jsonBytes, err := common.Marshal(PayMethods)
|
||||
if err != nil {
|
||||
return "[]"
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func ContainsPayMethod(method string) bool {
|
||||
for _, payMethod := range PayMethods {
|
||||
if payMethod["type"] == method {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
21
setting/operation_setting/quota_setting.go
Normal file
21
setting/operation_setting/quota_setting.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
type QuotaSetting struct {
|
||||
EnableFreeModelPreConsume bool `json:"enable_free_model_pre_consume"` // 是否对免费模型启用预消耗
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var quotaSetting = QuotaSetting{
|
||||
EnableFreeModelPreConsume: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("quota_setting", "aSetting)
|
||||
}
|
||||
|
||||
func GetQuotaSetting() *QuotaSetting {
|
||||
return "aSetting
|
||||
}
|
||||
208
setting/operation_setting/status_code_ranges.go
Normal file
208
setting/operation_setting/status_code_ranges.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
)
|
||||
|
||||
type StatusCodeRange struct {
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
var AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}}
|
||||
|
||||
// Default behavior matches legacy hardcoded retry rules in controller/relay.go shouldRetry:
|
||||
// retry for 1xx, 3xx, 4xx(except 400/408), 5xx(except 504/524), and no retry for 2xx.
|
||||
var AutomaticRetryStatusCodeRanges = []StatusCodeRange{
|
||||
{Start: 100, End: 199},
|
||||
{Start: 300, End: 399},
|
||||
{Start: 401, End: 407},
|
||||
{Start: 409, End: 499},
|
||||
{Start: 500, End: 503},
|
||||
{Start: 505, End: 523},
|
||||
{Start: 525, End: 599},
|
||||
}
|
||||
|
||||
var alwaysSkipRetryStatusCodes = map[int]struct{}{
|
||||
504: {},
|
||||
524: {},
|
||||
}
|
||||
|
||||
var alwaysSkipRetryCodes = map[types.ErrorCode]struct{}{
|
||||
types.ErrorCodeBadResponseBody: {},
|
||||
}
|
||||
|
||||
func AutomaticDisableStatusCodesToString() string {
|
||||
return statusCodeRangesToString(AutomaticDisableStatusCodeRanges)
|
||||
}
|
||||
|
||||
func AutomaticDisableStatusCodesFromString(s string) error {
|
||||
ranges, err := ParseHTTPStatusCodeRanges(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AutomaticDisableStatusCodeRanges = ranges
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShouldDisableByStatusCode(code int) bool {
|
||||
return shouldMatchStatusCodeRanges(AutomaticDisableStatusCodeRanges, code)
|
||||
}
|
||||
|
||||
func AutomaticRetryStatusCodesToString() string {
|
||||
return statusCodeRangesToString(AutomaticRetryStatusCodeRanges)
|
||||
}
|
||||
|
||||
func AutomaticRetryStatusCodesFromString(s string) error {
|
||||
ranges, err := ParseHTTPStatusCodeRanges(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AutomaticRetryStatusCodeRanges = ranges
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsAlwaysSkipRetryStatusCode(code int) bool {
|
||||
_, exists := alwaysSkipRetryStatusCodes[code]
|
||||
return exists
|
||||
}
|
||||
|
||||
func IsAlwaysSkipRetryCode(errorCode types.ErrorCode) bool {
|
||||
_, exists := alwaysSkipRetryCodes[errorCode]
|
||||
return exists
|
||||
}
|
||||
|
||||
func ShouldRetryByStatusCode(code int) bool {
|
||||
if IsAlwaysSkipRetryStatusCode(code) {
|
||||
return false
|
||||
}
|
||||
return shouldMatchStatusCodeRanges(AutomaticRetryStatusCodeRanges, code)
|
||||
}
|
||||
|
||||
func statusCodeRangesToString(ranges []StatusCodeRange) string {
|
||||
if len(ranges) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(ranges))
|
||||
for _, r := range ranges {
|
||||
if r.Start == r.End {
|
||||
parts = append(parts, strconv.Itoa(r.Start))
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%d-%d", r.Start, r.End))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func shouldMatchStatusCodeRanges(ranges []StatusCodeRange, code int) bool {
|
||||
if code < 100 || code > 599 {
|
||||
return false
|
||||
}
|
||||
for _, r := range ranges {
|
||||
if code < r.Start {
|
||||
return false
|
||||
}
|
||||
if code <= r.End {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ParseHTTPStatusCodeRanges(input string) ([]StatusCodeRange, error) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
input = strings.NewReplacer(",", ",").Replace(input)
|
||||
segments := strings.Split(input, ",")
|
||||
|
||||
var ranges []StatusCodeRange
|
||||
var invalid []string
|
||||
|
||||
for _, seg := range segments {
|
||||
seg = strings.TrimSpace(seg)
|
||||
if seg == "" {
|
||||
continue
|
||||
}
|
||||
r, err := parseHTTPStatusCodeToken(seg)
|
||||
if err != nil {
|
||||
invalid = append(invalid, seg)
|
||||
continue
|
||||
}
|
||||
ranges = append(ranges, r)
|
||||
}
|
||||
|
||||
if len(invalid) > 0 {
|
||||
return nil, fmt.Errorf("invalid http status code rules: %s", strings.Join(invalid, ", "))
|
||||
}
|
||||
if len(ranges) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sort.Slice(ranges, func(i, j int) bool {
|
||||
if ranges[i].Start == ranges[j].Start {
|
||||
return ranges[i].End < ranges[j].End
|
||||
}
|
||||
return ranges[i].Start < ranges[j].Start
|
||||
})
|
||||
|
||||
merged := []StatusCodeRange{ranges[0]}
|
||||
for _, r := range ranges[1:] {
|
||||
last := &merged[len(merged)-1]
|
||||
if r.Start <= last.End+1 {
|
||||
if r.End > last.End {
|
||||
last.End = r.End
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged = append(merged, r)
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
func parseHTTPStatusCodeToken(token string) (StatusCodeRange, error) {
|
||||
token = strings.TrimSpace(token)
|
||||
token = strings.ReplaceAll(token, " ", "")
|
||||
if token == "" {
|
||||
return StatusCodeRange{}, fmt.Errorf("empty token")
|
||||
}
|
||||
|
||||
if strings.Contains(token, "-") {
|
||||
parts := strings.Split(token, "-")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return StatusCodeRange{}, fmt.Errorf("invalid range token: %s", token)
|
||||
}
|
||||
start, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return StatusCodeRange{}, fmt.Errorf("invalid range start: %s", token)
|
||||
}
|
||||
end, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return StatusCodeRange{}, fmt.Errorf("invalid range end: %s", token)
|
||||
}
|
||||
if start > end {
|
||||
return StatusCodeRange{}, fmt.Errorf("range start > end: %s", token)
|
||||
}
|
||||
if start < 100 || end > 599 {
|
||||
return StatusCodeRange{}, fmt.Errorf("range out of bounds: %s", token)
|
||||
}
|
||||
return StatusCodeRange{Start: start, End: end}, nil
|
||||
}
|
||||
|
||||
code, err := strconv.Atoi(token)
|
||||
if err != nil {
|
||||
return StatusCodeRange{}, fmt.Errorf("invalid status code: %s", token)
|
||||
}
|
||||
if code < 100 || code > 599 {
|
||||
return StatusCodeRange{}, fmt.Errorf("status code out of bounds: %s", token)
|
||||
}
|
||||
return StatusCodeRange{Start: code, End: code}, nil
|
||||
}
|
||||
87
setting/operation_setting/status_code_ranges_test.go
Normal file
87
setting/operation_setting/status_code_ranges_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseHTTPStatusCodeRanges_CommaSeparated(t *testing.T) {
|
||||
ranges, err := ParseHTTPStatusCodeRanges("401,403,500-599")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []StatusCodeRange{
|
||||
{Start: 401, End: 401},
|
||||
{Start: 403, End: 403},
|
||||
{Start: 500, End: 599},
|
||||
}, ranges)
|
||||
}
|
||||
|
||||
func TestParseHTTPStatusCodeRanges_MergeAndNormalize(t *testing.T) {
|
||||
ranges, err := ParseHTTPStatusCodeRanges("500-505,504,401,403,402")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []StatusCodeRange{
|
||||
{Start: 401, End: 403},
|
||||
{Start: 500, End: 505},
|
||||
}, ranges)
|
||||
}
|
||||
|
||||
func TestParseHTTPStatusCodeRanges_Invalid(t *testing.T) {
|
||||
_, err := ParseHTTPStatusCodeRanges("99,600,foo,500-400,500-")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseHTTPStatusCodeRanges_NoComma_IsInvalid(t *testing.T) {
|
||||
_, err := ParseHTTPStatusCodeRanges("401 403")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestShouldDisableByStatusCode(t *testing.T) {
|
||||
orig := AutomaticDisableStatusCodeRanges
|
||||
t.Cleanup(func() { AutomaticDisableStatusCodeRanges = orig })
|
||||
|
||||
AutomaticDisableStatusCodeRanges = []StatusCodeRange{
|
||||
{Start: 401, End: 403},
|
||||
{Start: 500, End: 599},
|
||||
}
|
||||
|
||||
require.True(t, ShouldDisableByStatusCode(401))
|
||||
require.True(t, ShouldDisableByStatusCode(403))
|
||||
require.False(t, ShouldDisableByStatusCode(404))
|
||||
require.True(t, ShouldDisableByStatusCode(500))
|
||||
require.False(t, ShouldDisableByStatusCode(200))
|
||||
}
|
||||
|
||||
func TestShouldRetryByStatusCode(t *testing.T) {
|
||||
orig := AutomaticRetryStatusCodeRanges
|
||||
t.Cleanup(func() { AutomaticRetryStatusCodeRanges = orig })
|
||||
|
||||
AutomaticRetryStatusCodeRanges = []StatusCodeRange{
|
||||
{Start: 429, End: 429},
|
||||
{Start: 500, End: 599},
|
||||
}
|
||||
|
||||
require.True(t, ShouldRetryByStatusCode(429))
|
||||
require.True(t, ShouldRetryByStatusCode(500))
|
||||
require.False(t, ShouldRetryByStatusCode(504))
|
||||
require.False(t, ShouldRetryByStatusCode(524))
|
||||
require.False(t, ShouldRetryByStatusCode(400))
|
||||
require.False(t, ShouldRetryByStatusCode(200))
|
||||
}
|
||||
|
||||
func TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior(t *testing.T) {
|
||||
require.False(t, ShouldRetryByStatusCode(200))
|
||||
require.False(t, ShouldRetryByStatusCode(400))
|
||||
require.True(t, ShouldRetryByStatusCode(401))
|
||||
require.False(t, ShouldRetryByStatusCode(408))
|
||||
require.True(t, ShouldRetryByStatusCode(429))
|
||||
require.True(t, ShouldRetryByStatusCode(500))
|
||||
require.False(t, ShouldRetryByStatusCode(504))
|
||||
require.False(t, ShouldRetryByStatusCode(524))
|
||||
require.True(t, ShouldRetryByStatusCode(599))
|
||||
}
|
||||
|
||||
func TestIsAlwaysSkipRetryStatusCode(t *testing.T) {
|
||||
require.True(t, IsAlwaysSkipRetryStatusCode(504))
|
||||
require.True(t, IsAlwaysSkipRetryStatusCode(524))
|
||||
require.False(t, IsAlwaysSkipRetryStatusCode(500))
|
||||
}
|
||||
28
setting/operation_setting/token_setting.go
Normal file
28
setting/operation_setting/token_setting.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package operation_setting
|
||||
|
||||
import "github.com/QuantumNous/new-api/setting/config"
|
||||
|
||||
// TokenSetting 令牌相关配置
|
||||
type TokenSetting struct {
|
||||
MaxUserTokens int `json:"max_user_tokens"` // 每用户最大令牌数量
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var tokenSetting = TokenSetting{
|
||||
MaxUserTokens: 1000, // 默认每用户最多 1000 个令牌
|
||||
}
|
||||
|
||||
func init() {
|
||||
// 注册到全局配置管理器
|
||||
config.GlobalConfig.Register("token_setting", &tokenSetting)
|
||||
}
|
||||
|
||||
// GetTokenSetting 获取令牌配置
|
||||
func GetTokenSetting() *TokenSetting {
|
||||
return &tokenSetting
|
||||
}
|
||||
|
||||
// GetMaxUserTokens 获取每用户最大令牌数量
|
||||
func GetMaxUserTokens() int {
|
||||
return GetTokenSetting().MaxUserTokens
|
||||
}
|
||||
110
setting/operation_setting/tools.go
Normal file
110
setting/operation_setting/tools.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package operation_setting
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
// Web search
|
||||
WebSearchPriceHigh = 25.00
|
||||
WebSearchPrice = 10.00
|
||||
// File search
|
||||
FileSearchPrice = 2.5
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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": {
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user