diff --git a/.gitignore b/.gitignore index 9769aef1d042..6e3b5b7ab521 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ script/ *_test.go out.log out.log_2 -.env_3 \ No newline at end of file +.env_3 +*.prof +.env* \ No newline at end of file diff --git a/common/constants.go b/common/constants.go index 669fd93b10b8..e01593b0cd54 100644 --- a/common/constants.go +++ b/common/constants.go @@ -239,6 +239,7 @@ const ( ChannelTypeVolcEngine = 45 ChannelTypeBaiduV2 = 46 ChannelTypeXai = 47 + ChannelTypeDoubaoOffline = 100 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -293,4 +294,56 @@ var ChannelBaseURLs = []string{ "https://qianfan.baidubce.com", //46 "", //47 "https://api.x.ai", //48 + "", //49 + "", //50 + "", //51 + "", //52 + "", //53 + "", //54 + "", //55 + "", //56 + "", //57 + "", //58 + "", //59 + "", //60 + "", //61 + "", //62 + "", //63 + "", //64 + "", //65 + "", //66 + "", //67 + "", //68 + "", //69 + "", //70 + "", //71 + "", //72 + "", //73 + "", //74 + "", //75 + "", //76 + "", //77 + "", //78 + "", //79 + "", //80 + "", //81 + "", //82 + "", //83 + "", //84 + "", //85 + "", //86 + "", //87 + "", //88 + "", //89 + "", //90 + "", //91 + "", //92 + "", //93 + "", //94 + "", //95 + "", //96 + "", //97 + "", //98 + "", //99 + "https://ark.cn-beijing.volces.com", //100 - 豆包离线 } diff --git a/common/logger.go b/common/logger.go index 123efb158dc1..735a3f8e38c6 100644 --- a/common/logger.go +++ b/common/logger.go @@ -10,9 +10,12 @@ import ( "path/filepath" "runtime" "sort" + "strings" "sync" "time" + "one-api/metrics" + "github.com/bytedance/gopkg/util/gopool" "github.com/gin-gonic/gin" ) @@ -35,6 +38,20 @@ const ( maxLogCount = 1000000 ) +// 错误类型常量 +const ( + ErrorTypeOther = "other" + ErrorTypeParameter = "parameter_error" + ErrorTypeNoCandidates = "no_candidates" + ErrorTypeRequestFailed = "request_failed" + ErrorTypeBadGateway = "bad_gateway" + ErrorTypeResponseFailed = "response_failed" + ErrorTypeConnectionTimeout = "connection_timeout" + ErrorTypeTokenUnavailable = "token_unavailable" + ErrorTypeBadRequest = "bad_request" + ErrorTypeNoAvailableChannel = "no_available_channel" +) + var logCount int var setupLogLock sync.Mutex var setupLogWorking bool @@ -184,6 +201,42 @@ func LogError(ctx context.Context, msg string) { logHelper(ctx, loggerError, msg) } +// 获取错误类型 +func getErrorType(msg string) (string, string) { + // 提取错误码(如果有) + errorCode := "unknown" + if strings.Contains(msg, "status code:") { + parts := strings.Split(msg, "status code:") + if len(parts) > 1 { + errorCode = strings.TrimSpace(parts[1]) + } + } + + // 根据错误消息内容判断错误类型 + switch { + case strings.Contains(msg, "One or more parameter"): + return ErrorTypeParameter, errorCode + case strings.Contains(msg, "No candidates"): + return ErrorTypeNoCandidates, errorCode + case strings.Contains(msg, "do request failed"): + return ErrorTypeRequestFailed, errorCode + case strings.Contains(msg, "status code: 502"): + return ErrorTypeBadGateway, errorCode + case strings.Contains(msg, "doResponse failed"): + return ErrorTypeResponseFailed, errorCode + case strings.Contains(msg, "write: connection timed out"): + return ErrorTypeConnectionTimeout, errorCode + case strings.Contains(msg, "该令牌状态不可用"): + return ErrorTypeTokenUnavailable, errorCode + case strings.Contains(msg, "bad response status code 400"): + return ErrorTypeBadRequest, errorCode + case strings.Contains(msg, "无可用渠道"): + return ErrorTypeNoAvailableChannel, errorCode + default: + return ErrorTypeOther, errorCode + } +} + func logHelper(ctx context.Context, level string, msg string) { // 获取请求ID var requestId string @@ -209,6 +262,38 @@ func logHelper(ctx context.Context, level string, msg string) { now := time.Now() caller := getCallerInfo() _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), requestId, caller, msg) + + // 如果是错误日志,增加错误计数 + if level == loggerError { + errorType, errorCode := getErrorType(msg) + // 从上下文中获取相关信息 + channel := "unknown" + channelName := "unknown" + model := "unknown" + group := "unknown" + tokenName := "unknown" + + if ginCtx, ok := ctx.Value("gin_context").(*gin.Context); ok { + if ch := ginCtx.GetString("channel"); ch != "" { + channel = ch + } + if chName := ginCtx.GetString("channel_name"); chName != "" { + channelName = chName + } + if m := ginCtx.GetString("model"); m != "" { + model = m + } + if g := ginCtx.GetString("group"); g != "" { + group = g + } + if tn := ginCtx.GetString("token_name"); tn != "" { + tokenName = tn + } + } + + metrics.IncrementErrorLog(channel, channelName, errorCode, errorType, model, group, tokenName, 1.0) + } + logCount++ // we don't need accurate count, so no lock here if logCount > maxLogCount && !setupLogWorking { logCount = 0 diff --git a/controller/misc.go b/controller/misc.go index a451b5e3faa5..e5588edbe4ef 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -270,3 +270,13 @@ func ResetPassword(c *gin.Context) { }) return } + +func Ping(c *gin.Context) { + c.Writer.Header().Set("Retry_request_id", "Retry_request_id") + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "pong", + "timestamp": common.GetTimestamp(), + }) + return +} diff --git a/controller/relay.go b/controller/relay.go index 6031ba4e2dae..4a4f88eab75a 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -4,8 +4,6 @@ import ( "bytes" "errors" "fmt" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" "io" "log" "net/http" @@ -23,6 +21,9 @@ import ( "strconv" "strings" "time" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" ) func relayInfoHandler(c *gin.Context, relayMode int) (*relaycommon.RelayInfo, interface{}, string, *dto.OpenAIErrorWithStatusCode) { @@ -121,6 +122,9 @@ func Relay(c *gin.Context) { openaiErr = service.OpenAIErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError) break } + // 设置 channel 信息到上下文 + c.Set("channel", strconv.Itoa(channel.Id)) + c.Set("channel_name", channel.Name) fillRelayRequest(c, channel) var ( relayInfo *relaycommon.RelayInfo @@ -130,20 +134,20 @@ func Relay(c *gin.Context) { relayInfo, request, requestModel, openaiErr = relayInfoHandler(c, relayMode) if i == 0 { // e2e 用户请求计数 - metrics.IncrementRelayRequestE2ETotalCounter(strconv.Itoa(channel.Id), requestModel, group, tokenKey, tokenName, 1) + metrics.IncrementRelayRequestE2ETotalCounter(strconv.Itoa(channel.Id), channel.Name, requestModel, group, tokenKey, tokenName, 1) } else { // 重试计数 channelTag := "" if channel.Tag != nil { channelTag = *channel.Tag } - metrics.IncrementRelayRetryCounter(strconv.Itoa(channel.Id), channelTag, channel.GetBaseURL(), requestModel, group, 1) + metrics.IncrementRelayRetryCounter(strconv.Itoa(channel.Id), channel.Name, channelTag, channel.GetBaseURL(), requestModel, group, 1) } if openaiErr == nil { openaiErr = executeRelayRequest(c, relayMode, relayInfo, request) if openaiErr == nil { - metrics.IncrementRelayRequestE2ESuccessCounter(strconv.Itoa(channel.Id), requestModel, group, tokenKey, tokenName, 1) - metrics.ObserveRelayRequestE2EDuration(strconv.Itoa(channel.Id), requestModel, group, tokenKey, tokenName, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestE2ESuccessCounter(strconv.Itoa(channel.Id), channel.Name, requestModel, group, tokenKey, tokenName, 1) + metrics.ObserveRelayRequestE2EDuration(strconv.Itoa(channel.Id), channel.Name, requestModel, group, tokenKey, tokenName, time.Since(startTime).Seconds()) return } } @@ -152,7 +156,7 @@ func Relay(c *gin.Context) { if !shouldRetry(c, openaiErr, common.RetryTimes-i) { // e2e 失败计数 - metrics.IncrementRelayRequestE2EFailedCounter(strconv.Itoa(channel.Id), requestModel, group, strconv.Itoa(openaiErr.StatusCode), tokenKey, tokenName, 1) + metrics.IncrementRelayRequestE2EFailedCounter(strconv.Itoa(channel.Id), channel.Name, requestModel, group, strconv.Itoa(openaiErr.StatusCode), tokenKey, tokenName, 1) break } } @@ -167,6 +171,33 @@ func Relay(c *gin.Context) { common.LogError(c, fmt.Sprintf("origin 429 error: %s", openaiErr.Error.Message)) openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试" } + + // 处理自定义的 NewAPI batch 错误码 + if openaiErr.StatusCode == dto.StatusNewAPIBatchRateLimitExceeded { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "当前服务端限速已满,请稍后再试" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchTimeout { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "未等待到结果,请稍后使用Retry_request_id再次查询" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchInternal { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "服务内部错误,请稍后再试" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchSubmitted { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "批量请求已提交,但是结果还未出来,请使用Retry_request_id查询结果" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchAccepted { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "批量请求已接受,正在处理中,请稍后使用Retry_request_id查询结果" + } + if openaiErr.StatusCode == dto.StatusRequestConflict { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "请求冲突,有其他请求使用了这个Retry_request_id,请稍后再试" + } + openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId) c.JSON(openaiErr.StatusCode, gin.H{ "error": openaiErr.Error, @@ -230,6 +261,31 @@ func WssRelay(c *gin.Context) { if openaiErr.StatusCode == http.StatusTooManyRequests { openaiErr.Error.Message = "当前分组上游负载已饱和,请稍后再试" } + // 处理自定义的 NewAPI batch 错误码 + if openaiErr.StatusCode == dto.StatusNewAPIBatchRateLimitExceeded { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "当前服务端限速已满,请稍后再试" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchTimeout { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "未等待到结果,请稍后使用Retry_request_id再次查询" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchInternal { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "服务内部错误,请稍后再试" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchSubmitted { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "批量请求已提交,但是结果还未出来,请使用Retry_request_id查询结果" + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchAccepted { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "批量请求已接受,正在处理中,请稍后使用Retry_request_id查询结果" + } + if openaiErr.StatusCode == dto.StatusRequestConflict { + common.LogError(c, fmt.Sprintf("origin %d error: %s", openaiErr.StatusCode, openaiErr.Error.Message)) + openaiErr.Error.Message = "请求冲突,有其他请求使用了这个Retry_request_id,请稍后再试" + } openaiErr.Error.Message = common.MessageWithRequestId(openaiErr.Error.Message, requestId) helper.WssError(c, ws, openaiErr.Error) } @@ -298,6 +354,26 @@ func shouldRetry(c *gin.Context, openaiErr *dto.OpenAIErrorWithStatusCode, retry if openaiErr.StatusCode == http.StatusTooManyRequests { return true } + // 处理自定义的 NewAPI batch 错误码 + if openaiErr.StatusCode == dto.StatusNewAPIBatchRateLimitExceeded { + return false + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchTimeout { + return false + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchInternal { + return false + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchSubmitted { + return false + } + if openaiErr.StatusCode == dto.StatusNewAPIBatchAccepted { + return false + } + if openaiErr.StatusCode == dto.StatusRequestConflict { + return false + } + if openaiErr.StatusCode == 307 { return true } diff --git a/dto/error.go b/dto/error.go index b347f6a159ef..eaee8ac52097 100644 --- a/dto/error.go +++ b/dto/error.go @@ -53,3 +53,13 @@ func (e GeneralErrorResponse) ToMessage() string { } return "" } + +// 自定义HTTP状态码 (使用非标准状态码范围) +const ( + StatusNewAPIBatchRateLimitExceeded = 499 // 自定义限流状态码 + StatusNewAPIBatchTimeout = 598 // 自定义超时状态码 + StatusNewAPIBatchInternal = 599 // 自定义内部错误状态码 + StatusNewAPIBatchSubmitted = 203 // 批量请求已提交,需要重试获取结果 + StatusNewAPIBatchAccepted = 202 // 批量请求已接受,正在处理中 + StatusRequestConflict = 409 // 请求冲突,如分布式锁获取失败 +) diff --git a/dto/openai_request.go b/dto/openai_request.go index 2170e55d7c61..71dd9ad1833e 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -2,7 +2,10 @@ package dto import ( "encoding/json" + "fmt" "strings" + + "one-api/common" ) type ResponseFormat struct { @@ -42,6 +45,7 @@ type GeneralOpenAIRequest struct { ResponseFormat *ResponseFormat `json:"response_format,omitempty"` EncodingFormat any `json:"encoding_format,omitempty"` Seed float64 `json:"seed,omitempty"` + LogitBias map[string]int `json:"logit_bias,omitempty"` Tools []ToolCallRequest `json:"tools,omitempty"` ToolChoice any `json:"tool_choice,omitempty"` User string `json:"user,omitempty"` @@ -286,6 +290,13 @@ func (m *Message) ParseContent() []MediaContent { if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok { data, ok1 := audioData["data"].(string) format, ok2 := audioData["format"].(string) + if !ok2 { + if mimeType, ok3 := audioData["mime_type"].(string); ok3 { + format = mimeType + ok2 = true + } + } + common.SysLog(fmt.Sprintf("Parsing audio content: data_ok=%v, format_ok=%v, format=%s, data_length=%d", ok1, ok2, format, len(data))) if ok1 && ok2 { contentList = append(contentList, MediaContent{ Type: ContentTypeInputAudio, diff --git a/go.mod b/go.mod index 83fe5a067654..8c09f403784a 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/prometheus/client_golang v1.21.1 github.com/samber/lo v1.39.0 github.com/shirou/gopsutil v3.21.11+incompatible + github.com/volcengine/volcengine-go-sdk v1.1.17 github.com/xuri/excelize/v2 v2.9.0 golang.org/x/crypto v0.31.0 golang.org/x/image v0.23.0 @@ -69,6 +70,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect @@ -90,6 +92,7 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/volcengine/volc-sdk-golang v1.0.23 // indirect github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect @@ -99,6 +102,7 @@ require ( golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect diff --git a/go.sum b/go.sum index bb2c72adc401..f8016b2ddcf3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A= github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -6,6 +8,7 @@ github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+Kc github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI= github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI= github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6/go.mod h1:pbiaLIeYLUbgMY1kwEAdwO6UKD5ZNwdPGQlwokS9fe8= +github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= @@ -28,8 +31,10 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -44,6 +49,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -93,14 +100,31 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -124,6 +148,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -136,6 +164,7 @@ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kK github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -184,6 +213,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= @@ -212,6 +242,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -233,6 +264,10 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= +github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= +github.com/volcengine/volcengine-go-sdk v1.1.17 h1:Izrcx/FERzGvpY3ufPjt4GR7Ak6y94aMVXbnLmeuw2g= +github.com/volcengine/volcengine-go-sdk v1.1.17/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= @@ -246,19 +281,34 @@ github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -272,19 +322,42 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -305,6 +378,8 @@ gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBp gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= diff --git a/main.go b/main.go index eda3799c0cc7..8a31c6c0108e 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,8 @@ import ( "github.com/joho/godotenv" _ "net/http/pprof" + + "one-api/relay/channel/volcengine" ) //go:embed web/dist @@ -134,11 +136,26 @@ func main() { common.FatalLog("failed to initialize Redis: " + err.Error()) } + // Initialize Keep-Alive Manager for Redis keys + if err := volcengine.InitKeepAliveManager(); err != nil { + common.FatalLog("failed to initialize keep-alive manager: " + err.Error()) + } + // 在应用关闭时清理保活管理器 + defer func() { + if err := volcengine.ShutdownKeepAliveManager(); err != nil { + common.SysError("failed to shutdown keep-alive manager: " + err.Error()) + } + }() + // Initialize constants constant.InitEnv() // Initialize options model.InitOptionMap() model.InitGroups() + + // 初始化batch请求平均耗时 + volcengine.InitBatchRequestAverageDuration() + if common.RedisEnabled { // for compatibility with old versions common.MemoryCacheEnabled = true diff --git a/metrics/metrics.go b/metrics/metrics.go index 348ac3951637..f0b174fe1e1b 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -21,11 +21,16 @@ func RegisterMetrics(registry prometheus.Registerer) { registry.MustRegister(relayRequestE2ESuccessCounter) registry.MustRegister(relayRequestE2EFailedCounter) registry.MustRegister(relayRequestE2EDurationObsever) + // batch + registry.MustRegister(batchRequestCounter) + registry.MustRegister(batchRequestDurationObsever) // token metrics registry.MustRegister(inputTokensCounter) registry.MustRegister(outputTokensCounter) registry.MustRegister(cacheHitTokensCounter) registry.MustRegister(inferenceTokensCounter) + // error log metrics + registry.MustRegister(errorLogCounter) } var ( @@ -34,25 +39,25 @@ var ( Subsystem: Namespace, Name: "relay_request_total", Help: "Total number of relay request total", - }, []string{"channel", "tag", "base_url", "model", "group"}) + }, []string{"channel", "channel_name", "tag", "base_url", "model", "group"}) relayRequestSuccessCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_success", Help: "Total number of relay request success", - }, []string{"channel", "tag", "base_url", "model", "group"}) + }, []string{"channel", "channel_name", "tag", "base_url", "model", "group", "code"}) relayRequestFailedCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_failed", Help: "Total number of relay request failed", - }, []string{"channel", "tag", "base_url", "model", "group", "code"}) + }, []string{"channel", "channel_name", "tag", "base_url", "model", "group", "code"}) relayRequestRetryCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_retry", Help: "Total number of relay request retry", - }, []string{"channel", "tag", "base_url", "model", "group"}) + }, []string{"channel", "channel_name", "tag", "base_url", "model", "group"}) relayRequestDurationObsever = promauto.NewHistogramVec( prometheus.HistogramOpts{ Subsystem: Namespace, @@ -60,26 +65,26 @@ var ( Help: "Duration of relay request", Buckets: prometheus.ExponentialBuckets(1, 2, 12), }, - []string{"channel", "tag", "base_url", "model", "group"}, + []string{"channel", "channel_name", "tag", "base_url", "model", "group"}, ) relayRequestE2ETotalCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_e2e_total", Help: "Total number of relay request e2e total", - }, []string{"channel", "model", "group", "token_key", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "token_key", "token_name"}) relayRequestE2ESuccessCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_e2e_success", Help: "Total number of relay request e2e success", - }, []string{"channel", "model", "group", "token_key", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "token_key", "token_name"}) relayRequestE2EFailedCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "relay_request_e2e_failed", Help: "Total number of relay request e2e failed", - }, []string{"channel", "model", "group", "code", "token_key", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "code", "token_key", "token_name"}) relayRequestE2EDurationObsever = promauto.NewHistogramVec( prometheus.HistogramOpts{ Subsystem: Namespace, @@ -87,7 +92,23 @@ var ( Help: "Duration of relay request e2e", Buckets: prometheus.ExponentialBuckets(1, 2, 12), }, - []string{"channel", "model", "group", "token_key", "token_name"}, + []string{"channel", "channel_name", "model", "group", "token_key", "token_name"}, + ) + // Batch request metrics + batchRequestCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: Namespace, + Name: "batch_request_total", + Help: "Total number of batch requests by status code", + }, []string{"channel", "channel_name", "tag", "base_url", "model", "group", "code"}) + batchRequestDurationObsever = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Subsystem: Namespace, + Name: "batch_request_duration", + Help: "Duration of batch request", + Buckets: prometheus.ExponentialBuckets(1, 2, 12), + }, + []string{"channel", "channel_name", "tag", "base_url", "model", "group", "code"}, ) // Token metrics inputTokensCounter = prometheus.NewCounterVec( @@ -95,79 +116,100 @@ var ( Subsystem: Namespace, Name: "input_tokens_total", Help: "Total number of input tokens processed", - }, []string{"channel", "model", "group", "user_id", "user_name", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "user_id", "user_name", "token_name"}) outputTokensCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "output_tokens_total", Help: "Total number of output tokens generated", - }, []string{"channel", "model", "group", "user_id", "user_name", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "user_id", "user_name", "token_name"}) cacheHitTokensCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "cache_hit_tokens_total", Help: "Total number of tokens served from cache", - }, []string{"channel", "model", "group", "user_id", "user_name", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "user_id", "user_name", "token_name"}) inferenceTokensCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Subsystem: Namespace, Name: "inference_tokens_total", Help: "Total number of tokens processed during inference", - }, []string{"channel", "model", "group", "user_id", "user_name", "token_name"}) + }, []string{"channel", "channel_name", "model", "group", "user_id", "user_name", "token_name"}) + + errorLogCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Subsystem: Namespace, + Name: "error_log_total", + Help: "Total number of error logs", + }, []string{"channel", "channel_name", "error_code", "error_type", "model", "group", "token_name"}) ) -func IncrementRelayRequestTotalCounter(channel, tag, baseURL, model, group string, add float64) { - relayRequestTotalCounter.WithLabelValues(channel, tag, baseURL, model, group).Add(add) +func IncrementRelayRequestTotalCounter(channel, channelName, tag, baseURL, model, group string, add float64) { + relayRequestTotalCounter.WithLabelValues(channel, channelName, tag, baseURL, model, group).Add(add) +} + +func IncrementRelayRequestSuccessCounter(channel, channelName, tag, baseURL, model, group, statusCode string, add float64) { + relayRequestSuccessCounter.WithLabelValues(channel, channelName, tag, baseURL, model, group, statusCode).Add(add) } -func IncrementRelayRequestSuccessCounter(channel, tag, baseURL, model, group string, add float64) { - relayRequestSuccessCounter.WithLabelValues(channel, tag, baseURL, model, group).Add(add) +func IncrementRelayRequestFailedCounter(channel, channelName, tag, baseURL, model, group, code string, add float64) { + relayRequestFailedCounter.WithLabelValues(channel, channelName, tag, baseURL, model, group, code).Add(add) } -func IncrementRelayRequestFailedCounter(channel, tag, baseURL, model, group, code string, add float64) { - relayRequestFailedCounter.WithLabelValues(channel, tag, baseURL, model, group, code).Add(add) +func IncrementRelayRetryCounter(channel, channelName, tag, baseURL, model, group string, add float64) { + relayRequestRetryCounter.WithLabelValues(channel, channelName, tag, baseURL, model, group).Add(add) } -func IncrementRelayRetryCounter(channel, tag, baseURL, model, group string, add float64) { - relayRequestRetryCounter.WithLabelValues(channel, tag, baseURL, model, group).Add(add) +func ObserveRelayRequestDuration(channel, channelName, tag, baseURL, model, group string, duration float64) { + relayRequestDurationObsever.WithLabelValues(channel, channelName, tag, baseURL, model, group).Observe(duration) } -func ObserveRelayRequestDuration(channel, tag, baseURL, model, group string, duration float64) { - relayRequestDurationObsever.WithLabelValues(channel, tag, baseURL, model, group).Observe(duration) +func IncrementRelayRequestE2ETotalCounter(channel, channelName, model, group, tokenKey, tokenName string, add float64) { + relayRequestE2ETotalCounter.WithLabelValues(channel, channelName, model, group, tokenKey, tokenName).Add(add) } -func IncrementRelayRequestE2ETotalCounter(channel, model, group, tokenKey, tokenName string, add float64) { - relayRequestE2ETotalCounter.WithLabelValues(channel, model, group, tokenKey, tokenName).Add(add) +func IncrementRelayRequestE2ESuccessCounter(channel, channelName, model, group, tokenKey, tokenName string, add float64) { + relayRequestE2ESuccessCounter.WithLabelValues(channel, channelName, model, group, tokenKey, tokenName).Add(add) } -func IncrementRelayRequestE2ESuccessCounter(channel, model, group, tokenKey, tokenName string, add float64) { - relayRequestE2ESuccessCounter.WithLabelValues(channel, model, group, tokenKey, tokenName).Add(add) +func IncrementRelayRequestE2EFailedCounter(channel, channelName, model, group, code, tokenKey, tokenName string, add float64) { + relayRequestE2EFailedCounter.WithLabelValues(channel, channelName, model, group, code, tokenKey, tokenName).Add(add) } -func IncrementRelayRequestE2EFailedCounter(channel, model, group, code, tokenKey, tokenName string, add float64) { - relayRequestE2EFailedCounter.WithLabelValues(channel, model, group, code, tokenKey, tokenName).Add(add) +func ObserveRelayRequestE2EDuration(channel, channelName, model, group, tokenKey, tokenName string, duration float64) { + relayRequestE2EDurationObsever.WithLabelValues(channel, channelName, model, group, tokenKey, tokenName).Observe(duration) } -func ObserveRelayRequestE2EDuration(channel, model, group, tokenKey, tokenName string, duration float64) { - relayRequestE2EDurationObsever.WithLabelValues(channel, model, group, tokenKey, tokenName).Observe(duration) +// Batch request metrics functions +func IncrementBatchRequestCounter(channel, channelName, tag, baseURL, model, group, code string, add float64) { + batchRequestCounter.WithLabelValues(channel, channelName, tag, baseURL, model, group, code).Add(add) +} + +func ObserveBatchRequestDuration(channel, channelName, tag, baseURL, model, group, code string, duration float64) { + batchRequestDurationObsever.WithLabelValues(channel, channelName, tag, baseURL, model, group, code).Observe(duration) } // Token metrics functions -func IncrementInputTokens(channel, model, group, userId, userName, tokenName string, add float64) { - inputTokensCounter.WithLabelValues(channel, model, group, userId, userName, tokenName).Add(add) +func IncrementInputTokens(channel, channelName, model, group, userId, userName, tokenName string, add float64) { + inputTokensCounter.WithLabelValues(channel, channelName, model, group, userId, userName, tokenName).Add(add) +} + +func IncrementOutputTokens(channel, channelName, model, group, userId, userName, tokenName string, add float64) { + outputTokensCounter.WithLabelValues(channel, channelName, model, group, userId, userName, tokenName).Add(add) } -func IncrementOutputTokens(channel, model, group, userId, userName, tokenName string, add float64) { - outputTokensCounter.WithLabelValues(channel, model, group, userId, userName, tokenName).Add(add) +func IncrementCacheHitTokens(channel, channelName, model, group, userId, userName, tokenName string, add float64) { + cacheHitTokensCounter.WithLabelValues(channel, channelName, model, group, userId, userName, tokenName).Add(add) } -func IncrementCacheHitTokens(channel, model, group, userId, userName, tokenName string, add float64) { - cacheHitTokensCounter.WithLabelValues(channel, model, group, userId, userName, tokenName).Add(add) +func IncrementInferenceTokens(channel, channelName, model, group, userId, userName, tokenName string, add float64) { + inferenceTokensCounter.WithLabelValues(channel, channelName, model, group, userId, userName, tokenName).Add(add) } -func IncrementInferenceTokens(channel, model, group, userId, userName, tokenName string, add float64) { - inferenceTokensCounter.WithLabelValues(channel, model, group, userId, userName, tokenName).Add(add) +// Error log metrics function +func IncrementErrorLog(channel, channelName, errorCode, errorType, model, group, tokenName string, add float64) { + errorLogCounter.WithLabelValues(channel, channelName, errorCode, errorType, model, group, tokenName).Add(add) } diff --git a/middleware/distributor.go b/middleware/distributor.go index 748dac718557..eeacaea46510 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -1,8 +1,10 @@ package middleware import ( - "errors" + "bytes" + "encoding/json" "fmt" + "io" "net/http" "one-api/common" "one-api/constant" @@ -138,7 +140,7 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { midjourneyRequest := dto.MidjourneyRequest{} err = common.UnmarshalBodyReusable(c, &midjourneyRequest) if err != nil { - return nil, false, err + return nil, false, fmt.Errorf("无效的请求, %s", err.Error()) } midjourneyModel, mjErr, success := service.GetMjRequestModel(relayMode, &midjourneyRequest) if mjErr != nil { @@ -167,11 +169,40 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { c.Set("platform", string(constant.TaskPlatformSuno)) c.Set("relay_mode", relayMode) } else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") { - err = common.UnmarshalBodyReusable(c, &modelRequest) - } - if err != nil { - return nil, false, errors.New("无效的请求, " + err.Error()) + // 检查请求体是否为空 + body, err := io.ReadAll(c.Request.Body) + if err != nil { + return nil, false, fmt.Errorf("无效的请求, 读取请求体失败: %s", err.Error()) + } + // 重置请求体 + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) + + // 如果请求体为空,根据路径设置默认模型 + if len(body) == 0 { + if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { + modelRequest.Model = "text-moderation-stable" + } else if strings.HasSuffix(c.Request.URL.Path, "embeddings") { + modelRequest.Model = c.Param("model") + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { + modelRequest.Model = "dall-e" + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/speech") { + modelRequest.Model = "tts-1" + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/translations") { + modelRequest.Model = "whisper-1" + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") { + modelRequest.Model = "whisper-1" + } else if strings.HasPrefix(c.Request.URL.Path, "/v1/realtime") { + modelRequest.Model = c.Query("model") + } + } else { + // 请求体不为空,尝试解析 JSON + err = json.Unmarshal(body, &modelRequest) + if err != nil { + return nil, false, fmt.Errorf("无效的请求, JSON 解析失败: %s", err.Error()) + } + } } + if strings.HasPrefix(c.Request.URL.Path, "/v1/realtime") { //wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01 modelRequest.Model = c.Query("model") @@ -225,6 +256,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode c.Set("status_code_mapping", channel.GetStatusCodeMapping()) c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) c.Set("base_url", channel.GetBaseURL()) + c.Set("endpoint", channel.GetEndpoint()) // TODO: api_version统一 switch channel.Type { case common.ChannelTypeAzure: diff --git a/middleware/request-id.go b/middleware/request-id.go index 816396530cca..20fd086fe3de 100644 --- a/middleware/request-id.go +++ b/middleware/request-id.go @@ -2,8 +2,11 @@ package middleware import ( "crypto/md5" + "crypto/rand" + "encoding/hex" "one-api/common" "strconv" + "time" "github.com/gin-gonic/gin" ) @@ -12,35 +15,28 @@ func RequestId() func(c *gin.Context) { return func(c *gin.Context) { id := c.GetHeader(common.RequestIdKey) if id == "" { - id = common.GetTimeString() + common.GetRandomString(8) + // 使用更安全的request ID生成方法 + id = GenerateUniqueRequestId() c.Header(common.RequestIdKey, id) } c.Set(common.RequestIdKey, id) + c.Next() + } +} - // 优先使用上游传递的哈希值 - originHashValue := c.GetHeader("X-Origin-Hash-Value") +// GenerateUniqueRequestId 生成唯一的request ID +func GenerateUniqueRequestId() string { + // 获取当前时间戳(纳秒精度) + timestamp := time.Now().UnixNano() - if originHashValue != "" { - // 将上游的哈希值转换为整数 - value, err := strconv.Atoi(originHashValue) - if err == nil { - // 确保值在0-99范围内 - value = value % 100 - c.Set("hash_value", value) - c.Next() - return - } - } + // 生成16字节的随机数 + randomBytes := make([]byte, 16) + rand.Read(randomBytes) - // 如果没有上游哈希值或转换失败,则计算新的哈希值 - hash := md5.Sum([]byte(id)) - hashValue := 0 - for i := 0; i < len(hash); i++ { - hashValue = (hashValue*31 + int(hash[i])) % 100 - } - // 将哈希值存入上下文 - c.Set("hash_value", hashValue) + // 将时间戳和随机数组合 + combined := strconv.FormatInt(timestamp, 10) + hex.EncodeToString(randomBytes) - c.Next() - } + // 使用MD5生成最终的request ID(32位十六进制字符串) + hash := md5.Sum([]byte(combined)) + return hex.EncodeToString(hash[:]) } diff --git a/model/channel.go b/model/channel.go index e0ee2d2741d7..7b1d7940501b 100644 --- a/model/channel.go +++ b/model/channel.go @@ -22,6 +22,7 @@ type Channel struct { TestTime int64 `json:"test_time" gorm:"bigint"` ResponseTime int `json:"response_time"` // in milliseconds BaseURL *string `json:"base_url" gorm:"column:base_url;default:''"` + Endpoint *string `json:"endpoint" gorm:"column:endpoint;default:''"` Other string `json:"other"` Balance float64 `json:"balance"` // in USD BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` @@ -223,6 +224,13 @@ func (channel *Channel) GetBaseURL() string { return *channel.BaseURL } +func (channel *Channel) GetEndpoint() string { + if channel.Endpoint == nil { + return "" + } + return *channel.Endpoint +} + func (channel *Channel) GetModelMapping() string { if channel.ModelMapping == nil { return "" diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index 4e2c016ac054..248b88560c35 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -43,6 +43,14 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea for key, value := range info.Headers { req.Set(key, value) } + + // 添加指定的header - 从原始请求中获取 + if retryRequestId := c.GetHeader("retry_request_id"); retryRequestId != "" { + req.Set("retry_request_id", retryRequestId) + } + if retry := c.GetHeader("retry"); retry != "" { + req.Set("retry", retry) + } } func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) { @@ -167,7 +175,14 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http req.Header.Set("X-Origin-User-ID", strconv.Itoa(info.UserId)) req.Header.Set("X-Origin-Channel-ID", strconv.Itoa(info.ChannelId)) req.Header.Set("X-Retry-Count", strconv.Itoa(info.RetryCount)) - req.Header.Set("X-Origin-Hash-Value", strconv.Itoa(c.GetInt("hash_value"))) + + // 添加指定的header - 从原始请求中获取 + if retryRequestId := c.GetHeader("retry_request_id"); retryRequestId != "" { + req.Header.Set("retry_request_id", retryRequestId) + } + if retry := c.GetHeader("retry"); retry != "" { + req.Header.Set("retry", retry) + } // 打印请求头 requestId := c.GetString(onecommon.RequestIdKey) diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 28afe4d44639..602464584bf8 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -2,13 +2,14 @@ package aws import ( "errors" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/dto" "one-api/relay/channel/claude" relaycommon "one-api/relay/common" "one-api/setting/model_setting" + + "github.com/gin-gonic/gin" ) const ( @@ -40,6 +41,15 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { model_setting.GetClaudeSettings().WriteHeaders(info.OriginModelName, req) + + // 添加指定的header - 从原始请求中获取 + if retryRequestId := c.GetHeader("retry_request_id"); retryRequestId != "" { + req.Set("retry_request_id", retryRequestId) + } + if retry := c.GetHeader("retry"); retry != "" { + req.Set("retry", retry) + } + return nil } diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 6ba1a00b5da3..82ac7f120b03 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -212,6 +212,18 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque }, }) } + } else if part.Type == dto.ContentTypeInputAudio { + // 处理音频内容 + audioData := part.InputAudio.(dto.MessageInputAudio) + // 添加调试日志 + common.SysLog(fmt.Sprintf("Processing audio data: format=%s, data length=%d", audioData.Format, len(audioData.Data))) + // 将音频数据转换为Gemini的InlineData格式 + parts = append(parts, GeminiPart{ + InlineData: &GeminiInlineData{ + MimeType: audioData.Format, + Data: audioData.Data, + }, + }) } else if part.Type == dto.ContentTypeYoutube { parts = append(parts, GeminiPart{ FileData: &GeminiFileData{ diff --git a/relay/channel/task/suno/adaptor.go b/relay/channel/task/suno/adaptor.go index 03d60516f06a..1eaa31fd2c82 100644 --- a/relay/channel/task/suno/adaptor.go +++ b/relay/channel/task/suno/adaptor.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" @@ -16,6 +15,8 @@ import ( "one-api/service" "strings" "time" + + "github.com/gin-gonic/gin" ) type TaskAdaptor struct { @@ -64,6 +65,15 @@ func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) req.Header.Set("Accept", c.Request.Header.Get("Accept")) req.Header.Set("Authorization", "Bearer "+info.ApiKey) + + // 添加指定的header - 从原始请求中获取 + if retryRequestId := c.GetHeader("retry_request_id"); retryRequestId != "" { + req.Header.Set("retry_request_id", retryRequestId) + } + if retry := c.GetHeader("retry"); retry != "" { + req.Header.Set("retry", retry) + } + return nil } diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index f6b3d33fc4d9..55098993a284 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -76,6 +76,10 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela } func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + // 检查模型名称是否包含 "batch",如果是则使用批量接口 + if strings.Contains(strings.ToLower(info.OriginModelName), "batch") { + return DoBatchChatRequest(c, info, requestBody) + } return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/volcengine/batchchat.go b/relay/channel/volcengine/batchchat.go new file mode 100644 index 000000000000..994e40a8cea2 --- /dev/null +++ b/relay/channel/volcengine/batchchat.go @@ -0,0 +1,989 @@ +package volcengine + +import ( + "context" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/metrics" + relaycommon "one-api/relay/common" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gin-gonic/gin" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime" + "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" +) + +// 全局配置变量 +const ( + MaxParallelRequests = 2000 + + // 限速器默认大小为 MaxParallelRequests 的 3 倍 + RateLimiterSize = MaxParallelRequests * 3 + // 异步调用超时时间配置 + MinAsyncTimeout = 30 * time.Second + MaxAsyncTimeout = 60 * time.Second + // 限流等待时间配置 - 用于等待可用请求槽位的超时时间 + MinRateLimitWaitTime = 100 * time.Millisecond + MaxRateLimitWaitTime = 1000 * time.Millisecond + + // CreateBatchChatCompletion 调用的超时时间 + BatchCompletionTimeout = 24 * time.Hour + + // 子协程最大存活时间 + SubGoroutineMaxLifetime = 24 * time.Hour + + // 分布式锁过期时间 + DistributedLockExpiration = 24 * time.Hour +) + +// 异步调用的超时时间 - 可通过环境变量VOLCENGINE_ASYNC_CALL_TIMEOUT配置,默认30秒 +var AsyncCallTimeout = time.Duration(common.GetEnvOrDefault("VOLCENGINE_ASYNC_CALL_TIMEOUT", 30)) * time.Second + +// 客户端缓存 +var ( + clientCache = make(map[string]*arkruntime.Client) + clientMutex sync.RWMutex +) + +// 请求计数器 +var ( + requestCounter int64 = 0 +) + +// 限速器 +var ( + rateLimiter = make(chan struct{}, RateLimiterSize) +) + +// 建议重试时间相关变量 +var ( + batchRequestAvgDuration float64 = 30.0 // 默认30秒 + batchRequestAvgDurationMutex sync.RWMutex +) + +// NewBatchClient 创建一个新的批量请求客户端实例 +func NewBatchClient(apiKey string) *arkruntime.Client { + return arkruntime.NewClientWithApiKey( + apiKey, + arkruntime.WithBatchMaxParallel(MaxParallelRequests), // 使用全局变量设置发起请求的最大并发数量 + ) +} + +// GetBatchClient 根据 channel ID 获取或创建客户端实例 +func GetBatchClient(channelId string, apiKey string) *arkruntime.Client { + clientMutex.RLock() + if client, exists := clientCache[channelId]; exists { + clientMutex.RUnlock() + return client + } + clientMutex.RUnlock() + + // 如果缓存中没有,创建新的客户端 + clientMutex.Lock() + defer clientMutex.Unlock() + + // 双重检查,防止并发创建 + if client, exists := clientCache[channelId]; exists { + return client + } + + client := NewBatchClient(apiKey) + clientCache[channelId] = client + return client +} + +// acquireRequestSlot 获取请求槽位,如果达到上限则返回错误 +func acquireRequestSlot() error { + // 获取当前计数器值 + currentCount := atomic.LoadInt64(&requestCounter) + + // 如果已达到上限,返回错误 + if currentCount >= int64(RateLimiterSize) { + return fmt.Errorf("request limit reached, please retry later") + } + + // 尝试获取限速器槽位 + select { + case rateLimiter <- struct{}{}: + // 成功获取槽位,增加计数器 + atomic.AddInt64(&requestCounter, 1) + return nil + default: + // 限速器已满,返回错误 + return fmt.Errorf("request limit reached, please retry later") + } +} + +// releaseRequestSlot 释放请求槽位 +func releaseRequestSlot() { + // 减少计数器 + atomic.AddInt64(&requestCounter, -1) + // 释放限速器槽位 + select { + case <-rateLimiter: + default: + // 如果限速器为空,忽略 + } +} + +// waitForAvailableSlot 等待可用的请求槽位 +func waitForAvailableSlot(ctx context.Context) error { + ticker := time.NewTicker(100 * time.Millisecond) // 每100ms检查一次 + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + if err := acquireRequestSlot(); err == nil { + return nil + } + } + } +} + +// 从 context 获取异步调用超时时间,如果没有设置则使用随机计算的时间 +func getAsyncCallTimeout(ctx context.Context) time.Duration { + if timeout, ok := ctx.Value("async_call_timeout").(time.Duration); ok && timeout > 0 { + return timeout + } + // 使用随机等待时间作为异步调用超时时间 + return calculateRandomWaitTime() +} + +// 从 context 获取批量推理超时时间,如果没有设置则使用默认值 +func getBatchCompletionTimeout(ctx context.Context) time.Duration { + if timeout, ok := ctx.Value("batch_completion_timeout").(time.Duration); ok && timeout > 0 { + return timeout + } + return BatchCompletionTimeout +} + +// GetCurrentTimeouts 获取当前 context 中的超时配置 +func GetCurrentTimeouts(ctx context.Context) map[string]time.Duration { + return map[string]time.Duration{ + "async_call_timeout": getAsyncCallTimeout(ctx), + "batch_completion_timeout": getBatchCompletionTimeout(ctx), + "min_async_timeout": MinAsyncTimeout, + "max_async_timeout": MaxAsyncTimeout, + "min_rate_limit_wait": MinRateLimitWaitTime, + "max_rate_limit_wait": MaxRateLimitWaitTime, + "default_async_timeout": AsyncCallTimeout, + "default_batch_timeout": BatchCompletionTimeout, + } +} + +// getRequestID 从gin context获取请求ID,使用middleware设置的ID +func getRequestID(c *gin.Context) string { + // 优先使用retry_request_id + requestID := c.GetHeader("retry_request_id") + if requestID == "" { + // 如果没有retry_request_id,则使用正常的requestID + requestID = c.GetHeader(common.RequestIdKey) + } + return requestID +} + +// isRetryRequest 检查是否为重试请求 +func isRetryRequest(c *gin.Context) bool { + retryHeader := c.GetHeader("retry") + return retryHeader == "true" +} + +func DoBatchChatRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + // 获取请求ID + requestID := getRequestID(c) + c.Set("Retry_request_id", requestID) + + // 尝试获取分布式锁,避免重复执行 + lockKey := requestID + "_lock" + lockAcquired, err := TryAcquireLock(lockKey, DistributedLockExpiration) + if err != nil { + return nil, fmt.Errorf("failed to acquire lock: %w", err) + } + + if !lockAcquired { + // 返回内部错误响应 + errorResponse := gin.H{ + "error": gin.H{ + "message": fmt.Sprintf("request %s is already being processed, another request is in progress", requestID), + "type": "internal_error", + "code": "lock_acquisition_failed", + "request_id": requestID, + }, + } + errorJson, _ := json.Marshal(errorResponse) + response := &http.Response{ + StatusCode: dto.StatusRequestConflict, + Body: io.NopCloser(strings.NewReader(string(errorJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + return response, nil + } + + // 确保在函数结束时释放锁 + defer func() { + if releaseErr := ReleaseLock(lockKey); releaseErr != nil { + common.LogError(c, fmt.Sprintf("Failed to release lock for request %s: %v", requestID, releaseErr)) + } + }() + + // 检查是否为重试请求 + if isRetryRequest(c) { + // 从Redis获取结果,使用当前的requestID(可能是retry_request_id) + resultData, err := GetBatchResultFromRedis(requestID) + if err == nil { + // 检查Result是否为空且状态为pending,这种情况说明第一次请求可能超时了 + if resultData.Result == "" && resultData.Status == "pending" { + common.LogInfo(c.Request.Context(), fmt.Sprintf("Found pending request for %s, returning retry response", requestID)) + + // 返回重试提示 + errorResponse := gin.H{ + "error": gin.H{ + "message": "Request is still being processed, please retry later", + "type": "request_in_progress", + "code": "request_still_processing", + "request_id": requestID, + }, + } + errorJson, _ := json.Marshal(errorResponse) + + response := &http.Response{ + StatusCode: dto.StatusNewAPIBatchSubmitted, // 203 - 批量请求已提交,需要重试获取结果 + Body: io.NopCloser(strings.NewReader(string(errorJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + response.Header.Set("Retry_request_id", requestID) + // 添加建议重试时间header + avgDuration := GetBatchRequestAverageDuration() + response.Header.Set("X-Suggested-Retry-After", fmt.Sprintf("%.0f", avgDuration)) + c.Writer.Header().Set("Retry_request_id", requestID) + c.Writer.Header().Set("X-Suggested-Retry-After", fmt.Sprintf("%.0f", avgDuration)) + return response, nil + } else if resultData.Result != "" { + // 只有在Result不为空时才处理缓存结果 + // 删除Redis中的key + err = DeleteBatchResultFromRedis(requestID) + if err != nil { + common.LogError(c, err.Error()) + } + + // 先用火山引擎格式解析,再转换为SimpleResponse + openaiResponse, err := convertVolcEngineResponseToOpenAI([]byte(resultData.Result)) + if err != nil { + return nil, fmt.Errorf("failed to convert cached response format: %w", err) + } + + // 将转换后的结果序列化为JSON + openaiResponseJson, err := json.Marshal(openaiResponse) + if err != nil { + return nil, fmt.Errorf("failed to marshal cached OpenAI response: %w", err) + } + + // 找到结果,返回并删除key + response := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(string(openaiResponseJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + return response, nil + } else { + // Result为空但状态不是pending,说明是错误状态 + common.LogInfo(c.Request.Context(), fmt.Sprintf("Found error status for request %s, continuing with new request", requestID)) + // 删除Redis中的key,继续执行新建流程 + err = DeleteBatchResultFromRedis(requestID) + if err != nil { + common.LogError(c, err.Error()) + } + } + } + // 如果Redis中没有找到结果,继续执行新建流程 + } + + // 尝试获取请求槽位,如果无法立即获取则等待 + if err := acquireRequestSlot(); err != nil { + // 如果无法立即获取槽位,等待可用槽位 + // 随机计算等待时间:在100ms-1000ms之间随机选择 + waitTime := calculateRateLimitWaitTime() + + ctx, cancel := context.WithTimeout(c.Request.Context(), waitTime) + defer cancel() + + if waitErr := waitForAvailableSlot(ctx); waitErr != nil { + // 等待超时,返回自定义限流错误 + errorResponse := gin.H{ + "error": gin.H{ + "message": "Request limit reached, please retry later", + "type": "new_api_batch_rate_limit_exceeded", + "code": "new_api_batch_rate_limit_exceeded", + }, + } + errorJson, _ := json.Marshal(errorResponse) + response := &http.Response{ + StatusCode: dto.StatusNewAPIBatchRateLimitExceeded, + Body: io.NopCloser(strings.NewReader(string(errorJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + return response, nil + } + } + + // 确保在函数结束时释放槽位 + defer releaseRequestSlot() + + // 解析请求体 + var request dto.GeneralOpenAIRequest + if err := json.NewDecoder(requestBody).Decode(&request); err != nil { + return nil, fmt.Errorf("failed to decode request body: %w", err) + } + // 转换为豆包批量请求格式 + batchRequest, err := convertToBatchRequest(&request, info.Endpoint) + if err != nil { + return nil, fmt.Errorf("failed to convert request: %w", err) + } + + // 检查是否有未支持的参数 + checkUnsupportedParameters(&request) + + // 使用 channel ID 获取或创建客户端实例 + client := GetBatchClient(fmt.Sprintf("%d", info.ChannelId), info.ApiKey) + + // 创建带超时的 context,用于异步调用的整体超时 + timeoutDuration := getAsyncCallTimeout(c.Request.Context()) + asyncCtx, asyncCancel := context.WithTimeout(c.Request.Context(), timeoutDuration) + defer asyncCancel() + + // 创建通道用于接收异步结果 + resultChan := make(chan interface{}, 1) + errChan := make(chan error, 1) + + // 异步发起批量推理请求 + go func() { + // 使用独立的context,不受外层asyncCtx影响 + independentCtx := context.Background() + + // 记录batch请求开始时间 + batchStartTime := time.Now() + + result, err := executeBatchRequestWithRedis(independentCtx, client, batchRequest, requestID) + + // 记录batch请求指标 + // 获取实际状态码 + statusCode := "-1" // 默认状态码 + if err != nil { + statusCode = "request_failed" + } else if result != nil { + // 尝试从结果中提取状态码 + if resultMap, ok := result.(map[string]interface{}); ok { + if code, exists := resultMap["code"]; exists { + if codeStr, ok := code.(string); ok { + statusCode = codeStr + } else if codeInt, ok := code.(float64); ok { + statusCode = fmt.Sprintf("%.0f", codeInt) + } + } + } + } + + // 记录指标 + metrics.IncrementBatchRequestCounter( + info.ChannelTag, + info.ChannelName, + info.ChannelTag, + info.BaseUrl, + info.UpstreamModelName, + info.Group, + statusCode, + 1, + ) + metrics.ObserveBatchRequestDuration( + info.ChannelTag, + info.ChannelName, + info.ChannelTag, + info.BaseUrl, + info.UpstreamModelName, + info.Group, + statusCode, + time.Since(batchStartTime).Seconds(), + ) + + if err != nil { + common.LogError(c, fmt.Sprintf("Async batch request failed for requestID %s: %v", requestID, err)) + errChan <- err + return + } + resultChan <- result + }() + + // 等待结果或超时 + var result interface{} + select { + case result = <-resultChan: + // 成功获取结果 + common.LogInfo(c, fmt.Sprintf("Received result for requestID %s", requestID)) + case err := <-errChan: + // 发生错误 + common.LogError(c, fmt.Sprintf("batch request failed: %v", err)) + return nil, fmt.Errorf("batch request failed: %w", err) + case <-asyncCtx.Done(): + // 超时 + if asyncCtx.Err() == context.DeadlineExceeded { + common.LogError(c, fmt.Sprintf("Async call timeout after %v for requestID %s", timeoutDuration, requestID)) + c.Writer.Header().Set("Retry_request_id", requestID) + + // 返回自定义状态码表示请求已提交 + errorResponse := gin.H{ + "error": gin.H{ + "message": fmt.Sprintf("Async call timeout after %v for requestID %s, please retry later to get the result", timeoutDuration, requestID), + "type": "request_submitted", + "code": "request_submitted", + "request_id": requestID, + }, + } + errorJson, _ := json.Marshal(errorResponse) + response := &http.Response{ + StatusCode: dto.StatusNewAPIBatchSubmitted, // 203 - 批量请求已提交,需要重试获取结果 + Body: io.NopCloser(strings.NewReader(string(errorJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + response.Header.Set("Retry_request_id", requestID) + // 添加建议重试时间header + avgDuration := GetBatchRequestAverageDuration() + response.Header.Set("X-Suggested-Retry-After", fmt.Sprintf("%.0f", avgDuration)) + c.Writer.Header().Set("Retry_request_id", requestID) + c.Writer.Header().Set("X-Suggested-Retry-After", fmt.Sprintf("%.0f", avgDuration)) + return response, nil + } + common.LogError(c, fmt.Sprintf("Async call cancelled for requestID %s: %w", requestID, asyncCtx.Err())) + c.Writer.Header().Set("Retry_request_id", requestID) + return nil, fmt.Errorf("async call cancelled: %w", asyncCtx.Err()) + } + + // 将结果转换为JSON + resultJson, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + // 将火山引擎的响应转换为标准的OpenAI格式 + openaiResponse, err := convertVolcEngineResponseToOpenAI(resultJson) + if err != nil { + return nil, fmt.Errorf("failed to convert response format: %w", err) + } + + // 将转换后的结果序列化为JSON + openaiResponseJson, err := json.Marshal(openaiResponse) + if err != nil { + return nil, fmt.Errorf("failed to marshal OpenAI response: %w", err) + } + + // 创建HTTP响应 + response := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(string(openaiResponseJson))), + Header: make(http.Header), + } + response.Header.Set("Content-Type", "application/json") + response.Header.Set("Retry_request_id", requestID) + return response, nil +} + +// calculateRandomWaitTime 在 MinAsyncTimeout-MaxAsyncTimeout 之间随机计算异步调用超时时间 +func calculateRandomWaitTime() time.Duration { + // 计算时间范围(毫秒) + timeRange := int(MaxAsyncTimeout.Milliseconds() - MinAsyncTimeout.Milliseconds()) + + // 如果时间范围为0或负数,直接返回MinAsyncTimeout + if timeRange <= 0 { + return MinAsyncTimeout + } + + // 生成 MinAsyncTimeout-MaxAsyncTimeout 之间的随机等待时间(毫秒) + waitMilliseconds := rand.Intn(timeRange) + int(MinAsyncTimeout.Milliseconds()) + + return time.Duration(waitMilliseconds) * time.Millisecond +} + +// calculateRateLimitWaitTime 在 MinRateLimitWaitTime-MaxRateLimitWaitTime 之间随机计算限流等待时间 +func calculateRateLimitWaitTime() time.Duration { + // 计算时间范围(毫秒) + timeRange := int(MaxRateLimitWaitTime.Milliseconds() - MinRateLimitWaitTime.Milliseconds()) + + // 如果时间范围为0或负数,直接返回MinRateLimitWaitTime + if timeRange <= 0 { + return MinRateLimitWaitTime + } + + // 生成 MinRateLimitWaitTime-MaxRateLimitWaitTime 之间的随机等待时间(毫秒) + waitMilliseconds := rand.Intn(timeRange) + int(MinRateLimitWaitTime.Milliseconds()) + + return time.Duration(waitMilliseconds) * time.Millisecond +} + +// GetCurrentRequestCount 获取当前请求计数(用于监控) +func GetCurrentRequestCount() int64 { + return atomic.LoadInt64(&requestCounter) +} + +// GetRateLimiterStatus 获取限速器状态(用于监控) +func GetRateLimiterStatus() (current, capacity int) { + return len(rateLimiter), cap(rateLimiter) +} + +func MustMarshalJson(v interface{}) string { + s, _ := json.Marshal(v) + return string(s) +} + +// checkUnsupportedParameters 检查请求中是否有未支持的参数 +func checkUnsupportedParameters(request *dto.GeneralOpenAIRequest) { + var unsupportedParams []string + + // 检查 tool_choice 参数(豆包批量推理可能不支持) + if request.ToolChoice != nil { + unsupportedParams = append(unsupportedParams, "tool_choice") + } + + // 检查 n 参数(批量推理不支持,批量推理本身就是多个请求) + if request.N > 0 { + unsupportedParams = append(unsupportedParams, "n") + } + + // 检查 stream 参数(批量推理不支持流式输出) + if request.Stream { + unsupportedParams = append(unsupportedParams, "stream") + } + + // 检查 user 参数(豆包SDK支持,但批量推理可能不支持) + if request.User != "" { + unsupportedParams = append(unsupportedParams, "user") + } + + // 检查 seed 参数(豆包可能不支持) + if request.Seed != 0 { + unsupportedParams = append(unsupportedParams, "seed") + } + + // 检查 response_format 参数(豆包SDK支持,但批量推理可能不支持) + if request.ResponseFormat != nil { + unsupportedParams = append(unsupportedParams, "response_format") + } + + // 检查 stream_options 参数(豆包SDK支持,但批量推理可能不支持) + if request.StreamOptions != nil { + unsupportedParams = append(unsupportedParams, "stream_options") + } + + // 检查 functions 参数(已废弃,使用tools替代) + if request.Functions != nil { + unsupportedParams = append(unsupportedParams, "functions") + } + + // 检查其他可能不支持的参数 + if request.Prompt != nil { + unsupportedParams = append(unsupportedParams, "prompt") + } + + if request.Prefix != nil { + unsupportedParams = append(unsupportedParams, "prefix") + } + + if request.Suffix != nil { + unsupportedParams = append(unsupportedParams, "suffix") + } + + if request.Input != nil { + unsupportedParams = append(unsupportedParams, "input") + } + + if request.Instruction != "" { + unsupportedParams = append(unsupportedParams, "instruction") + } + + if request.Size != "" { + unsupportedParams = append(unsupportedParams, "size") + } + + if request.EncodingFormat != nil { + unsupportedParams = append(unsupportedParams, "encoding_format") + } + + if request.Dimensions > 0 { + unsupportedParams = append(unsupportedParams, "dimensions") + } + + if request.Modalities != nil { + unsupportedParams = append(unsupportedParams, "modalities") + } + + if request.Audio != nil { + unsupportedParams = append(unsupportedParams, "audio") + } + + if request.ExtraBody != nil { + unsupportedParams = append(unsupportedParams, "extra_body") + } + + if request.Thinking != nil { + unsupportedParams = append(unsupportedParams, "thinking") + } + + if request.ThinkingConfig != nil { + unsupportedParams = append(unsupportedParams, "thinking_config") + } + + // 如果有未支持的参数,打印错误日志 + if len(unsupportedParams) > 0 { + fmt.Printf("Error: Unsupported parameters detected in batch request: %v\n", unsupportedParams) + fmt.Printf("These parameters are not supported by VolcEngine batch inference API and will be ignored.\n") + } +} + +// convertToBatchRequest 将 OpenAI 格式的请求转换为豆包批量请求格式 +func convertToBatchRequest(request *dto.GeneralOpenAIRequest, endpoint string) (*model.CreateChatCompletionRequest, error) { + // 获取消息内容 + if len(request.Messages) == 0 { + return nil, fmt.Errorf("no messages found in request") + } + + // 转换消息为豆包格式,支持多模态消息 + messages := make([]*model.ChatCompletionMessage, 0, len(request.Messages)) + for _, msg := range request.Messages { + // 解析消息内容 + contentParts := msg.ParseContent() + + var messageContent *model.ChatCompletionMessageContent + + if len(contentParts) == 1 && contentParts[0].Type == "text" { + // 单文本消息 + text := contentParts[0].Text + messageContent = &model.ChatCompletionMessageContent{ + StringValue: &text, + } + } else { + // 多模态消息或复杂消息 + parts := make([]*model.ChatCompletionMessageContentPart, 0, len(contentParts)) + for _, part := range contentParts { + switch part.Type { + case "text": + parts = append(parts, &model.ChatCompletionMessageContentPart{ + Type: "text", + Text: part.Text, + }) + case "image_url": + if imageUrl, ok := part.ImageUrl.(dto.MessageImageUrl); ok { + detail := model.ImageURLDetail(imageUrl.Detail) + parts = append(parts, &model.ChatCompletionMessageContentPart{ + Type: "image_url", + ImageURL: &model.ChatMessageImageURL{ + URL: imageUrl.Url, + Detail: detail, + }, + }) + } + } + } + if len(parts) > 0 { + messageContent = &model.ChatCompletionMessageContent{ + ListValue: parts, + } + } + } + + if messageContent != nil { + messages = append(messages, &model.ChatCompletionMessage{ + Role: msg.Role, + Content: messageContent, + }) + } + } + + // 转换为豆包批量请求格式 + batchRequest := model.CreateChatCompletionRequest{ + Model: endpoint, + Messages: messages, + } + + // 设置 max_tokens,只有当请求中包含时才设置 + if request.MaxTokens > 0 { + maxTokens := int(request.MaxTokens) + batchRequest.MaxTokens = &maxTokens + } + + // 设置 stop 参数,只有当请求中包含时才设置 + if request.Stop != nil { + // 类型断言处理 stop 参数 + switch stop := request.Stop.(type) { + case string: + batchRequest.Stop = []string{stop} + case []string: + batchRequest.Stop = stop + case []interface{}: + stopStrings := make([]string, 0, len(stop)) + for _, s := range stop { + if str, ok := s.(string); ok { + stopStrings = append(stopStrings, str) + } + } + batchRequest.Stop = stopStrings + } + } + + // 设置 frequency_penalty,只有当请求中包含且不为0时才设置 + if request.FrequencyPenalty != 0 { + freqPenalty := float32(request.FrequencyPenalty) + batchRequest.FrequencyPenalty = &freqPenalty + } + + // 设置 presence_penalty,只有当请求中包含且不为0时才设置 + if request.PresencePenalty != 0 { + presPenalty := float32(request.PresencePenalty) + batchRequest.PresencePenalty = &presPenalty + } + + // 设置 temperature,只有当请求中包含时才设置 + if request.Temperature != nil { + temp := float32(*request.Temperature) + batchRequest.Temperature = &temp + } + + // 设置 top_p,只有当请求中包含且不为0时才设置 + if request.TopP != 0 { + topP := float32(request.TopP) + batchRequest.TopP = &topP + } + + // 设置 logprobs,只有当请求中包含且为true时才设置 + if request.LogProbs { + batchRequest.LogProbs = &request.LogProbs + } + + // 设置 top_logprobs,只有当请求中包含且大于0时才设置 + if request.TopLogProbs > 0 { + batchRequest.TopLogProbs = &request.TopLogProbs + } + + // 设置 logit_bias,只有当请求中包含时才设置 + if len(request.LogitBias) > 0 { + batchRequest.LogitBias = request.LogitBias + } + + // 设置 tools,只有当请求中包含时才设置 + if len(request.Tools) > 0 { + tools := make([]*model.Tool, 0, len(request.Tools)) + for _, tool := range request.Tools { + // 只支持function类型的工具 + if tool.Type == "function" { + chatTool := &model.Tool{ + Type: model.ToolTypeFunction, + Function: &model.FunctionDefinition{ + Name: tool.Function.Name, + Description: tool.Function.Description, + Parameters: tool.Function.Parameters, + }, + } + tools = append(tools, chatTool) + } + } + if len(tools) > 0 { + batchRequest.Tools = tools + } + } + + return &batchRequest, nil +} + +// executeBatchRequestWithRedis 执行批量请求并保存结果到Redis +func executeBatchRequestWithRedis(ctx context.Context, client *arkruntime.Client, batchRequest *model.CreateChatCompletionRequest, requestID string) (interface{}, error) { + // 在发起请求前先创建Redis key + if err := CreateBatchRequestKey(requestID); err != nil { + common.LogError(ctx, fmt.Sprintf("Failed to create initial Redis key for request %s: %v", requestID, err)) + // 即使创建Redis key失败,也继续执行请求,只是不保存结果 + } + + // 为 CreateBatchChatCompletion 创建独立的超时 context,不复用传入的ctx + apiCtx, apiCancel := context.WithTimeout(context.Background(), getBatchCompletionTimeout(ctx)) + defer apiCancel() + + common.LogInfo(ctx, fmt.Sprintf("Batch chat completion request: %+v ", batchRequest)) + result, err := client.CreateBatchChatCompletion(apiCtx, batchRequest) + if err != nil { + common.LogError(ctx, err.Error()) + // 检查是否是超时错误 + if apiCtx.Err() == context.DeadlineExceeded { + timeoutMsg := fmt.Sprintf("batch completion timeout after %v", getBatchCompletionTimeout(ctx)) + if saveErr := SaveBatchErrorToRedis(requestID, timeoutMsg); saveErr != nil { + fmt.Printf("Failed to save timeout error to Redis for request %s: %v\n", requestID, saveErr) + } + } else { + if saveErr := SaveBatchErrorToRedis(requestID, err.Error()); saveErr != nil { + fmt.Printf("Failed to save error to Redis for request %s: %v\n", requestID, saveErr) + } + } + return nil, err + } + // 保存成功结果到Redis,子协程独立运行 + common.LogInfo(ctx, fmt.Sprintf("Batch chat completion result: %+v", result)) + if saveErr := SaveBatchResultToRedis(requestID, result, "completed"); saveErr != nil { + fmt.Printf("Failed to save result to Redis for request %s: %v\n", requestID, saveErr) + } + + return result, nil +} + +// convertVolcEngineResponseToOpenAI 将火山引擎的响应转换为标准的OpenAI格式 +func convertVolcEngineResponseToOpenAI(resultJson []byte) (*dto.SimpleResponse, error) { + // 添加调试日志 + fmt.Printf("Original volcengine response: %s\n", string(resultJson)) + + // 直接解析为火山引擎的原始格式 + var volcResponse map[string]interface{} + if err := json.Unmarshal(resultJson, &volcResponse); err != nil { + return nil, fmt.Errorf("failed to unmarshal volcengine response: %w", err) + } + + // 提取choices和usage + choices, ok := volcResponse["choices"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid choices field in volcengine response") + } + + usageRaw, ok := volcResponse["usage"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid usage field in volcengine response") + } + + // 转换usage + usage := dto.Usage{} + if promptTokens, ok := usageRaw["prompt_tokens"].(float64); ok { + usage.PromptTokens = int(promptTokens) + } + if completionTokens, ok := usageRaw["completion_tokens"].(float64); ok { + usage.CompletionTokens = int(completionTokens) + } + if totalTokens, ok := usageRaw["total_tokens"].(float64); ok { + usage.TotalTokens = int(totalTokens) + } + + // 转换为标准的OpenAI格式 + openaiResponse := &dto.SimpleResponse{ + Usage: usage, + Choices: []dto.OpenAITextResponseChoice{}, + } + + // 转换choices + for _, choice := range choices { + choiceMap, ok := choice.(map[string]interface{}) + if !ok { + continue + } + + message, ok := choiceMap["message"].(map[string]interface{}) + if !ok { + continue + } + + // 处理content字段 + content := "" + if contentRaw, exists := message["content"]; exists { + switch v := contentRaw.(type) { + case string: + content = v + case []interface{}: + // 如果是数组,提取文本内容 + for _, part := range v { + if partMap, ok := part.(map[string]interface{}); ok { + if partType, ok := partMap["type"].(string); ok && partType == "text" { + if text, ok := partMap["text"].(string); ok { + content += text + } + } + } + } + } + } + + // 处理reasoning_content字段 + if reasoningContent, exists := message["reasoning_content"]; exists { + if reasoningStr, ok := reasoningContent.(string); ok && reasoningStr != "" { + content = reasoningStr + "\n" + content + } + } + + // 构建转换后的choice + index := 0 + if indexRaw, ok := choiceMap["index"].(float64); ok { + index = int(indexRaw) + } + + finishReason := "" + if finishReasonRaw, ok := choiceMap["finish_reason"].(string); ok { + finishReason = finishReasonRaw + } + + role := "" + if roleRaw, ok := message["role"].(string); ok { + role = roleRaw + } + + convertedChoice := dto.OpenAITextResponseChoice{ + Index: index, + Message: dto.Message{ + Role: role, + }, + FinishReason: finishReason, + } + convertedChoice.Message.SetStringContent(content) + + openaiResponse.Choices = append(openaiResponse.Choices, convertedChoice) + } + + // 添加调试日志 + openaiResponseJson, _ := json.Marshal(openaiResponse) + fmt.Printf("Converted OpenAI response: %s\n", string(openaiResponseJson)) + + return openaiResponse, nil +} + +// GetBatchRequestAverageDuration 获取batch请求的平均耗时(秒) +// 这个函数返回一个估算的平均耗时,用于建议重试时间 +func GetBatchRequestAverageDuration() float64 { + batchRequestAvgDurationMutex.RLock() + defer batchRequestAvgDurationMutex.RUnlock() + return batchRequestAvgDuration +} + +// SetBatchRequestAverageDuration 设置batch请求的平均耗时(秒) +func SetBatchRequestAverageDuration(duration float64) { + batchRequestAvgDurationMutex.Lock() + defer batchRequestAvgDurationMutex.Unlock() + if duration > 0 { + batchRequestAvgDuration = duration + } +} + +// InitBatchRequestAverageDuration 初始化batch请求的平均耗时 +// 从环境变量读取配置,如果没有配置则使用默认值 +func InitBatchRequestAverageDuration() { + if avgDurationStr := os.Getenv("BATCH_REQUEST_AVG_DURATION"); avgDurationStr != "" { + if avgDuration, err := strconv.ParseFloat(avgDurationStr, 64); err == nil && avgDuration > 0 { + SetBatchRequestAverageDuration(avgDuration) + } + } +} diff --git a/relay/channel/volcengine/keepalive.go b/relay/channel/volcengine/keepalive.go new file mode 100644 index 000000000000..79972600d998 --- /dev/null +++ b/relay/channel/volcengine/keepalive.go @@ -0,0 +1,546 @@ +package volcengine + +import ( + "context" + "fmt" + "math/rand" + "one-api/common" + "one-api/middleware" + "strings" + "sync" + "time" +) + +// KeepAliveManager 保活管理器 +type KeepAliveManager struct { + keys map[string]*KeepAliveKey + mutex sync.RWMutex + ctx context.Context + cancel context.CancelFunc + isRunning bool + interval time.Duration + expiration time.Duration +} + +// KeepAliveKey 保活key的信息 +type KeepAliveKey struct { + Key string `json:"key"` + CreatedAt time.Time `json:"created_at"` // 创建时间 + LastTouch time.Time `json:"last_touch"` // 最后触摸时间 + Expiration time.Duration `json:"expiration"` // 过期时间 + Status string `json:"status"` // active, inactive, error + ErrorCount int `json:"error_count"` // 错误计数 +} + +// 全局保活管理器实例 +var ( + keepAliveManager *KeepAliveManager + keepAliveOnce sync.Once +) + +// 默认配置 +const ( + DefaultKeepAliveInterval = 10 * time.Second // 默认保活间隔:10分钟 + DefaultKeepAliveExpiration = 10 * time.Minute // 默认过期时间:4小时 + MaxKeepAliveDuration = 10 * time.Minute // 最大保活时间:4小时 + MaxErrorCount = 5 // 最大错误次数 + KeepAliveTriggerTime = 5 * time.Minute // 保活触发时间:在key过期前5分钟开始保活 + MinKeepAliveInterval = 30 * time.Second // 随机保活时间范围 + MaxKeepAliveInterval = 2 * time.Minute // 随机保活时间范围 +) + +// GetKeepAliveManager 获取全局保活管理器实例(单例模式) +func GetKeepAliveManager() *KeepAliveManager { + keepAliveOnce.Do(func() { + keepAliveManager = NewKeepAliveManager(DefaultKeepAliveInterval, DefaultKeepAliveExpiration) + }) + return keepAliveManager +} + +// NewKeepAliveManager 创建新的保活管理器 +func NewKeepAliveManager(interval, expiration time.Duration) *KeepAliveManager { + ctx, cancel := context.WithCancel(context.Background()) + + manager := &KeepAliveManager{ + keys: make(map[string]*KeepAliveKey), + ctx: ctx, + cancel: cancel, + isRunning: false, + interval: interval, + expiration: expiration, + } + + return manager +} + +// Start 启动保活管理器 +func (kam *KeepAliveManager) Start() error { + kam.mutex.Lock() + defer kam.mutex.Unlock() + + if kam.isRunning { + return fmt.Errorf("keep-alive manager is already running") + } + + kam.isRunning = true + + // 启动保活协程 + go kam.keepAliveLoop() + + common.LogInfo(kam.ctx, fmt.Sprintf("Keep-alive manager started with interval: %v, expiration: %v", kam.interval, kam.expiration)) + return nil +} + +// Stop 停止保活管理器 +func (kam *KeepAliveManager) Stop() error { + kam.mutex.Lock() + defer kam.mutex.Unlock() + + if !kam.isRunning { + return fmt.Errorf("keep-alive manager is not running") + } + + kam.isRunning = false + kam.cancel() + + common.LogInfo(kam.ctx, "Keep-alive manager stopped") + return nil +} + +// AddKey 添加key到保活列表 +func (kam *KeepAliveManager) AddKey(key string, expiration time.Duration) error { + if key == "" { + return fmt.Errorf("key cannot be empty") + } + + if expiration <= 0 { + expiration = kam.expiration + } + + kam.mutex.Lock() + defer kam.mutex.Unlock() + + // 检查key是否已存在 + if _, exists := kam.keys[key]; exists { + return fmt.Errorf("key %s already exists in keep-alive list", key) + } + + // 创建新的保活key + keepAliveKey := &KeepAliveKey{ + Key: key, + CreatedAt: time.Now(), + LastTouch: time.Now(), + Expiration: expiration, + Status: "active", + ErrorCount: 0, + } + + kam.keys[key] = keepAliveKey + + common.LogInfo(kam.ctx, fmt.Sprintf("Added key %s to keep-alive list with expiration: %v", key, expiration)) + return nil +} + +// RemoveKey 从保活列表中移除key +func (kam *KeepAliveManager) RemoveKey(key string) error { + kam.mutex.Lock() + defer kam.mutex.Unlock() + + if _, exists := kam.keys[key]; !exists { + return fmt.Errorf("key %s not found in keep-alive list", key) + } + + delete(kam.keys, key) + + common.LogInfo(kam.ctx, fmt.Sprintf("Removed key %s from keep-alive list", key)) + return nil +} + +// GetKey 获取key的信息 +func (kam *KeepAliveManager) GetKey(key string) (*KeepAliveKey, error) { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + keepAliveKey, exists := kam.keys[key] + if !exists { + return nil, fmt.Errorf("key %s not found in keep-alive list", key) + } + + return keepAliveKey, nil +} + +// GetAllKeys 获取所有保活key的信息 +func (kam *KeepAliveManager) GetAllKeys() map[string]*KeepAliveKey { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + // 创建副本以避免并发访问问题 + result := make(map[string]*KeepAliveKey) + for key, value := range kam.keys { + result[key] = &KeepAliveKey{ + Key: value.Key, + CreatedAt: value.CreatedAt, + LastTouch: value.LastTouch, + Expiration: value.Expiration, + Status: value.Status, + ErrorCount: value.ErrorCount, + } + } + + return result +} + +// GetKeyCount 获取保活key的数量 +func (kam *KeepAliveManager) GetKeyCount() int { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + return len(kam.keys) +} + +// IsRunning 检查保活管理器是否正在运行 +func (kam *KeepAliveManager) IsRunning() bool { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + return kam.isRunning +} + +// keepAliveLoop 保活循环 +func (kam *KeepAliveManager) keepAliveLoop() { + for { + select { + case <-kam.ctx.Done(): + common.LogInfo(kam.ctx, "Keep-alive loop stopped") + return + default: + // 生成随机保活间隔时间 + randomInterval := kam.generateRandomInterval() + common.LogInfo(kam.ctx, fmt.Sprintf("Next keep-alive in %v", randomInterval)) + + // 等待随机时间 + select { + case <-kam.ctx.Done(): + common.LogInfo(kam.ctx, "Keep-alive loop stopped") + return + case <-time.After(randomInterval): + kam.performKeepAlive() + } + } + } +} + +// generateRandomInterval 生成随机保活间隔时间 +func (kam *KeepAliveManager) generateRandomInterval() time.Duration { + // 计算随机秒数 + minSeconds := int(MinKeepAliveInterval.Seconds()) + maxSeconds := int(MaxKeepAliveInterval.Seconds()) + randomSeconds := minSeconds + rand.Intn(maxSeconds-minSeconds+1) + + return time.Duration(randomSeconds) * time.Second +} + +// performKeepAlive 执行保活操作 +func (kam *KeepAliveManager) performKeepAlive() { + // 使用现有的RequestId生成逻辑创建ctx + requestID := middleware.GenerateUniqueRequestId() + ctx := context.WithValue(context.Background(), common.RequestIdKey, requestID) + + kam.mutex.RLock() + keys := make([]string, 0, len(kam.keys)) + for key := range kam.keys { + keys = append(keys, key) + } + kam.mutex.RUnlock() + + // 记录本轮保活开始 + common.LogInfo(ctx, fmt.Sprintf("Keep-alive round started, total keys: %d", len(keys))) + + // 统计变量 + var ( + successCount int + removedCount int + errorCount int + skippedCount int + ) + + // 逐个处理每个key + for i, key := range keys { + keyRequestID := fmt.Sprintf("%s-key-%d", requestID, i+1) + keyCtx := context.WithValue(ctx, "key_request_id", keyRequestID) + + result := kam.touchKey(keyCtx, key) + switch result { + case "success": + successCount++ + case "removed": + removedCount++ + case "error": + errorCount++ + case "skipped": + skippedCount++ + } + } + + // 记录本轮保活结束 + common.LogInfo(ctx, fmt.Sprintf("Keep-alive round completed, success: %d, removed: %d, errors: %d, skipped: %d", + successCount, removedCount, errorCount, skippedCount)) +} + +// touchKey 触摸单个key以保持活跃(内部方法,带详细日志) +func (kam *KeepAliveManager) touchKey(ctx context.Context, key string) string { + kam.mutex.Lock() + keepAliveKey, exists := kam.keys[key] + if !exists { + kam.mutex.Unlock() + common.LogInfo(ctx, fmt.Sprintf("Key %s not found in keep-alive list", key)) + return "removed" + } + kam.mutex.Unlock() + + // 记录开始处理key + common.LogInfo(ctx, fmt.Sprintf("Processing key %s, age: %v, error_count: %d", + key, time.Since(keepAliveKey.CreatedAt), keepAliveKey.ErrorCount)) + + // 检查是否超过最大保活时间 + if time.Since(keepAliveKey.CreatedAt) > MaxKeepAliveDuration { + common.LogInfo(ctx, fmt.Sprintf("Key %s has exceeded max keep-alive duration (%v), removing from keep-alive list", + key, MaxKeepAliveDuration)) + kam.RemoveKey(key) + return "removed" + } + + // 检查是否需要保活:只在key快到期的最后5分钟进行保活 + remainingTime := keepAliveKey.Expiration - time.Since(keepAliveKey.CreatedAt) + if remainingTime > KeepAliveTriggerTime { + common.LogInfo(ctx, fmt.Sprintf("Key %s has %v remaining, skipping keep-alive (trigger time: %v)", + key, remainingTime, KeepAliveTriggerTime)) + return "skipped" + } + + // 尝试触摸key + err := kam.touchRedisKey(key, keepAliveKey.Expiration) + + kam.mutex.Lock() + defer kam.mutex.Unlock() + + if err != nil { + // 检查是否是key不存在的错误 + if strings.Contains(err.Error(), "does not exist in Redis") { + common.LogInfo(ctx, fmt.Sprintf("Key %s no longer exists in Redis, removing from keep-alive list", + key)) + delete(kam.keys, key) + return "removed" + } + + // 其他错误,增加错误计数 + keepAliveKey.ErrorCount++ + keepAliveKey.Status = "error" + + common.LogError(ctx, fmt.Sprintf("Failed to touch key %s: %v (error count: %d)", + key, err, keepAliveKey.ErrorCount)) + + // 如果错误次数超过阈值,移除key + if keepAliveKey.ErrorCount >= MaxErrorCount { + common.LogError(ctx, fmt.Sprintf("Key %s exceeded max error count, removing from keep-alive list", + key)) + delete(kam.keys, key) + return "removed" + } + + return "error" + } else { + keepAliveKey.LastTouch = time.Now() + keepAliveKey.ErrorCount = 0 + keepAliveKey.Status = "active" + + common.LogInfo(ctx, fmt.Sprintf("Successfully touched key %s, new expiration: %v", + key, keepAliveKey.Expiration)) + + return "success" + } +} + +// touchRedisKey 触摸Redis中的key +func (kam *KeepAliveManager) touchRedisKey(key string, expiration time.Duration) error { + redisClient := getRedisClient() + ctx := context.Background() + + // 检查key是否存在 + exists, err := redisClient.Exists(ctx, key).Result() + if err != nil { + return fmt.Errorf("failed to check key existence: %w", err) + } + + if exists == 0 { + return fmt.Errorf("key %s does not exist in Redis, should be removed from keep-alive list", key) + } + + // 更新key的过期时间 + err = redisClient.Expire(ctx, key, expiration).Err() + if err != nil { + return fmt.Errorf("failed to update key expiration: %w", err) + } + + return nil +} + +// CleanupExpiredKeys 清理过期的key +func (kam *KeepAliveManager) CleanupExpiredKeys() int { + kam.mutex.Lock() + defer kam.mutex.Unlock() + + now := time.Now() + removedCount := 0 + + for key, keepAliveKey := range kam.keys { + if now.Sub(keepAliveKey.LastTouch) > keepAliveKey.Expiration { + delete(kam.keys, key) + removedCount++ + common.LogInfo(kam.ctx, fmt.Sprintf("Cleaned up expired key: %s", key)) + } + } + + if removedCount > 0 { + common.LogInfo(kam.ctx, fmt.Sprintf("Cleaned up %d expired keys", removedCount)) + } + + return removedCount +} + +// GetStats 获取保活管理器的统计信息 +func (kam *KeepAliveManager) GetStats() map[string]interface{} { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + stats := make(map[string]interface{}) + stats["is_running"] = kam.isRunning + stats["total_keys"] = len(kam.keys) + stats["interval"] = kam.interval.String() + stats["expiration"] = kam.expiration.String() + stats["max_keep_alive_duration"] = MaxKeepAliveDuration.String() + + // 统计不同状态的key数量 + statusCount := make(map[string]int) + // 统计保活时间分布 + ageDistribution := make(map[string]int) + now := time.Now() + + for _, key := range kam.keys { + statusCount[key.Status]++ + + // 计算key的年龄并分类 + age := now.Sub(key.CreatedAt) + switch { + case age < 1*time.Hour: + ageDistribution["<1h"]++ + case age < 2*time.Hour: + ageDistribution["1-2h"]++ + case age < 3*time.Hour: + ageDistribution["2-3h"]++ + case age < 4*time.Hour: + ageDistribution["3-4h"]++ + default: + ageDistribution[">4h"]++ + } + } + stats["status_count"] = statusCount + stats["age_distribution"] = ageDistribution + + return stats +} + +// 便捷函数,用于快速添加batch_result类型的key +func AddBatchResultKey(requestID string) error { + manager := GetKeepAliveManager() + key := "batch_result:" + requestID + return manager.AddKey(key, DefaultKeepAliveExpiration) +} + +// 便捷函数,用于快速移除batch_result类型的key +func RemoveBatchResultKey(requestID string) error { + manager := GetKeepAliveManager() + key := "batch_result:" + requestID + return manager.RemoveKey(key) +} + +// 便捷函数,用于获取batch_result类型key的剩余保活时间 +func GetBatchResultKeyRemainingTime(requestID string) (time.Duration, error) { + manager := GetKeepAliveManager() + key := "batch_result:" + requestID + return manager.GetKeyRemainingKeepAliveTime(key) +} + +// 便捷函数,用于获取batch_result类型key的年龄 +func GetBatchResultKeyAge(requestID string) (time.Duration, error) { + manager := GetKeepAliveManager() + key := "batch_result:" + requestID + return manager.GetKeyAge(key) +} + +// InitKeepAliveManager 初始化并启动保活管理器 +func InitKeepAliveManager() error { + manager := GetKeepAliveManager() + + // 如果已经运行,直接返回 + if manager.IsRunning() { + return nil + } + + // 启动保活管理器 + if err := manager.Start(); err != nil { + return fmt.Errorf("failed to start keep-alive manager: %w", err) + } + + common.LogInfo(context.Background(), "Keep-alive manager initialized and started successfully") + return nil +} + +// ShutdownKeepAliveManager 关闭保活管理器 +func ShutdownKeepAliveManager() error { + manager := GetKeepAliveManager() + + if !manager.IsRunning() { + return nil + } + + if err := manager.Stop(); err != nil { + return fmt.Errorf("failed to stop keep-alive manager: %w", err) + } + + common.LogInfo(context.Background(), "Keep-alive manager shutdown successfully") + return nil +} + +// GetKeyRemainingKeepAliveTime 获取key的剩余保活时间 +func (kam *KeepAliveManager) GetKeyRemainingKeepAliveTime(key string) (time.Duration, error) { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + keepAliveKey, exists := kam.keys[key] + if !exists { + return 0, fmt.Errorf("key %s not found in keep-alive list", key) + } + + elapsed := time.Since(keepAliveKey.CreatedAt) + remaining := MaxKeepAliveDuration - elapsed + + if remaining <= 0 { + return 0, nil + } + + return remaining, nil +} + +// GetKeyAge 获取key的年龄(从创建到现在的时间) +func (kam *KeepAliveManager) GetKeyAge(key string) (time.Duration, error) { + kam.mutex.RLock() + defer kam.mutex.RUnlock() + + keepAliveKey, exists := kam.keys[key] + if !exists { + return 0, fmt.Errorf("key %s not found in keep-alive list", key) + } + + return time.Since(keepAliveKey.CreatedAt), nil +} diff --git a/relay/channel/volcengine/redis_client.go b/relay/channel/volcengine/redis_client.go new file mode 100644 index 000000000000..2b6b0043c426 --- /dev/null +++ b/relay/channel/volcengine/redis_client.go @@ -0,0 +1,300 @@ +package volcengine + +import ( + "context" + "encoding/json" + "fmt" + "one-api/common" + "sync" + "time" + + "github.com/go-redis/redis/v8" +) + +// Redis 客户端 +var ( + redisClient *redis.Client + redisOnce sync.Once +) + +// BatchResultData 批量推理结果数据结构 +type BatchResultData struct { + Result string `json:"result"` + Timestamp int64 `json:"timestamp"` + Status string `json:"status"` + RequestID string `json:"request_id"` + Error string `json:"error,omitempty"` +} + +// getRedisClient 获取Redis客户端实例 +func getRedisClient() *redis.Client { + redisOnce.Do(func() { + // 使用项目统一的Redis客户端 + if common.RedisEnabled && common.RDB != nil { + redisClient = common.RDB + } else { + // 如果项目Redis未启用,创建一个默认的本地Redis客户端 + redisClient = redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", + DB: 0, + }) + } + }) + return redisClient +} + +// CreateBatchRequestKey 在发起请求前预先创建Redis key +func CreateBatchRequestKey(requestID string) error { + redisClient := getRedisClient() + ctx := context.Background() + + // 设置过期时间为10分钟 + expiration := 10 * time.Minute + + // 创建初始状态的数据结构 + resultData := BatchResultData{ + Result: "", + Timestamp: time.Now().Unix(), + Status: "pending", + RequestID: requestID, + } + + resultDataJson, err := json.Marshal(resultData) + if err != nil { + return fmt.Errorf("failed to marshal initial data: %w", err) + } + + // 写入Redis + key := "batch_result:" + requestID + err = redisClient.Set(ctx, key, string(resultDataJson), expiration).Err() + if err != nil { + return fmt.Errorf("failed to create initial key in Redis: %w", err) + } + + // 将key添加到保活管理器 + if err := AddBatchResultKey(requestID); err != nil { + // 即使添加到保活管理器失败,也不影响Redis key的创建 + fmt.Printf("Warning: Failed to add key %s to keep-alive manager: %v\n", key, err) + } + + fmt.Printf("Successfully created initial Redis key for request %s with status: pending\n", requestID) + return nil +} + +// SaveBatchResultToRedis 保存批量推理结果到Redis +func SaveBatchResultToRedis(requestID string, result interface{}, status string) error { + redisClient := getRedisClient() + ctx := context.Background() + + // 设置过期时间为24小时 + expiration := 24 * time.Hour + + // 将结果转换为JSON + var resultJson []byte + var err error + + if result == nil { + // 如果结果为nil,使用空字符串 + resultJson = []byte("") + } else { + resultJson, err = json.Marshal(result) + if err != nil { + return fmt.Errorf("failed to marshal result: %w", err) + } + } + + // 创建结果数据结构 + resultData := BatchResultData{ + Result: string(resultJson), + Timestamp: time.Now().Unix(), + Status: status, + RequestID: requestID, + } + + resultDataJson, err := json.Marshal(resultData) + if err != nil { + return fmt.Errorf("failed to marshal result data: %w", err) + } + + // 写入Redis + key := "batch_result:" + requestID + err = redisClient.Set(ctx, key, string(resultDataJson), expiration).Err() + if err != nil { + return fmt.Errorf("failed to write result to Redis: %w", err) + } + + fmt.Printf("Successfully wrote result to Redis for request %s with status: %s\n", requestID, status) + return nil +} + +// SaveBatchErrorToRedis 保存批量推理错误到Redis +func SaveBatchErrorToRedis(requestID string, errorMsg string) error { + redisClient := getRedisClient() + ctx := context.Background() + + // 设置过期时间为24小时 + expiration := 24 * time.Hour + + // 创建错误数据结构 + resultData := BatchResultData{ + Result: "", + Timestamp: time.Now().Unix(), + Status: "error", + RequestID: requestID, + Error: errorMsg, + } + + resultDataJson, err := json.Marshal(resultData) + if err != nil { + return fmt.Errorf("failed to marshal error data: %w", err) + } + + // 写入Redis + key := "batch_result:" + requestID + err = redisClient.Set(ctx, key, string(resultDataJson), expiration).Err() + if err != nil { + return fmt.Errorf("failed to write error to Redis: %w", err) + } + + fmt.Printf("Successfully wrote error to Redis for request %s\n", requestID) + return nil +} + +// GetBatchResultFromRedis 从Redis获取批量推理结果 +func GetBatchResultFromRedis(requestID string) (*BatchResultData, error) { + redisClient := getRedisClient() + ctx := context.Background() + + key := "batch_result:" + requestID + result, err := redisClient.Get(ctx, key).Result() + if err != nil { + if err == redis.Nil { + return nil, fmt.Errorf("result not found for request ID: %s", requestID) + } + return nil, fmt.Errorf("failed to get result from Redis: %w", err) + } + + var resultData BatchResultData + if err := json.Unmarshal([]byte(result), &resultData); err != nil { + return nil, fmt.Errorf("failed to unmarshal result data: %w", err) + } + + return &resultData, nil +} + +// DeleteBatchResultFromRedis 从Redis删除批量推理结果 +func DeleteBatchResultFromRedis(requestID string) error { + redisClient := getRedisClient() + ctx := context.Background() + + key := "batch_result:" + requestID + err := redisClient.Del(ctx, key).Err() + if err != nil { + return fmt.Errorf("failed to delete result from Redis: %w", err) + } + + fmt.Printf("Successfully deleted result from Redis for request %s\n", requestID) + return nil +} + +// ListBatchResultsFromRedis 列出所有批量推理结果 +func ListBatchResultsFromRedis() ([]string, error) { + redisClient := getRedisClient() + ctx := context.Background() + + pattern := "batch_result:*" + keys, err := redisClient.Keys(ctx, pattern).Result() + if err != nil { + return nil, fmt.Errorf("failed to list keys from Redis: %w", err) + } + + return keys, nil +} + +// GetBatchResultCount 获取批量推理结果数量 +func GetBatchResultCount() (int64, error) { + redisClient := getRedisClient() + ctx := context.Background() + + pattern := "batch_result:*" + count, err := redisClient.Keys(ctx, pattern).Result() + if err != nil { + return 0, fmt.Errorf("failed to count keys from Redis: %w", err) + } + + return int64(len(count)), nil +} + +// CleanExpiredBatchResults 清理过期的批量推理结果 +func CleanExpiredBatchResults() error { + redisClient := getRedisClient() + ctx := context.Background() + + // 获取所有批量推理结果的key + keys, err := ListBatchResultsFromRedis() + if err != nil { + return fmt.Errorf("failed to list keys: %w", err) + } + + // 检查每个key的TTL,如果小于等于0则删除 + for _, key := range keys { + ttl, err := redisClient.TTL(ctx, key).Result() + if err != nil { + fmt.Printf("Failed to get TTL for key %s: %v\n", key, err) + continue + } + + if ttl <= 0 { + err := redisClient.Del(ctx, key).Err() + if err != nil { + fmt.Printf("Failed to delete expired key %s: %v\n", key, err) + } else { + fmt.Printf("Successfully deleted expired key %s\n", key) + } + } + } + + return nil +} + +// PingRedis 测试Redis连接 +func PingRedis() error { + redisClient := getRedisClient() + ctx := context.Background() + + _, err := redisClient.Ping(ctx).Result() + if err != nil { + return fmt.Errorf("failed to ping Redis: %w", err) + } + + return nil +} + +// TryAcquireLock 尝试获取分布式锁 +func TryAcquireLock(lockKey string, expiration time.Duration) (bool, error) { + redisClient := getRedisClient() + ctx := context.Background() + + // 使用SET命令的NX和EX选项实现分布式锁 + result, err := redisClient.SetNX(ctx, "lock:"+lockKey, "locked", expiration).Result() + if err != nil { + return false, fmt.Errorf("failed to acquire lock: %w", err) + } + + return result, nil +} + +// ReleaseLock 释放分布式锁 +func ReleaseLock(lockKey string) error { + redisClient := getRedisClient() + ctx := context.Background() + + // 删除锁 + err := redisClient.Del(ctx, "lock:"+lockKey).Err() + if err != nil { + return fmt.Errorf("failed to release lock: %w", err) + } + + return nil +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 5405fc2d4eef..8834c6eb7345 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -21,6 +21,7 @@ type RelayInfo struct { ChannelType int ChannelId int ChannelTag string + ChannelName string TokenId int TokenKey string UserId int @@ -44,6 +45,7 @@ type RelayInfo struct { ApiKey string Organization string BaseUrl string + Endpoint string SupportStreamOptions bool ShouldIncludeUsage bool IsModelMapped bool @@ -91,6 +93,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { channelId := c.GetInt("channel_id") channelSetting := c.GetStringMap("channel_setting") channelTag := c.GetString("channel_tag") + channelName := c.GetString("channel_name") tokenId := c.GetInt("token_id") tokenKey := c.GetString("token_key") userId := c.GetInt("id") @@ -108,10 +111,12 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { isFirstResponse: true, RelayMode: relayconstant.Path2RelayMode(c.Request.URL.Path), BaseUrl: c.GetString("base_url"), + Endpoint: c.GetString("endpoint"), RequestURLPath: c.Request.URL.String(), ChannelType: channelType, ChannelId: channelId, ChannelTag: channelTag, + ChannelName: channelName, TokenId: tokenId, TokenKey: tokenKey, UserId: userId, diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index 273988cf9665..77812503defb 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -32,6 +32,7 @@ const ( APITypeBaiduV2 APITypeOpenRouter APITypeXai + APITypeDoubaoOffline APITypeDummy // this one is only for count, do not add any channel after this ) @@ -92,6 +93,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = APITypeOpenRouter case common.ChannelTypeXai: apiType = APITypeXai + case common.ChannelTypeDoubaoOffline: + apiType = APITypeVolcEngine } if apiType == -1 { return APITypeOpenAI, false diff --git a/relay/helper/common.go b/relay/helper/common.go index 2a72d30a9ce3..225da677d047 100644 --- a/relay/helper/common.go +++ b/relay/helper/common.go @@ -4,11 +4,12 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" - "github.com/gorilla/websocket" "net/http" "one-api/common" "one-api/dto" + + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" ) func SetEventStreamHeaders(c *gin.Context) { diff --git a/relay/relay-audio.go b/relay/relay-audio.go index b46956e9220f..456ae80876e6 100644 --- a/relay/relay-audio.go +++ b/relay/relay-audio.go @@ -3,7 +3,6 @@ package relay import ( "errors" "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/dto" @@ -16,6 +15,8 @@ import ( "strconv" "strings" "time" + + "github.com/gin-gonic/gin" ) func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.AudioRequest, error) { @@ -71,13 +72,14 @@ func AudioInfo(c *gin.Context) (*relaycommon.RelayInfo, *dto.AudioRequest, *dto. func AudioHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, audioRequest *dto.AudioRequest) (openaiErr *dto.OpenAIErrorWithStatusCode) { startTime := time.Now() var funcErr *dto.OpenAIErrorWithStatusCode - metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, 1) + var statusCode int = -1 + metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, 1) defer func() { if funcErr != nil { - metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) + metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) } else { - metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, 1) - metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, strconv.Itoa(statusCode), 1) + metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, audioRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) } }() var ( @@ -160,6 +162,9 @@ func AudioHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, audioRequest return openaiErr } + // 设置状态码用于指标记录 + statusCode = resp.(*http.Response).StatusCode + postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "") return nil diff --git a/relay/relay-image.go b/relay/relay-image.go index 4d21e617ef76..a638c59dfce2 100644 --- a/relay/relay-image.go +++ b/relay/relay-image.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" @@ -19,6 +18,8 @@ import ( "strconv" "strings" "time" + + "github.com/gin-gonic/gin" ) func getAndValidImageRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.ImageRequest, error) { @@ -87,13 +88,14 @@ func ImageInfo(c *gin.Context) (*relaycommon.RelayInfo, *dto.ImageRequest, *dto. func ImageHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, imageRequest *dto.ImageRequest) (openaiErr *dto.OpenAIErrorWithStatusCode) { startTime := time.Now() var funcErr *dto.OpenAIErrorWithStatusCode - metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, 1) + var statusCode int = -1 + metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, 1) defer func() { if funcErr != nil { - metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) + metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) } else { - metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, 1) - metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, strconv.Itoa(statusCode), 1) + metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, imageRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) } }() err := helper.ModelMappedHelper(c, relayInfo) @@ -116,6 +118,9 @@ func ImageHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, imageRequest } userQuota, err := model.GetUserQuota(relayInfo.UserId, false) + if err != nil { + common.LogError(c, fmt.Sprintf("get_user_quota_failed: %s", err.Error())) + } sizeRatio := 1.0 // Size @@ -195,6 +200,9 @@ func ImageHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, imageRequest return openaiErr } + // 设置状态码用于指标记录 + statusCode = resp.(*http.Response).StatusCode + usage := &dto.Usage{ PromptTokens: imageRequest.N, TotalTokens: imageRequest.N, diff --git a/relay/relay-text.go b/relay/relay-text.go index 10f98738eb18..f8a2637678f6 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -96,13 +96,14 @@ func TextInfo(c *gin.Context) (*relaycommon.RelayInfo, *dto.GeneralOpenAIRequest func TextHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest *dto.GeneralOpenAIRequest) (openaiErr *dto.OpenAIErrorWithStatusCode) { startTime := common.GetBeijingTime() var funcErr *dto.OpenAIErrorWithStatusCode - metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, 1) + var statusCode int = -1 + metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, 1) defer func() { if funcErr != nil { - metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, strconv.Itoa(openaiErr.StatusCode), 1) + metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, strconv.Itoa(openaiErr.StatusCode), 1) } else { - metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, 1) - metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, strconv.Itoa(statusCode), 1) + metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, textRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) } }() @@ -141,7 +142,7 @@ func TextHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest *d // Record input tokens metric tokenName := c.GetString("token_name") userName := c.GetString("username") - metrics.IncrementInputTokens(strconv.Itoa(relayInfo.ChannelId), textRequest.Model, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(promptTokens)) + metrics.IncrementInputTokens(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, textRequest.Model, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(promptTokens)) priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens)) if err != nil { @@ -221,9 +222,26 @@ func TextHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest *d relayInfo.Headers["X-Test-Traffic"] = "true" } + // // 如果请求中包含 retry_request_id 头,则添加到 relayInfo 中 + // if retryRequestId := c.GetHeader("retry_request_id"); retryRequestId != "" { + // if relayInfo.Headers == nil { + // relayInfo.Headers = make(map[string]string) + // } + // relayInfo.Headers["retry_request_id"] = retryRequestId + // } + + // // 如果请求中包含 retry 头,则添加到 relayInfo 中 + // if retry := c.GetHeader("retry"); retry != "" { + // if relayInfo.Headers == nil { + // relayInfo.Headers = make(map[string]string) + // } + // relayInfo.Headers["retry"] = retry + // } + statusCodeMappingStr := c.GetString("status_code_mapping") var httpResp *http.Response resp, err := adaptor.DoRequest(c, relayInfo, requestBody) + if err != nil { funcErr = service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) return funcErr @@ -231,13 +249,22 @@ func TextHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest *d if resp != nil { httpResp = resp.(*http.Response) - // 添加来源标识和重试标记 - httpResp.Header.Set("X-Origin-User-ID", strconv.Itoa(relayInfo.UserId)) - httpResp.Header.Set("X-Origin-Channel-ID", strconv.Itoa(relayInfo.ChannelId)) - httpResp.Header.Set("X-Retry-Count", strconv.Itoa(relayInfo.RetryCount)) - + // // 直接设置到 gin 的响应头 + // c.Writer.Header().Set("X-Origin-User-ID", strconv.Itoa(relayInfo.UserId)) + // c.Writer.Header().Set("X-Origin-Channel-ID", strconv.Itoa(relayInfo.ChannelId)) + // c.Writer.Header().Set("X-Retry-Count", strconv.Itoa(relayInfo.RetryCount)) + // if c.GetHeader("Retry_request_id") != "" { + // c.Writer.Header().Set("Retry_request_id", c.GetHeader("Retry_request_id")) + // } relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream") if httpResp.StatusCode != http.StatusOK { + for k, v := range httpResp.Header { + if k == "Content-Length" { + continue + } + c.Writer.Header().Set(k, v[0]) + // common.LogInfo(c, fmt.Sprintf("set header %s = %s", k, v[0])) + } openaiErr = service.RelayErrorHandler(httpResp) funcErr = openaiErr // reset status code 重置状态码 @@ -264,6 +291,9 @@ func TextHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest *d return openaiErr } common.LogInfo(c, fmt.Sprintf("response status code: %d, Usage: %+v", httpResp.StatusCode, usage)) + // 设置状态码用于指标记录 + statusCode = resp.(*http.Response).StatusCode + // Store request and response data together if persistence is enabled and status code is 200 if model.RequestPersistenceEnabled && httpResp.StatusCode == http.StatusOK && !(c.GetHeader("X-Test-Traffic") == "true") { // 读取请求数据 @@ -486,14 +516,13 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, } // Record token metrics - metrics.IncrementOutputTokens(strconv.Itoa(relayInfo.ChannelId), modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(completionTokens)) + metrics.IncrementOutputTokens(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(completionTokens)) if cacheTokens > 0 { - metrics.IncrementCacheHitTokens(strconv.Itoa(relayInfo.ChannelId), modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(cacheTokens)) + metrics.IncrementCacheHitTokens(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(cacheTokens)) } - metrics.IncrementInferenceTokens(strconv.Itoa(relayInfo.ChannelId), modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(thinkingTokens)) - fmt.Println(strconv.Itoa(relayInfo.ChannelId), modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(completionTokens)) + metrics.IncrementInferenceTokens(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, modelName, relayInfo.Group, strconv.Itoa(relayInfo.UserId), userName, tokenName, float64(thinkingTokens)) other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice) model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, thinkingTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index bba872d3e4b8..6245d749cb12 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -89,7 +89,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &openrouter.Adaptor{} case constant.APITypeXai: return &xai.Adaptor{} - + case constant.APITypeDoubaoOffline: + return &volcengine.Adaptor{} } return nil } diff --git a/relay/relay_embedding.go b/relay/relay_embedding.go index 7607f7d7849e..8dd76439162e 100644 --- a/relay/relay_embedding.go +++ b/relay/relay_embedding.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/dto" @@ -15,6 +14,8 @@ import ( "one-api/service" "strconv" "time" + + "github.com/gin-gonic/gin" ) func getEmbeddingPromptToken(embeddingRequest dto.EmbeddingRequest) int { @@ -50,13 +51,14 @@ func EmbeddingInfo(c *gin.Context) (*relaycommon.RelayInfo, *dto.EmbeddingReques func EmbeddingHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, embeddingRequest *dto.EmbeddingRequest) (openaiErr *dto.OpenAIErrorWithStatusCode) { startTime := time.Now() var funcErr *dto.OpenAIErrorWithStatusCode - metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, 1) + var statusCode int = -1 + metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, 1) defer func() { if funcErr != nil { - metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) + metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) } else { - metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, 1) - metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, strconv.Itoa(statusCode), 1) + metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, embeddingRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) } }() @@ -139,6 +141,10 @@ func EmbeddingHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, embedding service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr } + + // 设置状态码用于指标记录 + statusCode = resp.(*http.Response).StatusCode + postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "") return nil } diff --git a/relay/relay_rerank.go b/relay/relay_rerank.go index 17a5934b09bc..e3c3f0832953 100644 --- a/relay/relay_rerank.go +++ b/relay/relay_rerank.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/dto" @@ -14,6 +13,8 @@ import ( "one-api/service" "strconv" "time" + + "github.com/gin-gonic/gin" ) func getRerankPromptToken(rerankRequest dto.RerankRequest) int { @@ -42,13 +43,14 @@ func RerankInfo(c *gin.Context) (*relaycommon.RelayInfo, *dto.RerankRequest, *dt func RerankHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, rerankRequest *dto.RerankRequest) (openaiErr *dto.OpenAIErrorWithStatusCode) { startTime := time.Now() var funcErr *dto.OpenAIErrorWithStatusCode - metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, 1) + var statusCode int = -1 + metrics.IncrementRelayRequestTotalCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, 1) defer func() { if funcErr != nil { - metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) + metrics.IncrementRelayRequestFailedCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, strconv.Itoa(funcErr.StatusCode), 1) } else { - metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, 1) - metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) + metrics.IncrementRelayRequestSuccessCounter(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, strconv.Itoa(statusCode), 1) + metrics.ObserveRelayRequestDuration(strconv.Itoa(relayInfo.ChannelId), relayInfo.ChannelName, relayInfo.ChannelTag, relayInfo.BaseUrl, rerankRequest.Model, relayInfo.Group, time.Since(startTime).Seconds()) } }() if rerankRequest.Query == "" { @@ -132,6 +134,10 @@ func RerankHelper(c *gin.Context, relayInfo *relaycommon.RelayInfo, rerankReques service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr } + + // 设置状态码用于指标记录 + statusCode = resp.(*http.Response).StatusCode + postConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "") return nil } diff --git a/relay/relay_task.go b/relay/relay_task.go index 01237c37bbe8..ee66647eafd4 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" @@ -17,6 +16,8 @@ import ( "one-api/service" "one-api/setting" "one-api/setting/operation_setting" + + "github.com/gin-gonic/gin" ) /* diff --git a/router/api-router.go b/router/api-router.go index 1701313bf8a8..48333309248f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -13,6 +13,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.Use(gzip.Gzip(gzip.DefaultCompression)) apiRouter.Use(middleware.GlobalAPIRateLimit()) { + apiRouter.GET("/ping", controller.Ping) apiRouter.GET("/status", controller.GetStatus) apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) diff --git a/router/relay-router.go b/router/relay-router.go index f547767ebd6b..54799467e1e8 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -1,15 +1,20 @@ package router import ( - "github.com/gin-gonic/gin" "one-api/controller" "one-api/middleware" "one-api/relay" + + "github.com/gin-gonic/gin" ) func SetRelayRouter(router *gin.Engine) { router.Use(middleware.CORS()) router.Use(middleware.DecompressRequestMiddleware()) + + // 添加ping路由用于测试 + router.GET("/ping", controller.Ping) + // https://platform.openai.com/docs/api-reference/introduction modelsRouter := router.Group("/v1/models") modelsRouter.Use(middleware.TokenAuth()) diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 5738d656bb1f..60641cca44d6 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -108,5 +108,10 @@ export const CHANNEL_OPTIONS = [ value: 44, color: 'purple', label: '嵌入模型:MokaAI M3E' + }, + { + value: 100, + color: 'blue', + label: '豆包离线' } ]; diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index bfc611fe7fd5..824dc4898b46 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -77,6 +77,7 @@ const EditChannel = (props) => { openai_organization: '', max_input_tokens: 0, base_url: '', + endpoint: '', other: '', model_mapping: '', status_code_mapping: '', @@ -537,7 +538,7 @@ const EditChannel = (props) => { value={inputs.name} autoComplete="new-password" /> - {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && ( + {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && inputs.type !== 100 && ( <>
{t('代理站地址')}: @@ -556,6 +557,25 @@ const EditChannel = (props) => { )} + {inputs.type === 100 && ( + <> +
+ {t('接入点地址')}: +
+ + { + handleInputChange('endpoint', value); + }} + value={inputs.endpoint} + autoComplete="new-password" + /> + + + )}
{t('密钥')}: