初始化导入 new-api 源码
This commit is contained in:
37
setting/auto_group.go
Normal file
37
setting/auto_group.go
Normal 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
51
setting/chat.go
Normal 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
297
setting/config/config.go
Normal 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
|
||||
}
|
||||
39
setting/console_setting/config.go
Normal file
39
setting/console_setting/config.go
Normal 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
|
||||
}
|
||||
304
setting/console_setting/validation.go
Normal file
304
setting/console_setting/validation.go
Normal 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
7
setting/midjourney.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package setting
|
||||
|
||||
var MjNotifyEnabled = false
|
||||
var MjAccountFilterEnabled = false
|
||||
var MjModeClearEnabled = false
|
||||
var MjForwardUrlEnabled = true
|
||||
var MjActionCheckSuccessEnabled = true
|
||||
89
setting/model_setting/claude.go
Normal file
89
setting/model_setting/claude.go
Normal 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"]
|
||||
}
|
||||
76
setting/model_setting/gemini.go
Normal file
76
setting/model_setting/gemini.go
Normal 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
|
||||
}
|
||||
79
setting/model_setting/global.go
Normal file
79
setting/model_setting/global.go
Normal 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
|
||||
}
|
||||
24
setting/model_setting/grok.go
Normal file
24
setting/model_setting/grok.go
Normal 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
|
||||
}
|
||||
50
setting/model_setting/qwen.go
Normal file
50
setting/model_setting/qwen.go
Normal 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
|
||||
}
|
||||
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
|
||||
}
|
||||
6
setting/payment_creem.go
Normal file
6
setting/payment_creem.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package setting
|
||||
|
||||
var CreemApiKey = ""
|
||||
var CreemProducts = "[]"
|
||||
var CreemTestMode = false
|
||||
var CreemWebhookSecret = ""
|
||||
8
setting/payment_stripe.go
Normal file
8
setting/payment_stripe.go
Normal 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
67
setting/payment_waffo.go
Normal 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)
|
||||
}
|
||||
85
setting/performance_setting/config.go
Normal file
85
setting/performance_setting/config.go
Normal 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
69
setting/rate_limit.go
Normal 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
|
||||
}
|
||||
150
setting/ratio_setting/cache_ratio.go
Normal file
150
setting/ratio_setting/cache_ratio.go
Normal 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()
|
||||
}
|
||||
13
setting/ratio_setting/compact_suffix.go
Normal file
13
setting/ratio_setting/compact_suffix.go
Normal 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
|
||||
}
|
||||
17
setting/ratio_setting/expose_ratio.go
Normal file
17
setting/ratio_setting/expose_ratio.go
Normal 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()
|
||||
}
|
||||
56
setting/ratio_setting/exposed_cache.go
Normal file
56
setting/ratio_setting/exposed_cache.go
Normal 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)
|
||||
}
|
||||
125
setting/ratio_setting/group_ratio.go
Normal file
125
setting/ratio_setting/group_ratio.go
Normal 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
|
||||
}
|
||||
734
setting/ratio_setting/model_ratio.go
Normal file
734
setting/ratio_setting/model_ratio.go
Normal 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 返回模型的价格,如果模型不存在则返回-1,false
|
||||
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
|
||||
}
|
||||
20
setting/reasoning/suffix.go
Normal file
20
setting/reasoning/suffix.go
Normal 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
43
setting/sensitive.go
Normal 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
|
||||
//}
|
||||
49
setting/subscription_group_setting.go
Normal file
49
setting/subscription_group_setting.go
Normal 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
|
||||
}
|
||||
21
setting/system_setting/discord.go
Normal file
21
setting/system_setting/discord.go
Normal 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
|
||||
}
|
||||
34
setting/system_setting/fetch_setting.go
Normal file
34
setting/system_setting/fetch_setting.go
Normal 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
|
||||
}
|
||||
21
setting/system_setting/legal.go
Normal file
21
setting/system_setting/legal.go
Normal 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
|
||||
}
|
||||
25
setting/system_setting/oidc.go
Normal file
25
setting/system_setting/oidc.go
Normal 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
|
||||
}
|
||||
50
setting/system_setting/passkey.go
Normal file
50
setting/system_setting/passkey.go
Normal 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
|
||||
}
|
||||
10
setting/system_setting/system_setting_old.go
Normal file
10
setting/system_setting/system_setting_old.go
Normal 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 != ""
|
||||
}
|
||||
54
setting/user_usable_group.go
Normal file
54
setting/user_usable_group.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user