初始化导入 new-api 源码

This commit is contained in:
OpenClaw Task Bot
2026-04-09 22:31:14 +08:00
commit 75bda2e845
994 changed files with 250292 additions and 0 deletions

37
setting/auto_group.go Normal file
View File

@@ -0,0 +1,37 @@
package setting
import (
"github.com/QuantumNous/new-api/common"
)
var autoGroups = []string{
"default",
}
var DefaultUseAutoGroup = false
func ContainsAutoGroup(group string) bool {
for _, autoGroup := range autoGroups {
if autoGroup == group {
return true
}
}
return false
}
func UpdateAutoGroupsByJsonString(jsonString string) error {
autoGroups = make([]string, 0)
return common.Unmarshal([]byte(jsonString), &autoGroups)
}
func AutoGroups2JsonString() string {
jsonBytes, err := common.Marshal(autoGroups)
if err != nil {
return "[]"
}
return string(jsonBytes)
}
func GetAutoGroups() []string {
return autoGroups
}

51
setting/chat.go Normal file
View File

@@ -0,0 +1,51 @@
package setting
import (
"encoding/json"
"github.com/QuantumNous/new-api/common"
)
var Chats = []map[string]string{
//{
// "ChatGPT Next Web 官方示例": "https://app.nextchat.dev/#/?settings={\"key\":\"{key}\",\"url\":\"{address}\"}",
//},
{
"Cherry Studio": "cherrystudio://providers/api-keys?v=1&data={cherryConfig}",
},
{
"AionUI": "aionui://provider/add?v=1&data={aionuiConfig}",
},
{
"流畅阅读": "fluentread",
},
{
"CC Switch": "ccswitch",
},
{
"Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}",
},
{
"AI as Workspace": "https://aiaw.app/set-provider?provider={\"type\":\"openai\",\"settings\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\",\"compatibility\":\"strict\"}}",
},
{
"AMA 问天": "ama://set-api-key?server={address}&key={key}",
},
{
"OpenCat": "opencat://team/join?domain={address}&token={key}",
},
}
func UpdateChatsByJsonString(jsonString string) error {
Chats = make([]map[string]string, 0)
return json.Unmarshal([]byte(jsonString), &Chats)
}
func Chats2JsonString() string {
jsonBytes, err := json.Marshal(Chats)
if err != nil {
common.SysLog("error marshalling chats: " + err.Error())
return "[]"
}
return string(jsonBytes)
}

297
setting/config/config.go Normal file
View File

@@ -0,0 +1,297 @@
package config
import (
"encoding/json"
"reflect"
"strconv"
"strings"
"sync"
"github.com/QuantumNous/new-api/common"
)
// ConfigManager 统一管理所有配置
type ConfigManager struct {
configs map[string]interface{}
mutex sync.RWMutex
}
var GlobalConfig = NewConfigManager()
func NewConfigManager() *ConfigManager {
return &ConfigManager{
configs: make(map[string]interface{}),
}
}
// Register 注册一个配置模块
func (cm *ConfigManager) Register(name string, config interface{}) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cm.configs[name] = config
}
// Get 获取指定配置模块
func (cm *ConfigManager) Get(name string) interface{} {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
return cm.configs[name]
}
// LoadFromDB 从数据库加载配置
func (cm *ConfigManager) LoadFromDB(options map[string]string) error {
cm.mutex.Lock()
defer cm.mutex.Unlock()
for name, config := range cm.configs {
prefix := name + "."
configMap := make(map[string]string)
// 收集属于此配置的所有选项
for key, value := range options {
if strings.HasPrefix(key, prefix) {
configKey := strings.TrimPrefix(key, prefix)
configMap[configKey] = value
}
}
// 如果找到配置项,则更新配置
if len(configMap) > 0 {
if err := updateConfigFromMap(config, configMap); err != nil {
common.SysError("failed to update config " + name + ": " + err.Error())
continue
}
}
}
return nil
}
// SaveToDB 将配置保存到数据库
func (cm *ConfigManager) SaveToDB(updateFunc func(key, value string) error) error {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
for name, config := range cm.configs {
configMap, err := configToMap(config)
if err != nil {
return err
}
for key, value := range configMap {
dbKey := name + "." + key
if err := updateFunc(dbKey, value); err != nil {
return err
}
}
}
return nil
}
// 辅助函数将配置对象转换为map
func configToMap(config interface{}) (map[string]string, error) {
result := make(map[string]string)
val := reflect.ValueOf(config)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil, nil
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// 跳过未导出字段
if !fieldType.IsExported() {
continue
}
// 获取json标签作为键名
key := fieldType.Tag.Get("json")
if key == "" || key == "-" {
key = fieldType.Name
}
// 处理不同类型的字段
var strValue string
switch field.Kind() {
case reflect.String:
strValue = field.String()
case reflect.Bool:
strValue = strconv.FormatBool(field.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
strValue = strconv.FormatInt(field.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
strValue = strconv.FormatUint(field.Uint(), 10)
case reflect.Float32, reflect.Float64:
strValue = strconv.FormatFloat(field.Float(), 'f', -1, 64)
case reflect.Ptr:
// 处理指针类型:如果非 nil序列化指向的值
if !field.IsNil() {
bytes, err := json.Marshal(field.Interface())
if err != nil {
return nil, err
}
strValue = string(bytes)
} else {
// nil 指针序列化为 "null"
strValue = "null"
}
case reflect.Map, reflect.Slice, reflect.Struct:
// 复杂类型使用JSON序列化
bytes, err := json.Marshal(field.Interface())
if err != nil {
return nil, err
}
strValue = string(bytes)
default:
// 跳过不支持的类型
continue
}
result[key] = strValue
}
return result, nil
}
// 辅助函数从map更新配置对象
func updateConfigFromMap(config interface{}, configMap map[string]string) error {
val := reflect.ValueOf(config)
if val.Kind() != reflect.Ptr {
return nil
}
val = val.Elem()
if val.Kind() != reflect.Struct {
return nil
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// 跳过未导出字段
if !fieldType.IsExported() {
continue
}
// 获取json标签作为键名
key := fieldType.Tag.Get("json")
if key == "" || key == "-" {
key = fieldType.Name
}
// 检查map中是否有对应的值
strValue, ok := configMap[key]
if !ok {
continue
}
// 根据字段类型设置值
if !field.CanSet() {
continue
}
switch field.Kind() {
case reflect.String:
field.SetString(strValue)
case reflect.Bool:
boolValue, err := strconv.ParseBool(strValue)
if err != nil {
continue
}
field.SetBool(boolValue)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intValue, err := strconv.ParseInt(strValue, 10, 64)
if err != nil {
// 兼容 float 格式的字符串(如 "2.000000"
floatValue, fErr := strconv.ParseFloat(strValue, 64)
if fErr != nil {
continue
}
intValue = int64(floatValue)
}
field.SetInt(intValue)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintValue, err := strconv.ParseUint(strValue, 10, 64)
if err != nil {
// 兼容 float 格式的字符串
floatValue, fErr := strconv.ParseFloat(strValue, 64)
if fErr != nil || floatValue < 0 {
continue
}
uintValue = uint64(floatValue)
}
field.SetUint(uintValue)
case reflect.Float32, reflect.Float64:
floatValue, err := strconv.ParseFloat(strValue, 64)
if err != nil {
continue
}
field.SetFloat(floatValue)
case reflect.Ptr:
// 处理指针类型
if strValue == "null" {
field.Set(reflect.Zero(field.Type()))
} else {
// 如果指针是 nil需要先初始化
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
// 反序列化到指针指向的值
err := json.Unmarshal([]byte(strValue), field.Interface())
if err != nil {
continue
}
}
case reflect.Map, reflect.Slice, reflect.Struct:
// 复杂类型使用JSON反序列化
err := json.Unmarshal([]byte(strValue), field.Addr().Interface())
if err != nil {
continue
}
}
}
return nil
}
// ConfigToMap 将配置对象转换为map导出函数
func ConfigToMap(config interface{}) (map[string]string, error) {
return configToMap(config)
}
// UpdateConfigFromMap 从map更新配置对象导出函数
func UpdateConfigFromMap(config interface{}, configMap map[string]string) error {
return updateConfigFromMap(config, configMap)
}
// ExportAllConfigs 导出所有已注册的配置为扁平结构
func (cm *ConfigManager) ExportAllConfigs() map[string]string {
cm.mutex.RLock()
defer cm.mutex.RUnlock()
result := make(map[string]string)
for name, cfg := range cm.configs {
configMap, err := ConfigToMap(cfg)
if err != nil {
continue
}
// 使用 "模块名.配置项" 的格式添加到结果中
for key, value := range configMap {
result[name+"."+key] = value
}
}
return result
}

View File

@@ -0,0 +1,39 @@
package console_setting
import "github.com/QuantumNous/new-api/setting/config"
type ConsoleSetting struct {
ApiInfo string `json:"api_info"` // 控制台 API 信息 (JSON 数组字符串)
UptimeKumaGroups string `json:"uptime_kuma_groups"` // Uptime Kuma 分组配置 (JSON 数组字符串)
Announcements string `json:"announcements"` // 系统公告 (JSON 数组字符串)
FAQ string `json:"faq"` // 常见问题 (JSON 数组字符串)
ApiInfoEnabled bool `json:"api_info_enabled"` // 是否启用 API 信息面板
UptimeKumaEnabled bool `json:"uptime_kuma_enabled"` // 是否启用 Uptime Kuma 面板
AnnouncementsEnabled bool `json:"announcements_enabled"` // 是否启用系统公告面板
FAQEnabled bool `json:"faq_enabled"` // 是否启用常见问答面板
}
// 默认配置
var defaultConsoleSetting = ConsoleSetting{
ApiInfo: "",
UptimeKumaGroups: "",
Announcements: "",
FAQ: "",
ApiInfoEnabled: true,
UptimeKumaEnabled: true,
AnnouncementsEnabled: true,
FAQEnabled: true,
}
// 全局实例
var consoleSetting = defaultConsoleSetting
func init() {
// 注册到全局配置管理器,键名为 console_setting
config.GlobalConfig.Register("console_setting", &consoleSetting)
}
// GetConsoleSetting 获取 ConsoleSetting 配置实例
func GetConsoleSetting() *ConsoleSetting {
return &consoleSetting
}

View File

@@ -0,0 +1,304 @@
package console_setting
import (
"encoding/json"
"fmt"
"net/url"
"regexp"
"sort"
"strings"
"time"
)
var (
urlRegex = regexp.MustCompile(`^https?://(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?:\:[0-9]{1,5})?(?:/.*)?$`)
dangerousChars = []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
validColors = map[string]bool{
"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
"light-green": true, "teal": true, "light-blue": true, "indigo": true,
"violet": true, "grey": true,
}
slugRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
)
func parseJSONArray(jsonStr string, typeName string) ([]map[string]interface{}, error) {
var list []map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &list); err != nil {
return nil, fmt.Errorf("%s格式错误%s", typeName, err.Error())
}
return list, nil
}
func validateURL(urlStr string, index int, itemType string) error {
if !urlRegex.MatchString(urlStr) {
return fmt.Errorf("第%d个%s的URL格式不正确", index, itemType)
}
if _, err := url.Parse(urlStr); err != nil {
return fmt.Errorf("第%d个%s的URL无法解析%s", index, itemType, err.Error())
}
return nil
}
func checkDangerousContent(content string, index int, itemType string) error {
lower := strings.ToLower(content)
for _, d := range dangerousChars {
if strings.Contains(lower, d) {
return fmt.Errorf("第%d个%s包含不允许的内容", index, itemType)
}
}
return nil
}
func getJSONList(jsonStr string) []map[string]interface{} {
if jsonStr == "" {
return []map[string]interface{}{}
}
var list []map[string]interface{}
json.Unmarshal([]byte(jsonStr), &list)
return list
}
func ValidateConsoleSettings(settingsStr string, settingType string) error {
if settingsStr == "" {
return nil
}
switch settingType {
case "ApiInfo":
return validateApiInfo(settingsStr)
case "Announcements":
return validateAnnouncements(settingsStr)
case "FAQ":
return validateFAQ(settingsStr)
case "UptimeKumaGroups":
return validateUptimeKumaGroups(settingsStr)
default:
return fmt.Errorf("未知的设置类型:%s", settingType)
}
}
func validateApiInfo(apiInfoStr string) error {
apiInfoList, err := parseJSONArray(apiInfoStr, "API信息")
if err != nil {
return err
}
if len(apiInfoList) > 50 {
return fmt.Errorf("API信息数量不能超过50个")
}
for i, apiInfo := range apiInfoList {
urlStr, ok := apiInfo["url"].(string)
if !ok || urlStr == "" {
return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
}
route, ok := apiInfo["route"].(string)
if !ok || route == "" {
return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
}
description, ok := apiInfo["description"].(string)
if !ok || description == "" {
return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
}
color, ok := apiInfo["color"].(string)
if !ok || color == "" {
return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
}
if err := validateURL(urlStr, i+1, "API信息"); err != nil {
return err
}
if len(urlStr) > 500 {
return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
}
if len(route) > 100 {
return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
}
if len(description) > 200 {
return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
}
if !validColors[color] {
return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
}
if err := checkDangerousContent(description, i+1, "API信息"); err != nil {
return err
}
if err := checkDangerousContent(route, i+1, "API信息"); err != nil {
return err
}
}
return nil
}
func GetApiInfo() []map[string]interface{} {
return getJSONList(GetConsoleSetting().ApiInfo)
}
func validateAnnouncements(announcementsStr string) error {
list, err := parseJSONArray(announcementsStr, "系统公告")
if err != nil {
return err
}
if len(list) > 100 {
return fmt.Errorf("系统公告数量不能超过100个")
}
validTypes := map[string]bool{
"default": true, "ongoing": true, "success": true, "warning": true, "error": true,
}
for i, ann := range list {
content, ok := ann["content"].(string)
if !ok || content == "" {
return fmt.Errorf("第%d个公告缺少内容字段", i+1)
}
publishDateAny, exists := ann["publishDate"]
if !exists {
return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
}
publishDateStr, ok := publishDateAny.(string)
if !ok || publishDateStr == "" {
return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
}
if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
}
if t, exists := ann["type"]; exists {
if typeStr, ok := t.(string); ok {
if !validTypes[typeStr] {
return fmt.Errorf("第%d个公告的类型值不合法", i+1)
}
}
}
if len(content) > 500 {
return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
}
if extra, exists := ann["extra"]; exists {
if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
}
}
}
return nil
}
func validateFAQ(faqStr string) error {
list, err := parseJSONArray(faqStr, "FAQ信息")
if err != nil {
return err
}
if len(list) > 100 {
return fmt.Errorf("FAQ数量不能超过100个")
}
for i, faq := range list {
question, ok := faq["question"].(string)
if !ok || question == "" {
return fmt.Errorf("第%d个FAQ缺少问题字段", i+1)
}
answer, ok := faq["answer"].(string)
if !ok || answer == "" {
return fmt.Errorf("第%d个FAQ缺少答案字段", i+1)
}
if len(question) > 200 {
return fmt.Errorf("第%d个FAQ的问题长度不能超过200字符", i+1)
}
if len(answer) > 1000 {
return fmt.Errorf("第%d个FAQ的答案长度不能超过1000字符", i+1)
}
}
return nil
}
func getPublishTime(item map[string]interface{}) time.Time {
if v, ok := item["publishDate"]; ok {
if s, ok2 := v.(string); ok2 {
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t
}
}
}
return time.Time{}
}
func GetAnnouncements() []map[string]interface{} {
list := getJSONList(GetConsoleSetting().Announcements)
sort.SliceStable(list, func(i, j int) bool {
return getPublishTime(list[i]).After(getPublishTime(list[j]))
})
return list
}
func GetFAQ() []map[string]interface{} {
return getJSONList(GetConsoleSetting().FAQ)
}
func validateUptimeKumaGroups(groupsStr string) error {
groups, err := parseJSONArray(groupsStr, "Uptime Kuma分组配置")
if err != nil {
return err
}
if len(groups) > 20 {
return fmt.Errorf("Uptime Kuma分组数量不能超过20个")
}
nameSet := make(map[string]bool)
for i, group := range groups {
categoryName, ok := group["categoryName"].(string)
if !ok || categoryName == "" {
return fmt.Errorf("第%d个分组缺少分类名称字段", i+1)
}
if nameSet[categoryName] {
return fmt.Errorf("第%d个分组的分类名称与其他分组重复", i+1)
}
nameSet[categoryName] = true
urlStr, ok := group["url"].(string)
if !ok || urlStr == "" {
return fmt.Errorf("第%d个分组缺少URL字段", i+1)
}
slug, ok := group["slug"].(string)
if !ok || slug == "" {
return fmt.Errorf("第%d个分组缺少Slug字段", i+1)
}
description, ok := group["description"].(string)
if !ok {
description = ""
}
if err := validateURL(urlStr, i+1, "分组"); err != nil {
return err
}
if len(categoryName) > 50 {
return fmt.Errorf("第%d个分组的分类名称长度不能超过50字符", i+1)
}
if len(urlStr) > 500 {
return fmt.Errorf("第%d个分组的URL长度不能超过500字符", i+1)
}
if len(slug) > 100 {
return fmt.Errorf("第%d个分组的Slug长度不能超过100字符", i+1)
}
if len(description) > 200 {
return fmt.Errorf("第%d个分组的描述长度不能超过200字符", i+1)
}
if !slugRegex.MatchString(slug) {
return fmt.Errorf("第%d个分组的Slug只能包含字母、数字、下划线和连字符", i+1)
}
if err := checkDangerousContent(description, i+1, "分组"); err != nil {
return err
}
if err := checkDangerousContent(categoryName, i+1, "分组"); err != nil {
return err
}
}
return nil
}
func GetUptimeKumaGroups() []map[string]interface{} {
return getJSONList(GetConsoleSetting().UptimeKumaGroups)
}

