初始化导入 new-api 源码
This commit is contained in:
21
types/channel_error.go
Normal file
21
types/channel_error.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package types
|
||||
|
||||
type ChannelError struct {
|
||||
ChannelId int `json:"channel_id"`
|
||||
ChannelType int `json:"channel_type"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
IsMultiKey bool `json:"is_multi_key"`
|
||||
AutoBan bool `json:"auto_ban"`
|
||||
UsingKey string `json:"using_key"`
|
||||
}
|
||||
|
||||
func NewChannelError(channelId int, channelType int, channelName string, isMultiKey bool, usingKey string, autoBan bool) *ChannelError {
|
||||
return &ChannelError{
|
||||
ChannelId: channelId,
|
||||
ChannelType: channelType,
|
||||
ChannelName: channelName,
|
||||
IsMultiKey: isMultiKey,
|
||||
AutoBan: autoBan,
|
||||
UsingKey: usingKey,
|
||||
}
|
||||
}
|
||||
411
types/error.go
Normal file
411
types/error.go
Normal file
@@ -0,0 +1,411 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
type OpenAIError struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
Param string `json:"param"`
|
||||
Code any `json:"code"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type ClaudeError struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
ErrorTypeNewAPIError ErrorType = "new_api_error"
|
||||
ErrorTypeOpenAIError ErrorType = "openai_error"
|
||||
ErrorTypeClaudeError ErrorType = "claude_error"
|
||||
ErrorTypeMidjourneyError ErrorType = "midjourney_error"
|
||||
ErrorTypeGeminiError ErrorType = "gemini_error"
|
||||
ErrorTypeRerankError ErrorType = "rerank_error"
|
||||
ErrorTypeUpstreamError ErrorType = "upstream_error"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrorCodeInvalidRequest ErrorCode = "invalid_request"
|
||||
ErrorCodeSensitiveWordsDetected ErrorCode = "sensitive_words_detected"
|
||||
ErrorCodeViolationFeeGrokCSAM ErrorCode = "violation_fee.grok.csam"
|
||||
|
||||
// new api error
|
||||
ErrorCodeCountTokenFailed ErrorCode = "count_token_failed"
|
||||
ErrorCodeModelPriceError ErrorCode = "model_price_error"
|
||||
ErrorCodeInvalidApiType ErrorCode = "invalid_api_type"
|
||||
ErrorCodeJsonMarshalFailed ErrorCode = "json_marshal_failed"
|
||||
ErrorCodeDoRequestFailed ErrorCode = "do_request_failed"
|
||||
ErrorCodeGetChannelFailed ErrorCode = "get_channel_failed"
|
||||
ErrorCodeGenRelayInfoFailed ErrorCode = "gen_relay_info_failed"
|
||||
|
||||
// channel error
|
||||
ErrorCodeChannelNoAvailableKey ErrorCode = "channel:no_available_key"
|
||||
ErrorCodeChannelParamOverrideInvalid ErrorCode = "channel:param_override_invalid"
|
||||
ErrorCodeChannelHeaderOverrideInvalid ErrorCode = "channel:header_override_invalid"
|
||||
ErrorCodeChannelModelMappedError ErrorCode = "channel:model_mapped_error"
|
||||
ErrorCodeChannelAwsClientError ErrorCode = "channel:aws_client_error"
|
||||
ErrorCodeChannelInvalidKey ErrorCode = "channel:invalid_key"
|
||||
ErrorCodeChannelResponseTimeExceeded ErrorCode = "channel:response_time_exceeded"
|
||||
|
||||
// client request error
|
||||
ErrorCodeReadRequestBodyFailed ErrorCode = "read_request_body_failed"
|
||||
ErrorCodeConvertRequestFailed ErrorCode = "convert_request_failed"
|
||||
ErrorCodeAccessDenied ErrorCode = "access_denied"
|
||||
|
||||
// request error
|
||||
ErrorCodeBadRequestBody ErrorCode = "bad_request_body"
|
||||
|
||||
// response error
|
||||
ErrorCodeReadResponseBodyFailed ErrorCode = "read_response_body_failed"
|
||||
ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code"
|
||||
ErrorCodeBadResponse ErrorCode = "bad_response"
|
||||
ErrorCodeBadResponseBody ErrorCode = "bad_response_body"
|
||||
ErrorCodeEmptyResponse ErrorCode = "empty_response"
|
||||
ErrorCodeAwsInvokeError ErrorCode = "aws_invoke_error"
|
||||
ErrorCodeModelNotFound ErrorCode = "model_not_found"
|
||||
ErrorCodePromptBlocked ErrorCode = "prompt_blocked"
|
||||
|
||||
// sql error
|
||||
ErrorCodeQueryDataError ErrorCode = "query_data_error"
|
||||
ErrorCodeUpdateDataError ErrorCode = "update_data_error"
|
||||
|
||||
// quota error
|
||||
ErrorCodeInsufficientUserQuota ErrorCode = "insufficient_user_quota"
|
||||
ErrorCodePreConsumeTokenQuotaFailed ErrorCode = "pre_consume_token_quota_failed"
|
||||
)
|
||||
|
||||
type NewAPIError struct {
|
||||
Err error
|
||||
RelayError any
|
||||
skipRetry bool
|
||||
recordErrorLog *bool
|
||||
errorType ErrorType
|
||||
errorCode ErrorCode
|
||||
StatusCode int
|
||||
Metadata json.RawMessage
|
||||
}
|
||||
|
||||
// Unwrap enables errors.Is / errors.As to work with NewAPIError by exposing the underlying error.
|
||||
func (e *NewAPIError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func (e *NewAPIError) GetErrorCode() ErrorCode {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.errorCode
|
||||
}
|
||||
|
||||
func (e *NewAPIError) GetErrorType() ErrorType {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.errorType
|
||||
}
|
||||
|
||||
func (e *NewAPIError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Err == nil {
|
||||
// fallback message when underlying error is missing
|
||||
return string(e.errorCode)
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func (e *NewAPIError) ErrorWithStatusCode() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
msg := e.Error()
|
||||
if e.StatusCode == 0 {
|
||||
return msg
|
||||
}
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("status_code=%d", e.StatusCode)
|
||||
}
|
||||
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
|
||||
}
|
||||
|
||||
func (e *NewAPIError) MaskSensitiveError() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Err == nil {
|
||||
return string(e.errorCode)
|
||||
}
|
||||
errStr := e.Err.Error()
|
||||
if e.errorCode == ErrorCodeCountTokenFailed {
|
||||
return errStr
|
||||
}
|
||||
return common.MaskSensitiveInfo(errStr)
|
||||
}
|
||||
|
||||
func (e *NewAPIError) MaskSensitiveErrorWithStatusCode() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
msg := e.MaskSensitiveError()
|
||||
if e.StatusCode == 0 {
|
||||
return msg
|
||||
}
|
||||
if msg == "" {
|
||||
return fmt.Sprintf("status_code=%d", e.StatusCode)
|
||||
}
|
||||
return fmt.Sprintf("status_code=%d, %s", e.StatusCode, msg)
|
||||
}
|
||||
|
||||
func (e *NewAPIError) SetMessage(message string) {
|
||||
e.Err = errors.New(message)
|
||||
}
|
||||
|
||||
func (e *NewAPIError) ToOpenAIError() OpenAIError {
|
||||
var result OpenAIError
|
||||
switch e.errorType {
|
||||
case ErrorTypeOpenAIError:
|
||||
if openAIError, ok := e.RelayError.(OpenAIError); ok {
|
||||
result = openAIError
|
||||
}
|
||||
case ErrorTypeClaudeError:
|
||||
if claudeError, ok := e.RelayError.(ClaudeError); ok {
|
||||
result = OpenAIError{
|
||||
Message: e.Error(),
|
||||
Type: claudeError.Type,
|
||||
Param: "",
|
||||
Code: e.errorCode,
|
||||
}
|
||||
}
|
||||
default:
|
||||
result = OpenAIError{
|
||||
Message: e.Error(),
|
||||
Type: string(e.errorType),
|
||||
Param: "",
|
||||
Code: e.errorCode,
|
||||
}
|
||||
}
|
||||
if e.errorCode != ErrorCodeCountTokenFailed {
|
||||
result.Message = common.MaskSensitiveInfo(result.Message)
|
||||
}
|
||||
if result.Message == "" {
|
||||
result.Message = string(e.errorType)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *NewAPIError) ToClaudeError() ClaudeError {
|
||||
var result ClaudeError
|
||||
switch e.errorType {
|
||||
case ErrorTypeOpenAIError:
|
||||
if openAIError, ok := e.RelayError.(OpenAIError); ok {
|
||||
result = ClaudeError{
|
||||
Message: e.Error(),
|
||||
Type: fmt.Sprintf("%v", openAIError.Code),
|
||||
}
|
||||
}
|
||||
case ErrorTypeClaudeError:
|
||||
if claudeError, ok := e.RelayError.(ClaudeError); ok {
|
||||
result = claudeError
|
||||
}
|
||||
default:
|
||||
result = ClaudeError{
|
||||
Message: e.Error(),
|
||||
Type: string(e.errorType),
|
||||
}
|
||||
}
|
||||
if e.errorCode != ErrorCodeCountTokenFailed {
|
||||
result.Message = common.MaskSensitiveInfo(result.Message)
|
||||
}
|
||||
if result.Message == "" {
|
||||
result.Message = string(e.errorType)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type NewAPIErrorOptions func(*NewAPIError)
|
||||
|
||||
func NewError(err error, errorCode ErrorCode, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
var newErr *NewAPIError
|
||||
// 保留深层传递的 new err
|
||||
if errors.As(err, &newErr) {
|
||||
for _, op := range ops {
|
||||
op(newErr)
|
||||
}
|
||||
return newErr
|
||||
}
|
||||
e := &NewAPIError{
|
||||
Err: err,
|
||||
RelayError: nil,
|
||||
errorType: ErrorTypeNewAPIError,
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
errorCode: errorCode,
|
||||
}
|
||||
for _, op := range ops {
|
||||
op(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func NewOpenAIError(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
var newErr *NewAPIError
|
||||
// 保留深层传递的 new err
|
||||
if errors.As(err, &newErr) {
|
||||
if newErr.RelayError == nil {
|
||||
openaiError := OpenAIError{
|
||||
Message: newErr.Error(),
|
||||
Type: string(errorCode),
|
||||
Code: errorCode,
|
||||
}
|
||||
newErr.RelayError = openaiError
|
||||
}
|
||||
for _, op := range ops {
|
||||
op(newErr)
|
||||
}
|
||||
return newErr
|
||||
}
|
||||
openaiError := OpenAIError{
|
||||
Message: err.Error(),
|
||||
Type: string(errorCode),
|
||||
Code: errorCode,
|
||||
}
|
||||
return WithOpenAIError(openaiError, statusCode, ops...)
|
||||
}
|
||||
|
||||
func InitOpenAIError(errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
openaiError := OpenAIError{
|
||||
Type: string(errorCode),
|
||||
Code: errorCode,
|
||||
}
|
||||
return WithOpenAIError(openaiError, statusCode, ops...)
|
||||
}
|
||||
|
||||
func NewErrorWithStatusCode(err error, errorCode ErrorCode, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
e := &NewAPIError{
|
||||
Err: err,
|
||||
RelayError: OpenAIError{
|
||||
Message: err.Error(),
|
||||
Type: string(errorCode),
|
||||
},
|
||||
errorType: ErrorTypeNewAPIError,
|
||||
StatusCode: statusCode,
|
||||
errorCode: errorCode,
|
||||
}
|
||||
for _, op := range ops {
|
||||
op(e)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func WithOpenAIError(openAIError OpenAIError, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
code, ok := openAIError.Code.(string)
|
||||
if !ok {
|
||||
if openAIError.Code != nil {
|
||||
code = fmt.Sprintf("%v", openAIError.Code)
|
||||
} else {
|
||||
code = "unknown_error"
|
||||
}
|
||||
}
|
||||
if openAIError.Type == "" {
|
||||
openAIError.Type = "upstream_error"
|
||||
}
|
||||
e := &NewAPIError{
|
||||
RelayError: openAIError,
|
||||
errorType: ErrorTypeOpenAIError,
|
||||
StatusCode: statusCode,
|
||||
Err: errors.New(openAIError.Message),
|
||||
errorCode: ErrorCode(code),
|
||||
}
|
||||
// OpenRouter
|
||||
if len(openAIError.Metadata) > 0 {
|
||||
openAIError.Message = fmt.Sprintf("%s (%s)", openAIError.Message, openAIError.Metadata)
|
||||
e.Metadata = openAIError.Metadata
|
||||
e.RelayError = openAIError
|
||||
e.Err = errors.New(openAIError.Message)
|
||||
}
|
||||
for _, op := range ops {
|
||||
op(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func WithClaudeError(claudeError ClaudeError, statusCode int, ops ...NewAPIErrorOptions) *NewAPIError {
|
||||
if claudeError.Type == "" {
|
||||
claudeError.Type = "upstream_error"
|
||||
}
|
||||
e := &NewAPIError{
|
||||
RelayError: claudeError,
|
||||
errorType: ErrorTypeClaudeError,
|
||||
StatusCode: statusCode,
|
||||
Err: errors.New(claudeError.Message),
|
||||
errorCode: ErrorCode(claudeError.Type),
|
||||
}
|
||||
for _, op := range ops {
|
||||
op(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func IsChannelError(err *NewAPIError) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(string(err.errorCode), "channel:")
|
||||
}
|
||||
|
||||
func IsSkipRetryError(err *NewAPIError) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return err.skipRetry
|
||||
}
|
||||
|
||||
func ErrOptionWithSkipRetry() NewAPIErrorOptions {
|
||||
return func(e *NewAPIError) {
|
||||
e.skipRetry = true
|
||||
}
|
||||
}
|
||||
|
||||
func ErrOptionWithNoRecordErrorLog() NewAPIErrorOptions {
|
||||
return func(e *NewAPIError) {
|
||||
e.recordErrorLog = common.GetPointer(false)
|
||||
}
|
||||
}
|
||||
|
||||
func ErrOptionWithHideErrMsg(replaceStr string) NewAPIErrorOptions {
|
||||
return func(e *NewAPIError) {
|
||||
if common.DebugEnabled {
|
||||
fmt.Printf("ErrOptionWithHideErrMsg: %s, origin error: %s", replaceStr, e.Err)
|
||||
}
|
||||
e.Err = errors.New(replaceStr)
|
||||
}
|
||||
}
|
||||
|
||||
func IsRecordErrorLog(e *NewAPIError) bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
if e.recordErrorLog == nil {
|
||||
// default to true if not set
|
||||
return true
|
||||
}
|
||||
return *e.recordErrorLog
|
||||
}
|
||||
8
types/file_data.go
Normal file
8
types/file_data.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package types
|
||||
|
||||
type LocalFileData struct {
|
||||
MimeType string
|
||||
Base64Data string
|
||||
Url string
|
||||
Size int64
|
||||
}
|
||||
231
types/file_source.go
Normal file
231
types/file_source.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FileSourceType 文件来源类型
|
||||
type FileSourceType string
|
||||
|
||||
const (
|
||||
FileSourceTypeURL FileSourceType = "url" // URL 来源
|
||||
FileSourceTypeBase64 FileSourceType = "base64" // Base64 内联数据
|
||||
)
|
||||
|
||||
// FileSource 统一的文件来源抽象
|
||||
// 支持 URL 和 base64 两种来源,提供懒加载和缓存机制
|
||||
type FileSource struct {
|
||||
Type FileSourceType `json:"type"` // 来源类型
|
||||
URL string `json:"url,omitempty"` // URL(当 Type 为 url 时)
|
||||
Base64Data string `json:"base64_data,omitempty"` // Base64 数据(当 Type 为 base64 时)
|
||||
MimeType string `json:"mime_type,omitempty"` // MIME 类型(可选,会自动检测)
|
||||
|
||||
// 内部缓存(不导出,不序列化)
|
||||
cachedData *CachedFileData
|
||||
cacheLoaded bool
|
||||
registered bool // 是否已注册到清理列表
|
||||
mu sync.Mutex // 保护加载过程
|
||||
}
|
||||
|
||||
// Mu 获取内部锁
|
||||
func (f *FileSource) Mu() *sync.Mutex {
|
||||
return &f.mu
|
||||
}
|
||||
|
||||
// CachedFileData 缓存的文件数据
|
||||
// 支持内存缓存和磁盘缓存两种模式
|
||||
type CachedFileData struct {
|
||||
base64Data string // 内存中的 base64 数据(小文件)
|
||||
MimeType string // MIME 类型
|
||||
Size int64 // 文件大小(字节)
|
||||
DiskSize int64 // 磁盘缓存实际占用大小(字节,通常是 base64 长度)
|
||||
ImageConfig *image.Config // 图片配置(如果是图片)
|
||||
ImageFormat string // 图片格式(如果是图片)
|
||||
|
||||
// 磁盘缓存相关
|
||||
diskPath string // 磁盘缓存文件路径(大文件)
|
||||
isDisk bool // 是否使用磁盘缓存
|
||||
diskMu sync.Mutex // 磁盘操作锁(保护磁盘文件的读取和删除)
|
||||
diskClosed bool // 是否已关闭/清理
|
||||
statDecremented bool // 是否已扣减统计
|
||||
|
||||
// 统计回调,避免循环依赖
|
||||
OnClose func(size int64)
|
||||
}
|
||||
|
||||
// NewMemoryCachedData 创建内存缓存的数据
|
||||
func NewMemoryCachedData(base64Data string, mimeType string, size int64) *CachedFileData {
|
||||
return &CachedFileData{
|
||||
base64Data: base64Data,
|
||||
MimeType: mimeType,
|
||||
Size: size,
|
||||
isDisk: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDiskCachedData 创建磁盘缓存的数据
|
||||
func NewDiskCachedData(diskPath string, mimeType string, size int64) *CachedFileData {
|
||||
return &CachedFileData{
|
||||
diskPath: diskPath,
|
||||
MimeType: mimeType,
|
||||
Size: size,
|
||||
isDisk: true,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBase64Data 获取 base64 数据(自动处理内存/磁盘)
|
||||
func (c *CachedFileData) GetBase64Data() (string, error) {
|
||||
if !c.isDisk {
|
||||
return c.base64Data, nil
|
||||
}
|
||||
|
||||
c.diskMu.Lock()
|
||||
defer c.diskMu.Unlock()
|
||||
|
||||
if c.diskClosed {
|
||||
return "", fmt.Errorf("disk cache already closed")
|
||||
}
|
||||
|
||||
// 从磁盘读取
|
||||
data, err := os.ReadFile(c.diskPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read from disk cache: %w", err)
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// SetBase64Data 设置 base64 数据(仅用于内存模式)
|
||||
func (c *CachedFileData) SetBase64Data(data string) {
|
||||
if !c.isDisk {
|
||||
c.base64Data = data
|
||||
}
|
||||
}
|
||||
|
||||
// IsDisk 是否使用磁盘缓存
|
||||
func (c *CachedFileData) IsDisk() bool {
|
||||
return c.isDisk
|
||||
}
|
||||
|
||||
// Close 关闭并清理资源
|
||||
func (c *CachedFileData) Close() error {
|
||||
if !c.isDisk {
|
||||
c.base64Data = "" // 释放内存
|
||||
return nil
|
||||
}
|
||||
|
||||
c.diskMu.Lock()
|
||||
defer c.diskMu.Unlock()
|
||||
|
||||
if c.diskClosed {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.diskClosed = true
|
||||
if c.diskPath != "" {
|
||||
err := os.Remove(c.diskPath)
|
||||
// 只有在删除成功且未扣减过统计时,才执行回调
|
||||
if err == nil && !c.statDecremented && c.OnClose != nil {
|
||||
c.OnClose(c.DiskSize)
|
||||
c.statDecremented = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewURLFileSource 创建 URL 来源的 FileSource
|
||||
func NewURLFileSource(url string) *FileSource {
|
||||
return &FileSource{
|
||||
Type: FileSourceTypeURL,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBase64FileSource 创建 base64 来源的 FileSource
|
||||
func NewBase64FileSource(base64Data string, mimeType string) *FileSource {
|
||||
return &FileSource{
|
||||
Type: FileSourceTypeBase64,
|
||||
Base64Data: base64Data,
|
||||
MimeType: mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
// IsURL 判断是否是 URL 来源
|
||||
func (f *FileSource) IsURL() bool {
|
||||
return f.Type == FileSourceTypeURL
|
||||
}
|
||||
|
||||
// IsBase64 判断是否是 base64 来源
|
||||
func (f *FileSource) IsBase64() bool {
|
||||
return f.Type == FileSourceTypeBase64
|
||||
}
|
||||
|
||||
// GetIdentifier 获取文件标识符(用于日志和错误追踪)
|
||||
func (f *FileSource) GetIdentifier() string {
|
||||
if f.IsURL() {
|
||||
if len(f.URL) > 100 {
|
||||
return f.URL[:100] + "..."
|
||||
}
|
||||
return f.URL
|
||||
}
|
||||
if len(f.Base64Data) > 50 {
|
||||
return "base64:" + f.Base64Data[:50] + "..."
|
||||
}
|
||||
return "base64:" + f.Base64Data
|
||||
}
|
||||
|
||||
// GetRawData 获取原始数据(URL 或完整的 base64 字符串)
|
||||
func (f *FileSource) GetRawData() string {
|
||||
if f.IsURL() {
|
||||
return f.URL
|
||||
}
|
||||
return f.Base64Data
|
||||
}
|
||||
|
||||
// SetCache 设置缓存数据
|
||||
func (f *FileSource) SetCache(data *CachedFileData) {
|
||||
f.cachedData = data
|
||||
f.cacheLoaded = true
|
||||
}
|
||||
|
||||
// IsRegistered 是否已注册到清理列表
|
||||
func (f *FileSource) IsRegistered() bool {
|
||||
return f.registered
|
||||
}
|
||||
|
||||
// SetRegistered 设置注册状态
|
||||
func (f *FileSource) SetRegistered(registered bool) {
|
||||
f.registered = registered
|
||||
}
|
||||
|
||||
// GetCache 获取缓存数据
|
||||
func (f *FileSource) GetCache() *CachedFileData {
|
||||
return f.cachedData
|
||||
}
|
||||
|
||||
// HasCache 是否有缓存
|
||||
func (f *FileSource) HasCache() bool {
|
||||
return f.cacheLoaded && f.cachedData != nil
|
||||
}
|
||||
|
||||
// ClearCache 清除缓存,释放内存和磁盘文件
|
||||
func (f *FileSource) ClearCache() {
|
||||
// 如果有缓存数据,先关闭它(会清理磁盘文件)
|
||||
if f.cachedData != nil {
|
||||
f.cachedData.Close()
|
||||
}
|
||||
f.cachedData = nil
|
||||
f.cacheLoaded = false
|
||||
}
|
||||
|
||||
// ClearRawData 清除原始数据,只保留必要的元信息
|
||||
// 用于在处理完成后释放大文件的内存
|
||||
func (f *FileSource) ClearRawData() {
|
||||
// 保留 URL(通常很短),只清除大的 base64 数据
|
||||
if f.IsBase64() && len(f.Base64Data) > 1024 {
|
||||
f.Base64Data = ""
|
||||
}
|
||||
}
|
||||
42
types/price_data.go
Normal file
42
types/price_data.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GroupRatioInfo struct {
|
||||
GroupRatio float64
|
||||
GroupSpecialRatio float64
|
||||
HasSpecialRatio bool
|
||||
}
|
||||
|
||||
type PriceData struct {
|
||||
FreeModel bool
|
||||
ModelPrice float64
|
||||
ModelRatio float64
|
||||
CompletionRatio float64
|
||||
CacheRatio float64
|
||||
CacheCreationRatio float64
|
||||
CacheCreation5mRatio float64
|
||||
CacheCreation1hRatio float64
|
||||
ImageRatio float64
|
||||
AudioRatio float64
|
||||
AudioCompletionRatio float64
|
||||
OtherRatios map[string]float64
|
||||
UsePrice bool
|
||||
Quota int // 按次计费的最终额度(MJ / Task)
|
||||
QuotaToPreConsume int // 按量计费的预消耗额度
|
||||
GroupRatioInfo GroupRatioInfo
|
||||
}
|
||||
|
||||
func (p *PriceData) AddOtherRatio(key string, ratio float64) {
|
||||
if p.OtherRatios == nil {
|
||||
p.OtherRatios = make(map[string]float64)
|
||||
}
|
||||
if ratio <= 0 {
|
||||
return
|
||||
}
|
||||
p.OtherRatios[key] = ratio
|
||||
}
|
||||
|
||||
func (p *PriceData) ToSetting() string {
|
||||
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
|
||||
}
|
||||
19
types/relay_format.go
Normal file
19
types/relay_format.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package types
|
||||
|
||||
type RelayFormat string
|
||||
|
||||
const (
|
||||
RelayFormatOpenAI RelayFormat = "openai"
|
||||
RelayFormatClaude = "claude"
|
||||
RelayFormatGemini = "gemini"
|
||||
RelayFormatOpenAIResponses = "openai_responses"
|
||||
RelayFormatOpenAIResponsesCompaction = "openai_responses_compaction"
|
||||
RelayFormatOpenAIAudio = "openai_audio"
|
||||
RelayFormatOpenAIImage = "openai_image"
|
||||
RelayFormatOpenAIRealtime = "openai_realtime"
|
||||
RelayFormatRerank = "rerank"
|
||||
RelayFormatEmbedding = "embedding"
|
||||
|
||||
RelayFormatTask = "task"
|
||||
RelayFormatMjProxy = "mj_proxy"
|
||||
)
|
||||
84
types/request_meta.go
Normal file
84
types/request_meta.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package types
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
FileTypeImage FileType = "image" // Image file type
|
||||
FileTypeAudio FileType = "audio" // Audio file type
|
||||
FileTypeVideo FileType = "video" // Video file type
|
||||
FileTypeFile FileType = "file" // Generic file type
|
||||
)
|
||||
|
||||
type TokenType string
|
||||
|
||||
const (
|
||||
TokenTypeTextNumber TokenType = "text_number" // Text or number tokens
|
||||
TokenTypeTokenizer TokenType = "tokenizer" // Tokenizer tokens
|
||||
TokenTypeImage TokenType = "image" // Image tokens
|
||||
)
|
||||
|
||||
type TokenCountMeta struct {
|
||||
TokenType TokenType `json:"token_type,omitempty"` // Type of tokens used in the request
|
||||
CombineText string `json:"combine_text,omitempty"` // Combined text from all messages
|
||||
ToolsCount int `json:"tools_count,omitempty"` // Number of tools used
|
||||
NameCount int `json:"name_count,omitempty"` // Number of names in the request
|
||||
MessagesCount int `json:"messages_count,omitempty"` // Number of messages in the request
|
||||
Files []*FileMeta `json:"files,omitempty"` // List of files, each with type and content
|
||||
MaxTokens int `json:"max_tokens,omitempty"` // Maximum tokens allowed in the request
|
||||
|
||||
ImagePriceRatio float64 `json:"image_ratio,omitempty"` // Ratio for image size, if applicable
|
||||
//IsStreaming bool `json:"is_streaming,omitempty"` // Indicates if the request is streaming
|
||||
}
|
||||
|
||||
type FileMeta struct {
|
||||
FileType
|
||||
MimeType string
|
||||
Source *FileSource // 统一的文件来源(URL 或 base64)
|
||||
Detail string // 图片细节级别(low/high/auto)
|
||||
}
|
||||
|
||||
// NewFileMeta 创建新的 FileMeta
|
||||
func NewFileMeta(fileType FileType, source *FileSource) *FileMeta {
|
||||
return &FileMeta{
|
||||
FileType: fileType,
|
||||
Source: source,
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageFileMeta 创建图片类型的 FileMeta
|
||||
func NewImageFileMeta(source *FileSource, detail string) *FileMeta {
|
||||
return &FileMeta{
|
||||
FileType: FileTypeImage,
|
||||
Source: source,
|
||||
Detail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
// GetIdentifier 获取文件标识符(用于日志)
|
||||
func (f *FileMeta) GetIdentifier() string {
|
||||
if f.Source != nil {
|
||||
return f.Source.GetIdentifier()
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// IsURL 判断是否是 URL 来源
|
||||
func (f *FileMeta) IsURL() bool {
|
||||
return f.Source != nil && f.Source.IsURL()
|
||||
}
|
||||
|
||||
// GetRawData 获取原始数据(兼容旧代码)
|
||||
// Deprecated: 请使用 Source.GetRawData()
|
||||
func (f *FileMeta) GetRawData() string {
|
||||
if f.Source != nil {
|
||||
return f.Source.GetRawData()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RequestMeta struct {
|
||||
OriginalModelName string `json:"original_model_name"`
|
||||
UserUsingGroup string `json:"user_using_group"`
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
PreConsumedQuota int `json:"pre_consumed_quota"`
|
||||
}
|
||||
103
types/rw_map.go
Normal file
103
types/rw_map.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
type RWMap[K comparable, V any] struct {
|
||||
data map[K]V
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) UnmarshalJSON(b []byte) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
return common.Unmarshal(b, &m.data)
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) MarshalJSON() ([]byte, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return common.Marshal(m.data)
|
||||
}
|
||||
|
||||
func NewRWMap[K comparable, V any]() *RWMap[K, V] {
|
||||
return &RWMap[K, V]{
|
||||
data: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Get(key K) (V, bool) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
value, exists := m.data[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Set(key K, value V) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data[key] = value
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) AddAll(other map[K]V) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
for k, v := range other {
|
||||
m.data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Clear() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
}
|
||||
|
||||
// ReadAll returns a copy of the entire map.
|
||||
func (m *RWMap[K, V]) ReadAll() map[K]V {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
copiedMap := make(map[K]V)
|
||||
for k, v := range m.data {
|
||||
copiedMap[k] = v
|
||||
}
|
||||
return copiedMap
|
||||
}
|
||||
|
||||
func (m *RWMap[K, V]) Len() int {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return len(m.data)
|
||||
}
|
||||
|
||||
func LoadFromJsonString[K comparable, V any](m *RWMap[K, V], jsonStr string) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
return common.Unmarshal([]byte(jsonStr), &m.data)
|
||||
}
|
||||
|
||||
// LoadFromJsonStringWithCallback loads a JSON string into the RWMap and calls the callback on success.
|
||||
func LoadFromJsonStringWithCallback[K comparable, V any](m *RWMap[K, V], jsonStr string, onSuccess func()) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
m.data = make(map[K]V)
|
||||
err := common.Unmarshal([]byte(jsonStr), &m.data)
|
||||
if err == nil && onSuccess != nil {
|
||||
onSuccess()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalJSONString returns the JSON string representation of the RWMap.
|
||||
func (m *RWMap[K, V]) MarshalJSONString() string {
|
||||
bytes, err := m.MarshalJSON()
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
42
types/set.go
Normal file
42
types/set.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package types
|
||||
|
||||
type Set[T comparable] struct {
|
||||
items map[T]struct{}
|
||||
}
|
||||
|
||||
// NewSet 创建并返回一个新的 Set
|
||||
func NewSet[T comparable]() *Set[T] {
|
||||
return &Set[T]{
|
||||
items: make(map[T]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set[T]) Add(item T) {
|
||||
s.items[item] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove 从 Set 中移除一个元素
|
||||
func (s *Set[T]) Remove(item T) {
|
||||
delete(s.items, item)
|
||||
}
|
||||
|
||||
// Contains 检查 Set 是否包含某个元素
|
||||
func (s *Set[T]) Contains(item T) bool {
|
||||
_, exists := s.items[item]
|
||||
return exists
|
||||
}
|
||||
|
||||
// Len 返回 Set 中元素的数量
|
||||
func (s *Set[T]) Len() int {
|
||||
return len(s.items)
|
||||
}
|
||||
|
||||
// Items 返回 Set 中所有元素组成的切片
|
||||
// 注意:由于 map 的无序性,返回的切片元素顺序是随机的
|
||||
func (s *Set[T]) Items() []T {
|
||||
items := make([]T, 0, s.Len())
|
||||
for item := range s.items {
|
||||
items = append(items, item)
|
||||
}
|
||||
return items
|
||||
}
|
||||
Reference in New Issue
Block a user