7
setting/midjourney.go Normal file
View File

@@ -0,0 +1,7 @@
package setting
var MjNotifyEnabled = false
var MjAccountFilterEnabled = false
var MjModeClearEnabled = false
var MjForwardUrlEnabled = true
var MjActionCheckSuccessEnabled = true

View File

@@ -0,0 +1,89 @@
package model_setting
import (
"net/http"
"strings"
"github.com/QuantumNous/new-api/setting/config"
)
//var claudeHeadersSettings = map[string][]string{}
//
//var ClaudeThinkingAdapterEnabled = true
//var ClaudeThinkingAdapterMaxTokens = 8192
//var ClaudeThinkingAdapterBudgetTokensPercentage = 0.8
// ClaudeSettings 定义Claude模型的配置
type ClaudeSettings struct {
HeadersSettings map[string]map[string][]string `json:"model_headers_settings"`
DefaultMaxTokens map[string]int `json:"default_max_tokens"`
ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"`
ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"`
}
// 默认配置
var defaultClaudeSettings = ClaudeSettings{
HeadersSettings: map[string]map[string][]string{},
ThinkingAdapterEnabled: true,
DefaultMaxTokens: map[string]int{
"default": 8192,
},
ThinkingAdapterBudgetTokensPercentage: 0.8,
}
// 全局实例
var claudeSettings = defaultClaudeSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("claude", &claudeSettings)
}
// GetClaudeSettings 获取Claude配置
func GetClaudeSettings() *ClaudeSettings {
// check default max tokens must have default key
if _, ok := claudeSettings.DefaultMaxTokens["default"]; !ok {
claudeSettings.DefaultMaxTokens["default"] = 8192
}
return &claudeSettings
}
func (c *ClaudeSettings) WriteHeaders(originModel string, httpHeader *http.Header) {
if headers, ok := c.HeadersSettings[originModel]; ok {
for headerKey, headerValues := range headers {
mergedValues := normalizeHeaderListValues(
append(append([]string(nil), httpHeader.Values(headerKey)...), headerValues...),
)
if len(mergedValues) == 0 {
continue
}
httpHeader.Set(headerKey, strings.Join(mergedValues, ","))
}
}
}
func normalizeHeaderListValues(values []string) []string {
normalizedValues := make([]string, 0, len(values))
seenValues := make(map[string]struct{}, len(values))
for _, value := range values {
for _, item := range strings.Split(value, ",") {
normalizedItem := strings.TrimSpace(item)
if normalizedItem == "" {
continue
}
if _, exists := seenValues[normalizedItem]; exists {
continue
}
seenValues[normalizedItem] = struct{}{}
normalizedValues = append(normalizedValues, normalizedItem)
}
}
return normalizedValues
}
func (c *ClaudeSettings) GetDefaultMaxTokens(model string) int {
if maxTokens, ok := c.DefaultMaxTokens[model]; ok {
return maxTokens
}
return c.DefaultMaxTokens["default"]
}

View File

@@ -0,0 +1,76 @@
package model_setting
import (
"github.com/QuantumNous/new-api/setting/config"
)
// GeminiSettings defines Gemini model configuration. 注意bool要以enabled结尾才可以生效编辑
type GeminiSettings struct {
SafetySettings map[string]string `json:"safety_settings"`
VersionSettings map[string]string `json:"version_settings"`
SupportedImagineModels []string `json:"supported_imagine_models"`
ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"`
ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"`
FunctionCallThoughtSignatureEnabled bool `json:"function_call_thought_signature_enabled"`
RemoveFunctionResponseIdEnabled bool `json:"remove_function_response_id_enabled"`
}
// 默认配置
var defaultGeminiSettings = GeminiSettings{
SafetySettings: map[string]string{
"default": "OFF",
},
VersionSettings: map[string]string{
"default": "v1beta",
"gemini-1.0-pro": "v1",
},
SupportedImagineModels: []string{
"gemini-2.0-flash-exp-image-generation",
"gemini-2.0-flash-exp",
"gemini-3-pro-image-preview",
"gemini-2.5-flash-image",
"gemini-3.1-flash-image-preview",
},
ThinkingAdapterEnabled: false,
ThinkingAdapterBudgetTokensPercentage: 0.6,
FunctionCallThoughtSignatureEnabled: true,
RemoveFunctionResponseIdEnabled: true,
}
// 全局实例
var geminiSettings = defaultGeminiSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("gemini", &geminiSettings)
}
// GetGeminiSettings 获取Gemini配置
func GetGeminiSettings() *GeminiSettings {
return &geminiSettings
}
// GetGeminiSafetySetting 获取安全设置
func GetGeminiSafetySetting(key string) string {
if value, ok := geminiSettings.SafetySettings[key]; ok {
return value
}
return geminiSettings.SafetySettings["default"]
}
// GetGeminiVersionSetting 获取版本设置
func GetGeminiVersionSetting(key string) string {
if value, ok := geminiSettings.VersionSettings[key]; ok {
return value
}
return geminiSettings.VersionSettings["default"]
}
func IsGeminiModelSupportImagine(model string) bool {
for _, v := range geminiSettings.SupportedImagineModels {
if v == model {
return true
}
}
return false
}

View File

@@ -0,0 +1,79 @@
package model_setting
import (
"slices"
"strings"
"github.com/QuantumNous/new-api/setting/config"
)
type ChatCompletionsToResponsesPolicy struct {
Enabled bool `json:"enabled"`
AllChannels bool `json:"all_channels"`
ChannelIDs []int `json:"channel_ids,omitempty"`
ChannelTypes []int `json:"channel_types,omitempty"`
ModelPatterns []string `json:"model_patterns,omitempty"`
}
func (p ChatCompletionsToResponsesPolicy) IsChannelEnabled(channelID int, channelType int) bool {
if !p.Enabled {
return false
}
if p.AllChannels {
return true
}
if channelID > 0 && len(p.ChannelIDs) > 0 && slices.Contains(p.ChannelIDs, channelID) {
return true
}
if channelType > 0 && len(p.ChannelTypes) > 0 && slices.Contains(p.ChannelTypes, channelType) {
return true
}
return false
}
type GlobalSettings struct {
PassThroughRequestEnabled bool `json:"pass_through_request_enabled"`
ThinkingModelBlacklist []string `json:"thinking_model_blacklist"`
ChatCompletionsToResponsesPolicy ChatCompletionsToResponsesPolicy `json:"chat_completions_to_responses_policy"`
}
// 默认配置
var defaultOpenaiSettings = GlobalSettings{
PassThroughRequestEnabled: false,
ThinkingModelBlacklist: []string{
"moonshotai/kimi-k2-thinking",
"kimi-k2-thinking",
},
ChatCompletionsToResponsesPolicy: ChatCompletionsToResponsesPolicy{
Enabled: false,
AllChannels: true,
},
}
// 全局实例
var globalSettings = defaultOpenaiSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("global", &globalSettings)
}
func GetGlobalSettings() *GlobalSettings {
return &globalSettings
}
// ShouldPreserveThinkingSuffix 判断模型是否配置为保留 thinking/-nothinking/-low/-high/-medium 后缀
func ShouldPreserveThinkingSuffix(modelName string) bool {
target := strings.TrimSpace(modelName)
if target == "" {
return false
}
for _, entry := range globalSettings.ThinkingModelBlacklist {
if strings.TrimSpace(entry) == target {
return true
}
}
return false
}

View File

@@ -0,0 +1,24 @@
package model_setting
import "github.com/QuantumNous/new-api/setting/config"
// GrokSettings defines Grok model configuration.
type GrokSettings struct {
ViolationDeductionEnabled bool `json:"violation_deduction_enabled"`
ViolationDeductionAmount float64 `json:"violation_deduction_amount"`
}
var defaultGrokSettings = GrokSettings{
ViolationDeductionEnabled: true,
ViolationDeductionAmount: 0.05,
}
var grokSettings = defaultGrokSettings
func init() {
config.GlobalConfig.Register("grok", &grokSettings)
}
func GetGrokSettings() *GrokSettings {
return &grokSettings
}

View File

@@ -0,0 +1,50 @@
package model_setting
import (
"strings"
"github.com/QuantumNous/new-api/setting/config"
)
// QwenSettings defines Qwen model configuration. 注意bool要以enabled结尾才可以生效编辑
type QwenSettings struct {
SyncImageModels []string `json:"sync_image_models"`
}
// 默认配置
var defaultQwenSettings = QwenSettings{
SyncImageModels: []string{
"z-image",
"qwen-image",
"wan2.6",
"qwen-image-edit",
"qwen-image-edit-max",
"qwen-image-edit-max-2026-01-16",
"qwen-image-edit-plus",
"qwen-image-edit-plus-2025-12-15",
"qwen-image-edit-plus-2025-10-30",
},
}
// 全局实例
var qwenSettings = defaultQwenSettings
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("qwen", &qwenSettings)
}
// GetQwenSettings
func GetQwenSettings() *QwenSettings {
return &qwenSettings
}
// IsSyncImageModel
func IsSyncImageModel(model string) bool {
for _, m := range qwenSettings.SyncImageModels {
if strings.Contains(model, m) {
return true
}
}
return false
}

View 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
}

View 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
}

View 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> 的 XTOKENS 不适用)
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
}
}

View 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
}

View 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)
}
}
}

View 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
}

View 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
}

View 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", &quotaSetting)
}
func GetQuotaSetting() *QuotaSetting {
return &quotaSetting
}

View 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
}

View 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))
}

View 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
}

View 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
}

6
setting/payment_creem.go Normal file
View File

@@ -0,0 +1,6 @@
package setting
var CreemApiKey = ""
var CreemProducts = "[]"
var CreemTestMode = false
var CreemWebhookSecret = ""

View File

@@ -0,0 +1,8 @@
package setting
var StripeApiSecret = ""
var StripeWebhookSecret = ""
var StripePriceId = ""
var StripeUnitPrice = 8.0
var StripeMinTopUp = 1
var StripePromotionCodesEnabled = false

67
setting/payment_waffo.go Normal file
View File

@@ -0,0 +1,67 @@
package setting
import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
)
var (
WaffoEnabled bool
WaffoApiKey string
WaffoPrivateKey string
WaffoPublicCert string
WaffoSandboxPublicCert string
WaffoSandboxApiKey string
WaffoSandboxPrivateKey string
WaffoSandbox bool
WaffoMerchantId string
WaffoNotifyUrl string
WaffoReturnUrl string
WaffoSubscriptionReturnUrl string
WaffoCurrency string
WaffoUnitPrice float64 = 1.0
WaffoMinTopUp int = 1
)
// GetWaffoPayMethods 从 options 读取 Waffo 支付方式配置
func GetWaffoPayMethods() []constant.WaffoPayMethod {
common.OptionMapRWMutex.RLock()
jsonStr := common.OptionMap["WaffoPayMethods"]
common.OptionMapRWMutex.RUnlock()
if jsonStr == "" {
return copyDefaultWaffoPayMethods()
}
var methods []constant.WaffoPayMethod
if err := common.UnmarshalJsonStr(jsonStr, &methods); err != nil {
return copyDefaultWaffoPayMethods()
}
return methods
}
// SetWaffoPayMethods 序列化 Waffo 支付方式配置并更新 OptionMap
func SetWaffoPayMethods(methods []constant.WaffoPayMethod) error {
jsonBytes, err := common.Marshal(methods)
if err != nil {
return err
}
common.OptionMapRWMutex.Lock()
common.OptionMap["WaffoPayMethods"] = string(jsonBytes)
common.OptionMapRWMutex.Unlock()
return nil
}
func copyDefaultWaffoPayMethods() []constant.WaffoPayMethod {
cp := make([]constant.WaffoPayMethod, len(constant.DefaultWaffoPayMethods))
copy(cp, constant.DefaultWaffoPayMethods)
return cp
}
// WaffoPayMethods2JsonString 将默认 WaffoPayMethods 序列化为 JSON 字符串(供 InitOptionMap 使用)
func WaffoPayMethods2JsonString() string {
jsonBytes, err := common.Marshal(constant.DefaultWaffoPayMethods)
if err != nil {
return "[]"
}
return string(jsonBytes)
}

View File

@@ -0,0 +1,85 @@
package performance_setting
import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/config"
)
// PerformanceSetting 性能设置配置
type PerformanceSetting struct {
// DiskCacheEnabled 是否启用磁盘缓存(磁盘换内存)
DiskCacheEnabled bool `json:"disk_cache_enabled"`
// DiskCacheThresholdMB 触发磁盘缓存的请求体大小阈值MB
DiskCacheThresholdMB int `json:"disk_cache_threshold_mb"`
// DiskCacheMaxSizeMB 磁盘缓存最大总大小MB
DiskCacheMaxSizeMB int `json:"disk_cache_max_size_mb"`
// DiskCachePath 磁盘缓存目录
DiskCachePath string `json:"disk_cache_path"`
// MonitorEnabled 是否启用性能监控
MonitorEnabled bool `json:"monitor_enabled"`
// MonitorCPUThreshold CPU 使用率阈值(%
MonitorCPUThreshold int `json:"monitor_cpu_threshold"`
// MonitorMemoryThreshold 内存使用率阈值(%
MonitorMemoryThreshold int `json:"monitor_memory_threshold"`
// MonitorDiskThreshold 磁盘使用率阈值(%
MonitorDiskThreshold int `json:"monitor_disk_threshold"`
}
// 默认配置
var performanceSetting = PerformanceSetting{
DiskCacheEnabled: false,
DiskCacheThresholdMB: 10, // 超过 10MB 使用磁盘缓存
DiskCacheMaxSizeMB: 1024, // 最大 1GB 磁盘缓存
DiskCachePath: "", // 空表示使用系统临时目录
MonitorEnabled: true,
MonitorCPUThreshold: 90,
MonitorMemoryThreshold: 90,
MonitorDiskThreshold: 95,
}
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("performance_setting", &performanceSetting)
// 同步初始配置到 common 包
syncToCommon()
}
// syncToCommon 将配置同步到 common 包
func syncToCommon() {
common.SetDiskCacheConfig(common.DiskCacheConfig{
Enabled: performanceSetting.DiskCacheEnabled,
ThresholdMB: performanceSetting.DiskCacheThresholdMB,
MaxSizeMB: performanceSetting.DiskCacheMaxSizeMB,
Path: performanceSetting.DiskCachePath,
})
common.SetPerformanceMonitorConfig(common.PerformanceMonitorConfig{
Enabled: performanceSetting.MonitorEnabled,
CPUThreshold: performanceSetting.MonitorCPUThreshold,
MemoryThreshold: performanceSetting.MonitorMemoryThreshold,
DiskThreshold: performanceSetting.MonitorDiskThreshold,
})
}
// GetPerformanceSetting 获取性能设置
func GetPerformanceSetting() *PerformanceSetting {
return &performanceSetting
}
// UpdateAndSync 更新配置并同步到 common 包
// 当配置从数据库加载后,需要调用此函数同步
func UpdateAndSync() {
syncToCommon()
}
// GetCacheStats 获取缓存统计信息(代理到 common 包)
func GetCacheStats() common.DiskCacheStats {
return common.GetDiskCacheStats()
}
// ResetStats 重置统计信息
func ResetStats() {
common.ResetDiskCacheStats()
}

69
setting/rate_limit.go Normal file
View File

@@ -0,0 +1,69 @@
package setting
import (
"encoding/json"
"fmt"
"math"
"sync"
"github.com/QuantumNous/new-api/common"
)
var ModelRequestRateLimitEnabled = false
var ModelRequestRateLimitDurationMinutes = 1
var ModelRequestRateLimitCount = 0
var ModelRequestRateLimitSuccessCount = 1000
var ModelRequestRateLimitGroup = map[string][2]int{}
var ModelRequestRateLimitMutex sync.RWMutex
func ModelRequestRateLimitGroup2JSONString() string {
ModelRequestRateLimitMutex.RLock()
defer ModelRequestRateLimitMutex.RUnlock()
jsonBytes, err := json.Marshal(ModelRequestRateLimitGroup)
if err != nil {
common.SysLog("error marshalling model ratio: " + err.Error())
}
return string(jsonBytes)
}
func UpdateModelRequestRateLimitGroupByJSONString(jsonStr string) error {
ModelRequestRateLimitMutex.RLock()
defer ModelRequestRateLimitMutex.RUnlock()
ModelRequestRateLimitGroup = make(map[string][2]int)
return json.Unmarshal([]byte(jsonStr), &ModelRequestRateLimitGroup)
}
func GetGroupRateLimit(group string) (totalCount, successCount int, found bool) {
ModelRequestRateLimitMutex.RLock()
defer ModelRequestRateLimitMutex.RUnlock()
if ModelRequestRateLimitGroup == nil {
return 0, 0, false
}
limits, found := ModelRequestRateLimitGroup[group]
if !found {
return 0, 0, false
}
return limits[0], limits[1], true
}
func CheckModelRequestRateLimitGroup(jsonStr string) error {
checkModelRequestRateLimitGroup := make(map[string][2]int)
err := json.Unmarshal([]byte(jsonStr), &checkModelRequestRateLimitGroup)
if err != nil {
return err
}
for group, limits := range checkModelRequestRateLimitGroup {
if limits[0] < 0 || limits[1] < 1 {
return fmt.Errorf("group %s has negative rate limit values: [%d, %d]", group, limits[0], limits[1])
}
if limits[0] > math.MaxInt32 || limits[1] > math.MaxInt32 {
return fmt.Errorf("group %s [%d, %d] has max rate limits value 2147483647", group, limits[0], limits[1])
}
}
return nil
}

View File

@@ -0,0 +1,150 @@
package ratio_setting
import (
"github.com/QuantumNous/new-api/types"
)
var defaultCacheRatio = map[string]float64{
"gemini-3-flash-preview": 0.1,
"gemini-3-pro-preview": 0.1,
"gemini-3.1-pro-preview": 0.1,
"gpt-4": 0.5,
"o1": 0.5,
"o1-2024-12-17": 0.5,
"o1-preview-2024-09-12": 0.5,
"o1-preview": 0.5,
"o1-mini-2024-09-12": 0.5,
"o1-mini": 0.5,
"o3-mini": 0.5,
"o3-mini-2025-01-31": 0.5,
"gpt-4o-2024-11-20": 0.5,
"gpt-4o-2024-08-06": 0.5,
"gpt-4o": 0.5,
"gpt-4o-mini-2024-07-18": 0.5,
"gpt-4o-mini": 0.5,
"gpt-4o-realtime-preview": 0.5,
"gpt-4o-mini-realtime-preview": 0.5,
"gpt-4.5-preview": 0.5,
"gpt-4.5-preview-2025-02-27": 0.5,
"gpt-4.1": 0.25,
"gpt-4.1-mini": 0.25,
"gpt-4.1-nano": 0.25,
"gpt-5": 0.1,
"gpt-5-2025-08-07": 0.1,
"gpt-5-chat-latest": 0.1,
"gpt-5-mini": 0.1,
"gpt-5-mini-2025-08-07": 0.1,
"gpt-5-nano": 0.1,
"gpt-5-nano-2025-08-07": 0.1,
"deepseek-chat": 0.25,
"deepseek-reasoner": 0.25,
"deepseek-coder": 0.25,
"claude-3-sonnet-20240229": 0.1,
"claude-3-opus-20240229": 0.1,
"claude-3-haiku-20240307": 0.1,
"claude-3-5-haiku-20241022": 0.1,
"claude-haiku-4-5-20251001": 0.1,
"claude-3-5-sonnet-20240620": 0.1,
"claude-3-5-sonnet-20241022": 0.1,
"claude-3-7-sonnet-20250219": 0.1,
"claude-3-7-sonnet-20250219-thinking": 0.1,
"claude-sonnet-4-20250514": 0.1,
"claude-sonnet-4-20250514-thinking": 0.1,
"claude-opus-4-20250514": 0.1,
"claude-opus-4-20250514-thinking": 0.1,
"claude-opus-4-1-20250805": 0.1,
"claude-opus-4-1-20250805-thinking": 0.1,
"claude-sonnet-4-5-20250929": 0.1,
"claude-sonnet-4-5-20250929-thinking": 0.1,
"claude-opus-4-5-20251101": 0.1,
"claude-opus-4-5-20251101-thinking": 0.1,
"claude-opus-4-6": 0.1,
"claude-opus-4-6-thinking": 0.1,
"claude-opus-4-6-max": 0.1,
"claude-opus-4-6-high": 0.1,
"claude-opus-4-6-medium": 0.1,
"claude-opus-4-6-low": 0.1,
}
var defaultCreateCacheRatio = map[string]float64{
"claude-3-sonnet-20240229": 1.25,
"claude-3-opus-20240229": 1.25,
"claude-3-haiku-20240307": 1.25,
"claude-3-5-haiku-20241022": 1.25,
"claude-haiku-4-5-20251001": 1.25,
"claude-3-5-sonnet-20240620": 1.25,
"claude-3-5-sonnet-20241022": 1.25,
"claude-3-7-sonnet-20250219": 1.25,
"claude-3-7-sonnet-20250219-thinking": 1.25,
"claude-sonnet-4-20250514": 1.25,
"claude-sonnet-4-20250514-thinking": 1.25,
"claude-opus-4-20250514": 1.25,
"claude-opus-4-20250514-thinking": 1.25,
"claude-opus-4-1-20250805": 1.25,
"claude-opus-4-1-20250805-thinking": 1.25,
"claude-sonnet-4-5-20250929": 1.25,
"claude-sonnet-4-5-20250929-thinking": 1.25,
"claude-opus-4-5-20251101": 1.25,
"claude-opus-4-5-20251101-thinking": 1.25,
"claude-opus-4-6": 1.25,
"claude-opus-4-6-thinking": 1.25,
"claude-opus-4-6-max": 1.25,
"claude-opus-4-6-high": 1.25,
"claude-opus-4-6-medium": 1.25,
"claude-opus-4-6-low": 1.25,
}
//var defaultCreateCacheRatio = map[string]float64{}
var cacheRatioMap = types.NewRWMap[string, float64]()
var createCacheRatioMap = types.NewRWMap[string, float64]()
// GetCacheRatioMap returns a copy of the cache ratio map
func GetCacheRatioMap() map[string]float64 {
return cacheRatioMap.ReadAll()
}
// CacheRatio2JSONString converts the cache ratio map to a JSON string
func CacheRatio2JSONString() string {
return cacheRatioMap.MarshalJSONString()
}
// CreateCacheRatio2JSONString converts the create cache ratio map to a JSON string
func CreateCacheRatio2JSONString() string {
return createCacheRatioMap.MarshalJSONString()
}
// UpdateCacheRatioByJSONString updates the cache ratio map from a JSON string
func UpdateCacheRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(cacheRatioMap, jsonStr, InvalidateExposedDataCache)
}
// UpdateCreateCacheRatioByJSONString updates the create cache ratio map from a JSON string
func UpdateCreateCacheRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(createCacheRatioMap, jsonStr, InvalidateExposedDataCache)
}
// GetCacheRatio returns the cache ratio for a model
func GetCacheRatio(name string) (float64, bool) {
ratio, ok := cacheRatioMap.Get(name)
if !ok {
return 1, false // Default to 1 if not found
}
return ratio, true
}
func GetCreateCacheRatio(name string) (float64, bool) {
ratio, ok := createCacheRatioMap.Get(name)
if !ok {
return 1.25, false // Default to 1.25 if not found
}
return ratio, true
}
func GetCacheRatioCopy() map[string]float64 {
return cacheRatioMap.ReadAll()
}
func GetCreateCacheRatioCopy() map[string]float64 {
return createCacheRatioMap.ReadAll()
}

View File

@@ -0,0 +1,13 @@
package ratio_setting
import "strings"
const CompactModelSuffix = "-openai-compact"
const CompactWildcardModelKey = "*" + CompactModelSuffix
func WithCompactModelSuffix(modelName string) string {
if strings.HasSuffix(modelName, CompactModelSuffix) {
return modelName
}
return modelName + CompactModelSuffix
}

View File

@@ -0,0 +1,17 @@
package ratio_setting
import "sync/atomic"
var exposeRatioEnabled atomic.Bool
func init() {
exposeRatioEnabled.Store(false)
}
func SetExposeRatioEnabled(enabled bool) {
exposeRatioEnabled.Store(enabled)
}
func IsExposeRatioEnabled() bool {
return exposeRatioEnabled.Load()
}

View File

@@ -0,0 +1,56 @@
package ratio_setting
import (
"sync"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
)
const exposedDataTTL = 30 * time.Second
type exposedCache struct {
data gin.H
expiresAt time.Time
}
var (
exposedData atomic.Value
rebuildMu sync.Mutex
)
func InvalidateExposedDataCache() {
exposedData.Store((*exposedCache)(nil))
}
func cloneGinH(src gin.H) gin.H {
dst := make(gin.H, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}
func GetExposedData() gin.H {
if c, ok := exposedData.Load().(*exposedCache); ok && c != nil && time.Now().Before(c.expiresAt) {
return cloneGinH(c.data)
}
rebuildMu.Lock()
defer rebuildMu.Unlock()
if c, ok := exposedData.Load().(*exposedCache); ok && c != nil && time.Now().Before(c.expiresAt) {
return cloneGinH(c.data)
}
newData := gin.H{
"model_ratio": GetModelRatioCopy(),
"completion_ratio": GetCompletionRatioCopy(),
"cache_ratio": GetCacheRatioCopy(),
"create_cache_ratio": GetCreateCacheRatioCopy(),
"model_price": GetModelPriceCopy(),
}
exposedData.Store(&exposedCache{
data: newData,
expiresAt: time.Now().Add(exposedDataTTL),
})
return cloneGinH(newData)
}

View File

@@ -0,0 +1,125 @@
package ratio_setting
import (
"encoding/json"
"errors"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/config"
"github.com/QuantumNous/new-api/types"
)
var defaultGroupRatio = map[string]float64{
"default": 1,
"vip": 1,
"svip": 1,
}
var groupRatioMap = types.NewRWMap[string, float64]()
var defaultGroupGroupRatio = map[string]map[string]float64{
"vip": {
"edit_this": 0.9,
},
}
var groupGroupRatioMap = types.NewRWMap[string, map[string]float64]()
var defaultGroupSpecialUsableGroup = map[string]map[string]string{
"vip": {
"append_1": "vip_special_group_1",
"-:remove_1": "vip_removed_group_1",
},
}
type GroupRatioSetting struct {
GroupRatio *types.RWMap[string, float64] `json:"group_ratio"`
GroupGroupRatio *types.RWMap[string, map[string]float64] `json:"group_group_ratio"`
GroupSpecialUsableGroup *types.RWMap[string, map[string]string] `json:"group_special_usable_group"`
}
var groupRatioSetting GroupRatioSetting
func init() {
groupSpecialUsableGroup := types.NewRWMap[string, map[string]string]()
groupSpecialUsableGroup.AddAll(defaultGroupSpecialUsableGroup)
groupRatioMap.AddAll(defaultGroupRatio)
groupGroupRatioMap.AddAll(defaultGroupGroupRatio)
groupRatioSetting = GroupRatioSetting{
GroupSpecialUsableGroup: groupSpecialUsableGroup,
GroupRatio: groupRatioMap,
GroupGroupRatio: groupGroupRatioMap,
}
config.GlobalConfig.Register("group_ratio_setting", &groupRatioSetting)
}
func GetGroupRatioSetting() *GroupRatioSetting {
if groupRatioSetting.GroupSpecialUsableGroup == nil {
groupRatioSetting.GroupSpecialUsableGroup = types.NewRWMap[string, map[string]string]()
groupRatioSetting.GroupSpecialUsableGroup.AddAll(defaultGroupSpecialUsableGroup)
}
return &groupRatioSetting
}
func GetGroupRatioCopy() map[string]float64 {
return groupRatioMap.ReadAll()
}
func ContainsGroupRatio(name string) bool {
_, ok := groupRatioMap.Get(name)
return ok
}
func GroupRatio2JSONString() string {
return groupRatioMap.MarshalJSONString()
}
func UpdateGroupRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonString(groupRatioMap, jsonStr)
}
func GetGroupRatio(name string) float64 {
ratio, ok := groupRatioMap.Get(name)
if !ok {
common.SysLog("group ratio not found: " + name)
return 1
}
return ratio
}
func GetGroupGroupRatio(userGroup, usingGroup string) (float64, bool) {
gp, ok := groupGroupRatioMap.Get(userGroup)
if !ok {
return -1, false
}
ratio, ok := gp[usingGroup]
if !ok {
return -1, false
}
return ratio, true
}
func GroupGroupRatio2JSONString() string {
return groupGroupRatioMap.MarshalJSONString()
}
func UpdateGroupGroupRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonString(groupGroupRatioMap, jsonStr)
}
func CheckGroupRatio(jsonStr string) error {
checkGroupRatio := make(map[string]float64)
err := json.Unmarshal([]byte(jsonStr), &checkGroupRatio)
if err != nil {
return err
}
for name, ratio := range checkGroupRatio {
if ratio < 0 {
return errors.New("group ratio must be not less than 0: " + name)
}
}
return nil
}

View File

@@ -0,0 +1,734 @@
package ratio_setting
import (
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/types"
)
// from songquanpeng/one-api
const (
USD2RMB = 7.3 // 暂定 1 USD = 7.3 RMB
USD = 500 // $0.002 = 1 -> $1 = 500
RMB = USD / USD2RMB
)
// modelRatio
// https://platform.openai.com/docs/models/model-endpoint-compatibility
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
// https://openai.com/pricing
// TODO: when a new api is enabled, check the pricing here
// 1 === $0.002 / 1K tokens
// 1 === ¥0.014 / 1k tokens
var defaultModelRatio = map[string]float64{
//"midjourney": 50,
"gpt-4-gizmo-*": 15,
"gpt-4o-gizmo-*": 2.5,
"gpt-4-all": 15,
"gpt-4o-all": 15,
"gpt-4": 15,
//"gpt-4-0314": 15, //deprecated
"gpt-4-0613": 15,
"gpt-4-32k": 30,
//"gpt-4-32k-0314": 30, //deprecated
"gpt-4-32k-0613": 30,
"gpt-4-1106-preview": 5, // $10 / 1M tokens
"gpt-4-0125-preview": 5, // $10 / 1M tokens
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
"gpt-4-vision-preview": 5, // $10 / 1M tokens
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
"gpt-4o": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
"gpt-4o-realtime-preview": 2.5,
"gpt-4o-realtime-preview-2024-10-01": 2.5,
"gpt-4o-realtime-preview-2024-12-17": 2.5,
"gpt-4o-mini-realtime-preview": 0.3,
"gpt-4o-mini-realtime-preview-2024-12-17": 0.3,
"gpt-4.1": 1.0, // $2 / 1M tokens
"gpt-4.1-2025-04-14": 1.0, // $2 / 1M tokens
"gpt-4.1-mini": 0.2, // $0.4 / 1M tokens
"gpt-4.1-mini-2025-04-14": 0.2, // $0.4 / 1M tokens
"gpt-4.1-nano": 0.05, // $0.1 / 1M tokens
"gpt-4.1-nano-2025-04-14": 0.05, // $0.1 / 1M tokens
"gpt-image-1": 2.5, // $5 / 1M tokens
"o1": 7.5, // $15 / 1M tokens
"o1-2024-12-17": 7.5, // $15 / 1M tokens
"o1-preview": 7.5, // $15 / 1M tokens
"o1-preview-2024-09-12": 7.5, // $15 / 1M tokens
"o1-mini": 0.55, // $1.1 / 1M tokens
"o1-mini-2024-09-12": 0.55, // $1.1 / 1M tokens
"o1-pro": 75.0, // $150 / 1M tokens
"o1-pro-2025-03-19": 75.0, // $150 / 1M tokens
"o3-mini": 0.55,
"o3-mini-2025-01-31": 0.55,
"o3-mini-high": 0.55,
"o3-mini-2025-01-31-high": 0.55,
"o3-mini-low": 0.55,
"o3-mini-2025-01-31-low": 0.55,
"o3-mini-medium": 0.55,
"o3-mini-2025-01-31-medium": 0.55,
"o3": 1.0, // $2 / 1M tokens
"o3-2025-04-16": 1.0, // $2 / 1M tokens
"o3-pro": 10.0, // $20 / 1M tokens
"o3-pro-2025-06-10": 10.0, // $20 / 1M tokens
"o3-deep-research": 5.0, // $10 / 1M tokens
"o3-deep-research-2025-06-26": 5.0, // $10 / 1M tokens
"o4-mini": 0.55, // $1.1 / 1M tokens
"o4-mini-2025-04-16": 0.55, // $1.1 / 1M tokens
"o4-mini-deep-research": 1.0, // $2 / 1M tokens
"o4-mini-deep-research-2025-06-26": 1.0, // $2 / 1M tokens
"gpt-4o-mini": 0.075,
"gpt-4o-mini-2024-07-18": 0.075,
"gpt-4-turbo": 5, // $0.01 / 1K tokens
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
"gpt-4.5-preview": 37.5,
"gpt-4.5-preview-2025-02-27": 37.5,
"gpt-5": 0.625,
"gpt-5-2025-08-07": 0.625,
"gpt-5-chat-latest": 0.625,
"gpt-5-mini": 0.125,
"gpt-5-mini-2025-08-07": 0.125,
"gpt-5-nano": 0.025,
"gpt-5-nano-2025-08-07": 0.025,
//"gpt-3.5-turbo-0301": 0.75, //deprecated
"gpt-3.5-turbo": 0.25,
"gpt-3.5-turbo-0613": 0.75,
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
"gpt-3.5-turbo-16k-0613": 1.5,
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
"gpt-3.5-turbo-0125": 0.25,
"babbage-002": 0.2, // $0.0004 / 1K tokens
"davinci-002": 1, // $0.002 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
//"text-davinci-002": 10,
//"text-davinci-003": 10,
"text-davinci-edit-001": 10,
"code-davinci-edit-001": 10,
"whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens
"tts-1": 7.5, // 1k characters -> $0.015
"tts-1-1106": 7.5, // 1k characters -> $0.015
"tts-1-hd": 15, // 1k characters -> $0.03
"tts-1-hd-1106": 15, // 1k characters -> $0.03
"davinci": 10,
"curie": 10,
"babbage": 10,
"ada": 10,
"text-embedding-3-small": 0.01,
"text-embedding-3-large": 0.065,
"text-embedding-ada-002": 0.05,
"text-search-ada-doc-001": 10,
"text-moderation-stable": 0.1,
"text-moderation-latest": 0.1,
"claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens
"claude-3-5-haiku-20241022": 0.5, // $1 / 1M tokens
"claude-haiku-4-5-20251001": 0.5, // $1 / 1M tokens
"claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens
"claude-3-5-sonnet-20240620": 1.5,
"claude-3-5-sonnet-20241022": 1.5,
"claude-3-7-sonnet-20250219": 1.5,
"claude-3-7-sonnet-20250219-thinking": 1.5,
"claude-sonnet-4-20250514": 1.5,
"claude-sonnet-4-5-20250929": 1.5,
"claude-opus-4-5-20251101": 2.5,
"claude-opus-4-6": 2.5,
"claude-opus-4-6-max": 2.5,
"claude-opus-4-6-high": 2.5,
"claude-opus-4-6-medium": 2.5,
"claude-opus-4-6-low": 2.5,
"claude-3-opus-20240229": 7.5, // $15 / 1M tokens
"claude-opus-4-20250514": 7.5,
"claude-opus-4-1-20250805": 7.5,
"ERNIE-4.0-8K": 0.120 * RMB,
"ERNIE-3.5-8K": 0.012 * RMB,
"ERNIE-3.5-8K-0205": 0.024 * RMB,
"ERNIE-3.5-8K-1222": 0.012 * RMB,
"ERNIE-Bot-8K": 0.024 * RMB,
"ERNIE-3.5-4K-0205": 0.012 * RMB,
"ERNIE-Speed-8K": 0.004 * RMB,
"ERNIE-Speed-128K": 0.004 * RMB,
"ERNIE-Lite-8K-0922": 0.008 * RMB,
"ERNIE-Lite-8K-0308": 0.003 * RMB,
"ERNIE-Tiny-8K": 0.001 * RMB,
"BLOOMZ-7B": 0.004 * RMB,
"Embedding-V1": 0.002 * RMB,
"bge-large-zh": 0.002 * RMB,
"bge-large-en": 0.002 * RMB,
"tao-8k": 0.002 * RMB,
"PaLM-2": 1,
"gemini-1.5-pro-latest": 1.25, // $3.5 / 1M tokens
"gemini-1.5-flash-latest": 0.075,
"gemini-2.0-flash": 0.05,
"gemini-2.5-pro-exp-03-25": 0.625,
"gemini-2.5-pro-preview-03-25": 0.625,
"gemini-2.5-pro": 0.625,
"gemini-2.5-flash-preview-04-17": 0.075,
"gemini-2.5-flash-preview-04-17-thinking": 0.075,
"gemini-2.5-flash-preview-04-17-nothinking": 0.075,
"gemini-2.5-flash-preview-05-20": 0.075,
"gemini-2.5-flash-preview-05-20-thinking": 0.075,
"gemini-2.5-flash-preview-05-20-nothinking": 0.075,
"gemini-2.5-flash-thinking-*": 0.075, // 用于为后续所有2.5 flash thinking budget 模型设置默认倍率
"gemini-2.5-pro-thinking-*": 0.625, // 用于为后续所有2.5 pro thinking budget 模型设置默认倍率
"gemini-2.5-flash-lite-preview-thinking-*": 0.05,
"gemini-2.5-flash-lite-preview-06-17": 0.05,
"gemini-2.5-flash": 0.15,
"gemini-robotics-er-1.5-preview": 0.15,
"gemini-embedding-001": 0.075,
"text-embedding-004": 0.001,
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
"glm-4": 7.143, // ¥0.1 / 1k tokens
"glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens
"glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens
"glm-3-turbo": 0.3572,
"glm-4-plus": 0.05 * RMB,
"glm-4-0520": 0.1 * RMB,
"glm-4-air": 0.001 * RMB,
"glm-4-airx": 0.01 * RMB,
"glm-4-long": 0.001 * RMB,
"glm-4-flash": 0,
"glm-4v-plus": 0.01 * RMB,
"qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
"qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
"SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens
"SparkDesk-v4.0": 1.2858,
"360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
"360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
// https://platform.lingyiwanwu.com/docs#-计费单元
// 已经按照 7.2 来换算美元价格
"yi-34b-chat-0205": 0.18,
"yi-34b-chat-200k": 0.864,
"yi-vl-plus": 0.432,
"yi-large": 20.0 / 1000 * RMB,
"yi-medium": 2.5 / 1000 * RMB,
"yi-vision": 6.0 / 1000 * RMB,
"yi-medium-200k": 12.0 / 1000 * RMB,
"yi-spark": 1.0 / 1000 * RMB,
"yi-large-rag": 25.0 / 1000 * RMB,
"yi-large-turbo": 12.0 / 1000 * RMB,
"yi-large-preview": 20.0 / 1000 * RMB,
"yi-large-rag-preview": 25.0 / 1000 * RMB,
"command": 0.5,
"command-nightly": 0.5,
"command-light": 0.5,
"command-light-nightly": 0.5,
"command-r": 0.25,
"command-r-plus": 1.5,
"command-r-08-2024": 0.075,
"command-r-plus-08-2024": 1.25,
"deepseek-chat": 0.27 / 2,
"deepseek-coder": 0.27 / 2,
"deepseek-reasoner": 0.55 / 2, // 0.55 / 1k tokens
// Perplexity online 模型对搜索额外收费,有需要应自行调整,此处不计入搜索费用
"llama-3-sonar-small-32k-chat": 0.2 / 1000 * USD,
"llama-3-sonar-small-32k-online": 0.2 / 1000 * USD,
"llama-3-sonar-large-32k-chat": 1 / 1000 * USD,
"llama-3-sonar-large-32k-online": 1 / 1000 * USD,
// grok
"grok-3-beta": 1.5,
"grok-3-mini-beta": 0.15,
"grok-2": 1,
"grok-2-vision": 1,
"grok-beta": 2.5,
"grok-vision-beta": 2.5,
"grok-3-fast-beta": 2.5,
"grok-3-mini-fast-beta": 0.3,
// submodel
"NousResearch/Hermes-4-405B-FP8": 0.8,
"Qwen/Qwen3-235B-A22B-Thinking-2507": 0.6,
"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8": 0.8,
"Qwen/Qwen3-235B-A22B-Instruct-2507": 0.3,
"zai-org/GLM-4.5-FP8": 0.8,
"openai/gpt-oss-120b": 0.5,
"deepseek-ai/DeepSeek-R1-0528": 0.8,
"deepseek-ai/DeepSeek-R1": 0.8,
"deepseek-ai/DeepSeek-V3-0324": 0.8,
"deepseek-ai/DeepSeek-V3.1": 0.8,
}
var defaultModelPrice = map[string]float64{
"suno_music": 0.1,
"suno_lyrics": 0.01,
"dall-e-3": 0.04,
"imagen-3.0-generate-002": 0.03,
"black-forest-labs/flux-1.1-pro": 0.04,
"gpt-4-gizmo-*": 0.1,
"mj_video": 0.8,
"mj_imagine": 0.1,
"mj_edits": 0.1,
"mj_variation": 0.1,
"mj_reroll": 0.1,
"mj_blend": 0.1,
"mj_modal": 0.1,
"mj_zoom": 0.1,
"mj_shorten": 0.1,
"mj_high_variation": 0.1,
"mj_low_variation": 0.1,
"mj_pan": 0.1,
"mj_inpaint": 0,
"mj_custom_zoom": 0,
"mj_describe": 0.05,
"mj_upscale": 0.05,
"swap_face": 0.05,
"mj_upload": 0.05,
"sora-2": 0.3,
"sora-2-pro": 0.5,
"gpt-4o-mini-tts": 0.3,
"veo-3.0-generate-001": 0.4,
"veo-3.0-fast-generate-001": 0.15,
"veo-3.1-generate-preview": 0.4,
"veo-3.1-fast-generate-preview": 0.15,
}
var defaultAudioRatio = map[string]float64{
"gpt-4o-audio-preview": 16,
"gpt-4o-mini-audio-preview": 66.67,
"gpt-4o-realtime-preview": 8,
"gpt-4o-mini-realtime-preview": 16.67,
"gpt-4o-mini-tts": 25,
}
var defaultAudioCompletionRatio = map[string]float64{
"gpt-4o-realtime": 2,
"gpt-4o-mini-realtime": 2,
"gpt-4o-mini-tts": 1,
"tts-1": 0,
"tts-1-hd": 0,
"tts-1-1106": 0,
"tts-1-hd-1106": 0,
}
var modelPriceMap = types.NewRWMap[string, float64]()
var modelRatioMap = types.NewRWMap[string, float64]()
var completionRatioMap = types.NewRWMap[string, float64]()
var defaultCompletionRatio = map[string]float64{
"gpt-4-gizmo-*": 2,
"gpt-4o-gizmo-*": 3,
"gpt-4-all": 2,
"gpt-image-1": 8,
}
// InitRatioSettings initializes all model related settings maps
func InitRatioSettings() {
modelPriceMap.AddAll(defaultModelPrice)
modelRatioMap.AddAll(defaultModelRatio)
completionRatioMap.AddAll(defaultCompletionRatio)
cacheRatioMap.AddAll(defaultCacheRatio)
createCacheRatioMap.AddAll(defaultCreateCacheRatio)
imageRatioMap.AddAll(defaultImageRatio)
audioRatioMap.AddAll(defaultAudioRatio)
audioCompletionRatioMap.AddAll(defaultAudioCompletionRatio)
}
func GetModelPriceMap() map[string]float64 {
return modelPriceMap.ReadAll()
}
func ModelPrice2JSONString() string {
return modelPriceMap.MarshalJSONString()
}
func UpdateModelPriceByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(modelPriceMap, jsonStr, InvalidateExposedDataCache)
}
// GetModelPrice 返回模型的价格,如果模型不存在则返回-1false
func GetModelPrice(name string, printErr bool) (float64, bool) {
name = FormatMatchingModelName(name)
if strings.HasSuffix(name, CompactModelSuffix) {
price, ok := modelPriceMap.Get(CompactWildcardModelKey)
if !ok {
if printErr {
common.SysError("model price not found: " + name)
}
return -1, false
}
return price, true
}
price, ok := modelPriceMap.Get(name)
if !ok {
if printErr {
common.SysError("model price not found: " + name)
}
return -1, false
}
return price, true
}
func UpdateModelRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(modelRatioMap, jsonStr, InvalidateExposedDataCache)
}
// 处理带有思考预算的模型名称,方便统一定价
func handleThinkingBudgetModel(name, prefix, wildcard string) string {
if strings.HasPrefix(name, prefix) && strings.Contains(name, "-thinking-") {
return wildcard
}
return name
}
func GetModelRatio(name string) (float64, bool, string) {
name = FormatMatchingModelName(name)
ratio, ok := modelRatioMap.Get(name)
if !ok {
if strings.HasSuffix(name, CompactModelSuffix) {
if wildcardRatio, ok := modelRatioMap.Get(CompactWildcardModelKey); ok {
return wildcardRatio, true, name
}
//return 0, true, name
}
return 37.5, operation_setting.SelfUseModeEnabled, name
}
return ratio, true, name
}
func DefaultModelRatio2JSONString() string {
jsonBytes, err := common.Marshal(defaultModelRatio)
if err != nil {
common.SysError("error marshalling model ratio: " + err.Error())
}
return string(jsonBytes)
}
func GetDefaultModelRatioMap() map[string]float64 {
return defaultModelRatio
}
func GetDefaultModelPriceMap() map[string]float64 {
return defaultModelPrice
}
func CompletionRatio2JSONString() string {
return completionRatioMap.MarshalJSONString()
}
func UpdateCompletionRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(completionRatioMap, jsonStr, InvalidateExposedDataCache)
}
func GetCompletionRatio(name string) float64 {
name = FormatMatchingModelName(name)
if strings.Contains(name, "/") {
if ratio, ok := completionRatioMap.Get(name); ok {
return ratio
}
}
hardCodedRatio, contain := getHardcodedCompletionModelRatio(name)
if contain {
return hardCodedRatio
}
if ratio, ok := completionRatioMap.Get(name); ok {
return ratio
}
return hardCodedRatio
}
type CompletionRatioInfo struct {
Ratio float64 `json:"ratio"`
Locked bool `json:"locked"`
}
func GetCompletionRatioInfo(name string) CompletionRatioInfo {
name = FormatMatchingModelName(name)
if strings.Contains(name, "/") {
if ratio, ok := completionRatioMap.Get(name); ok {
return CompletionRatioInfo{
Ratio: ratio,
Locked: false,
}
}
}
hardCodedRatio, locked := getHardcodedCompletionModelRatio(name)
if locked {
return CompletionRatioInfo{
Ratio: hardCodedRatio,
Locked: true,
}
}
if ratio, ok := completionRatioMap.Get(name); ok {
return CompletionRatioInfo{
Ratio: ratio,
Locked: false,
}
}
return CompletionRatioInfo{
Ratio: hardCodedRatio,
Locked: false,
}
}
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
isReservedModel := strings.HasSuffix(name, "-all") || strings.HasSuffix(name, "-gizmo-*")
if isReservedModel {
return 2, false
}
if strings.HasPrefix(name, "gpt-") {
if strings.HasPrefix(name, "gpt-4o") {
if name == "gpt-4o-2024-05-13" {
return 3, true
}
if strings.HasPrefix(name, "gpt-4o-mini-tts") {
return 20, false
}
return 4, false
}
// gpt-5 匹配
if strings.HasPrefix(name, "gpt-5") {
if strings.HasPrefix(name, "gpt-5.4") {
if strings.HasPrefix(name, "gpt-5.4-nano") {
return 6.25, true
}
return 6, true
}
return 8, true
}
// gpt-4.5-preview匹配
if strings.HasPrefix(name, "gpt-4.5-preview") {
return 2, true
}
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "gpt-4-1106") || strings.HasSuffix(name, "gpt-4-1105") {
return 3, true
}
// 没有特殊标记的 gpt-4 模型默认倍率为 2
return 2, false
}
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
return 4, true
}
if name == "chatgpt-4o-latest" {
return 3, true
}
if strings.Contains(name, "claude-3") {
return 5, true
} else if strings.Contains(name, "claude-sonnet-4") || strings.Contains(name, "claude-opus-4") || strings.Contains(name, "claude-haiku-4") {
return 5, true
}
if strings.HasPrefix(name, "gpt-3.5") {
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
// https://openai.com/blog/new-embedding-models-and-api-updates
// Updated GPT-3.5 Turbo model and lower pricing
return 3, true
}
if strings.HasSuffix(name, "1106") {
return 2, true
}
return 4.0 / 3.0, true
}
if strings.HasPrefix(name, "mistral-") {
return 3, true
}
if strings.HasPrefix(name, "gemini-") {
if strings.HasPrefix(name, "gemini-1.5") {
return 4, true
} else if strings.HasPrefix(name, "gemini-2.0") {
return 4, true
} else if strings.HasPrefix(name, "gemini-2.5-pro") { // 移除preview来增加兼容性这里假设正式版的倍率和preview一致
return 8, false
} else if strings.HasPrefix(name, "gemini-2.5-flash") { // 处理不同的flash模型倍率
if strings.HasPrefix(name, "gemini-2.5-flash-preview") {
if strings.HasSuffix(name, "-nothinking") {
return 4, false
}
return 3.5 / 0.15, false
}
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
return 4, false
}
return 2.5 / 0.3, false
} else if strings.HasPrefix(name, "gemini-robotics-er-1.5") {
return 2.5 / 0.3, false
} else if strings.HasPrefix(name, "gemini-3-pro") {
if strings.HasPrefix(name, "gemini-3-pro-image") {
return 60, false
}
return 6, false
}
return 4, false
}
if strings.HasPrefix(name, "command") {
switch name {
case "command-r":
return 3, true
case "command-r-plus":
return 5, true
case "command-r-08-2024":
return 4, true
case "command-r-plus-08-2024":
return 4, true
default:
return 4, false
}
}
// hint 只给官方上4倍率由于开源模型供应商自行定价不对其进行补全倍率进行强制对齐
if strings.HasPrefix(name, "ERNIE-Speed-") {
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Character") {
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Functions") {
return 2, true
}
switch name {
case "llama2-70b-4096":
return 0.8 / 0.64, true
case "llama3-8b-8192":
return 2, true
case "llama3-70b-8192":
return 0.79 / 0.59, true
}
return 1, false
}
func GetAudioRatio(name string) float64 {
name = FormatMatchingModelName(name)
if ratio, ok := audioRatioMap.Get(name); ok {
return ratio
}
return 1
}
func GetAudioCompletionRatio(name string) float64 {
name = FormatMatchingModelName(name)
if ratio, ok := audioCompletionRatioMap.Get(name); ok {
return ratio
}
return 1
}
func ContainsAudioRatio(name string) bool {
name = FormatMatchingModelName(name)
_, ok := audioRatioMap.Get(name)
return ok
}
func ContainsAudioCompletionRatio(name string) bool {
name = FormatMatchingModelName(name)
_, ok := audioCompletionRatioMap.Get(name)
return ok
}
func ModelRatio2JSONString() string {
return modelRatioMap.MarshalJSONString()
}
var defaultImageRatio = map[string]float64{
"gpt-image-1": 2,
}
var imageRatioMap = types.NewRWMap[string, float64]()
var audioRatioMap = types.NewRWMap[string, float64]()
var audioCompletionRatioMap = types.NewRWMap[string, float64]()
func ImageRatio2JSONString() string {
return imageRatioMap.MarshalJSONString()
}
func UpdateImageRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonString(imageRatioMap, jsonStr)
}
func GetImageRatio(name string) (float64, bool) {
ratio, ok := imageRatioMap.Get(name)
if !ok {
return 1, false // Default to 1 if not found
}
return ratio, true
}
func AudioRatio2JSONString() string {
return audioRatioMap.MarshalJSONString()
}
func UpdateAudioRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(audioRatioMap, jsonStr, InvalidateExposedDataCache)
}
func AudioCompletionRatio2JSONString() string {
return audioCompletionRatioMap.MarshalJSONString()
}
func UpdateAudioCompletionRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonStringWithCallback(audioCompletionRatioMap, jsonStr, InvalidateExposedDataCache)
}
func GetModelRatioCopy() map[string]float64 {
return modelRatioMap.ReadAll()
}
func GetModelPriceCopy() map[string]float64 {
return modelPriceMap.ReadAll()
}
func GetCompletionRatioCopy() map[string]float64 {
return completionRatioMap.ReadAll()
}
// 转换模型名,减少渠道必须配置各种带参数模型
func FormatMatchingModelName(name string) string {
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")
} else if strings.HasPrefix(name, "gemini-2.5-flash") {
name = handleThinkingBudgetModel(name, "gemini-2.5-flash", "gemini-2.5-flash-thinking-*")
} else if strings.HasPrefix(name, "gemini-2.5-pro") {
name = handleThinkingBudgetModel(name, "gemini-2.5-pro", "gemini-2.5-pro-thinking-*")
}
if strings.HasPrefix(name, "gpt-4-gizmo") {
name = "gpt-4-gizmo-*"
}
if strings.HasPrefix(name, "gpt-4o-gizmo") {
name = "gpt-4o-gizmo-*"
}
return name
}
// result: 倍率or价格 usePrice exist
func GetModelRatioOrPrice(model string) (float64, bool, bool) { // price or ratio
price, usePrice := GetModelPrice(model, false)
if usePrice {
return price, true, true
}
modelRatio, success, _ := GetModelRatio(model)
if success {
return modelRatio, false, true
}
return 37.5, false, false
}

View File

@@ -0,0 +1,20 @@
package reasoning
import (
"strings"
"github.com/samber/lo"
)
var EffortSuffixes = []string{"-max", "-high", "-medium", "-low", "-minimal"}
// TrimEffortSuffix -> modelName level(low) exists
func TrimEffortSuffix(modelName string) (string, string, bool) {
suffix, found := lo.Find(EffortSuffixes, func(s string) bool {
return strings.HasSuffix(modelName, s)
})
if !found {
return modelName, "", false
}
return strings.TrimSuffix(modelName, suffix), strings.TrimPrefix(suffix, "-"), true
}

43
setting/sensitive.go Normal file
View File

@@ -0,0 +1,43 @@
package setting
import "strings"
var CheckSensitiveEnabled = true
var CheckSensitiveOnPromptEnabled = true
//var CheckSensitiveOnCompletionEnabled = true
// StopOnSensitiveEnabled 如果检测到敏感词,是否立刻停止生成,否则替换敏感词
var StopOnSensitiveEnabled = true
// StreamCacheQueueLength 流模式缓存队列长度0表示无缓存
var StreamCacheQueueLength = 0
// SensitiveWords 敏感词
// var SensitiveWords []string
var SensitiveWords = []string{
"test_sensitive",
}
func SensitiveWordsToString() string {
return strings.Join(SensitiveWords, "\n")
}
func SensitiveWordsFromString(s string) {
SensitiveWords = []string{}
sw := strings.Split(s, "\n")
for _, w := range sw {
w = strings.TrimSpace(w)
if w != "" {
SensitiveWords = append(SensitiveWords, w)
}
}
}
func ShouldCheckPromptSensitive() bool {
return CheckSensitiveEnabled && CheckSensitiveOnPromptEnabled
}
//func ShouldCheckCompletionSensitive() bool {
// return CheckSensitiveEnabled && CheckSensitiveOnCompletionEnabled
//}

View File

@@ -0,0 +1,49 @@
package setting
import (
"encoding/json"
"sync"
"github.com/QuantumNous/new-api/common"
)
var subscriptionGroups = map[string]string{}
var subscriptionGroupsMutex sync.RWMutex
func GetSubscriptionGroupsCopy() map[string]string {
subscriptionGroupsMutex.RLock()
defer subscriptionGroupsMutex.RUnlock()
copy := make(map[string]string, len(subscriptionGroups))
for k, v := range subscriptionGroups {
copy[k] = v
}
return copy
}
func SubscriptionGroups2JSONString() string {
subscriptionGroupsMutex.RLock()
defer subscriptionGroupsMutex.RUnlock()
jsonBytes, err := json.Marshal(subscriptionGroups)
if err != nil {
common.SysLog("error marshalling subscription groups: " + err.Error())
}
return string(jsonBytes)
}
func UpdateSubscriptionGroupsByJSONString(jsonStr string) error {
subscriptionGroupsMutex.Lock()
defer subscriptionGroupsMutex.Unlock()
subscriptionGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &subscriptionGroups)
}
func IsSubscriptionGroup(name string) bool {
subscriptionGroupsMutex.RLock()
defer subscriptionGroupsMutex.RUnlock()
_, ok := subscriptionGroups[name]
return ok
}

View File

@@ -0,0 +1,21 @@
package system_setting
import "github.com/QuantumNous/new-api/setting/config"
type DiscordSettings struct {
Enabled bool `json:"enabled"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
}
// 默认配置
var defaultDiscordSettings = DiscordSettings{}
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("discord", &defaultDiscordSettings)
}
func GetDiscordSettings() *DiscordSettings {
return &defaultDiscordSettings
}

View File

@@ -0,0 +1,34 @@
package system_setting
import "github.com/QuantumNous/new-api/setting/config"
type FetchSetting struct {
EnableSSRFProtection bool `json:"enable_ssrf_protection"` // 是否启用SSRF防护
AllowPrivateIp bool `json:"allow_private_ip"`
DomainFilterMode bool `json:"domain_filter_mode"` // 域名过滤模式true: 白名单模式false: 黑名单模式
IpFilterMode bool `json:"ip_filter_mode"` // IP过滤模式true: 白名单模式false: 黑名单模式
DomainList []string `json:"domain_list"` // domain format, e.g. example.com, *.example.com
IpList []string `json:"ip_list"` // CIDR format
AllowedPorts []string `json:"allowed_ports"` // port range format, e.g. 80, 443, 8000-9000
ApplyIPFilterForDomain bool `json:"apply_ip_filter_for_domain"` // 对域名启用IP过滤实验性
}
var defaultFetchSetting = FetchSetting{
EnableSSRFProtection: true, // 默认开启SSRF防护
AllowPrivateIp: false,
DomainFilterMode: false,
IpFilterMode: false,
DomainList: []string{},
IpList: []string{},
AllowedPorts: []string{"80", "443", "8080", "8443"},
ApplyIPFilterForDomain: false,
}
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("fetch_setting", &defaultFetchSetting)
}
func GetFetchSetting() *FetchSetting {
return &defaultFetchSetting
}

View File

@@ -0,0 +1,21 @@
package system_setting
import "github.com/QuantumNous/new-api/setting/config"
type LegalSettings struct {
UserAgreement string `json:"user_agreement"`
PrivacyPolicy string `json:"privacy_policy"`
}
var defaultLegalSettings = LegalSettings{
UserAgreement: "",
PrivacyPolicy: "",
}
func init() {
config.GlobalConfig.Register("legal", &defaultLegalSettings)
}
func GetLegalSettings() *LegalSettings {
return &defaultLegalSettings
}

View File

@@ -0,0 +1,25 @@
package system_setting
import "github.com/QuantumNous/new-api/setting/config"
type OIDCSettings struct {
Enabled bool `json:"enabled"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"`
WellKnown string `json:"well_known"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"user_info_endpoint"`
}
// 默认配置
var defaultOIDCSettings = OIDCSettings{}
func init() {
// 注册到全局配置管理器
config.GlobalConfig.Register("oidc", &defaultOIDCSettings)
}
func GetOIDCSettings() *OIDCSettings {
return &defaultOIDCSettings
}

View File

@@ -0,0 +1,50 @@
package system_setting
import (
"net/url"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/setting/config"
)
type PasskeySettings struct {
Enabled bool `json:"enabled"`
RPDisplayName string `json:"rp_display_name"`
RPID string `json:"rp_id"`
Origins string `json:"origins"`
AllowInsecureOrigin bool `json:"allow_insecure_origin"`
UserVerification string `json:"user_verification"`
AttachmentPreference string `json:"attachment_preference"`
}
var defaultPasskeySettings = PasskeySettings{
Enabled: false,
RPDisplayName: common.SystemName,
RPID: "",
Origins: "",
AllowInsecureOrigin: false,
UserVerification: "preferred",
AttachmentPreference: "",
}
func init() {
config.GlobalConfig.Register("passkey", &defaultPasskeySettings)
}
func GetPasskeySettings() *PasskeySettings {
if defaultPasskeySettings.RPID == "" && ServerAddress != "" {
// 从ServerAddress提取域名作为RPID
// ServerAddress可能是 "https://newapi.pro" 这种格式
serverAddr := strings.TrimSpace(ServerAddress)
if parsed, err := url.Parse(serverAddr); err == nil && parsed.Host != "" {
defaultPasskeySettings.RPID = parsed.Host
} else {
defaultPasskeySettings.RPID = serverAddr
}
}
if defaultPasskeySettings.Origins == "" || defaultPasskeySettings.Origins == "[]" {
defaultPasskeySettings.Origins = ServerAddress
}
return &defaultPasskeySettings
}

View File

@@ -0,0 +1,10 @@
package system_setting
var ServerAddress = "http://localhost:3000"
var WorkerUrl = ""
var WorkerValidKey = ""
var WorkerAllowHttpImageRequestEnabled = false
func EnableWorker() bool {
return WorkerUrl != ""
}

View File

@@ -0,0 +1,54 @@
package setting
import (
"encoding/json"
"sync"
"github.com/QuantumNous/new-api/common"
)
var userUsableGroups = map[string]string{
"default": "默认分组",
"vip": "vip分组",
}
var userUsableGroupsMutex sync.RWMutex
func GetUserUsableGroupsCopy() map[string]string {
userUsableGroupsMutex.RLock()
defer userUsableGroupsMutex.RUnlock()
copyUserUsableGroups := make(map[string]string)
for k, v := range userUsableGroups {
copyUserUsableGroups[k] = v
}
return copyUserUsableGroups
}
func UserUsableGroups2JSONString() string {
userUsableGroupsMutex.RLock()
defer userUsableGroupsMutex.RUnlock()
jsonBytes, err := json.Marshal(userUsableGroups)
if err != nil {
common.SysLog("error marshalling user groups: " + err.Error())
}
return string(jsonBytes)
}
func UpdateUserUsableGroupsByJSONString(jsonStr string) error {
userUsableGroupsMutex.Lock()
defer userUsableGroupsMutex.Unlock()
userUsableGroups = make(map[string]string)
return json.Unmarshal([]byte(jsonStr), &userUsableGroups)
}
func GetUsableGroupDescription(groupName string) string {
userUsableGroupsMutex.RLock()
defer userUsableGroupsMutex.RUnlock()
if desc, ok := userUsableGroups[groupName]; ok {
return desc
}
return groupName
}