Skip to content
Closed

update #3062

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/docker-image-ghcr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Build Docker image (GHCR)

on:
workflow_dispatch:
inputs:
tag:
description: "image tag"
required: true
default: "debug"
push:
branches:
- main

permissions:
contents: read
packages: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Normalize GHCR repository
run: echo "GHCR_REPOSITORY=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV

- name: Set image tag
run: |
if [ -n "${{ github.event.inputs.tag }}" ]; then
echo "IMAGE_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
else
echo "IMAGE_TAG=latest" >> $GITHUB_ENV
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build & push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ env.IMAGE_TAG }}
ghcr.io/${{ env.GHCR_REPOSITORY }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
sbom: false
1 change: 1 addition & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (

ContextKeyOriginalModel ContextKey = "original_model"
ContextKeyRequestStartTime ContextKey = "request_start_time"
ContextKeyProcessedRequestBody ContextKey = "processed_request_body"

/* token related keys */
ContextKeyTokenUnlimited ContextKey = "token_unlimited_quota"
Expand Down
37 changes: 36 additions & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,20 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}

for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() {
logger.LogDebug(c, "渠道请求开始: retry=%d/%d, model=%q, relay_mode=%d, token_group=%q, using_group=%q", retryParam.GetRetry(), common.RetryTimes, relayInfo.OriginModelName, relayInfo.RelayMode, relayInfo.TokenGroup, relayInfo.UsingGroup)
channel, channelErr := getChannel(c, relayInfo, retryParam)
if channelErr != nil {
logger.LogDebug(c, "渠道选择失败: retry=%d, err=%s", retryParam.GetRetry(), channelErr.Error())
logger.LogError(c, channelErr.Error())
newAPIError = channelErr
break
}

logger.LogDebug(c, "渠道选择成功: retry=%d, channel_id=%d, channel_type=%d, channel_name=%q", retryParam.GetRetry(), channel.Id, channel.Type, channel.Name)
addUsedChannel(c, channel.Id)
requestBody, bodyErr := common.GetRequestBody(c)
if bodyErr != nil {
logger.LogDebug(c, "读取请求体失败: retry=%d, err=%s", retryParam.GetRetry(), bodyErr.Error())
// Ensure consistent 413 for oversized bodies even when error occurs later (e.g., retry path)
if common.IsRequestBodyTooLargeError(bodyErr) || errors.Is(bodyErr, common.ErrRequestBodyTooLarge) {
newAPIError = types.NewErrorWithStatusCode(bodyErr, types.ErrorCodeReadRequestBodyFailed, http.StatusRequestEntityTooLarge, types.ErrOptionWithSkipRetry())
Expand All @@ -212,14 +216,19 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
}

if newAPIError == nil {
logger.LogDebug(c, "渠道请求成功: retry=%d, channel_id=%d", retryParam.GetRetry(), channel.Id)
return
}

logger.LogDebug(c, "渠道请求失败: retry=%d, channel_id=%d, status=%d, err=%s", retryParam.GetRetry(), channel.Id, newAPIError.StatusCode, newAPIError.Error())
processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)

if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) {
remainTimes := common.RetryTimes - retryParam.GetRetry()
if !shouldRetry(c, newAPIError, remainTimes) {
logger.LogDebug(c, "渠道重试结束: retry=%d, remaining=%d, channel_id=%d", retryParam.GetRetry(), remainTimes, channel.Id)
break
}
logger.LogDebug(c, "渠道重试继续: retry=%d, remaining=%d, channel_id=%d", retryParam.GetRetry(), remainTimes, channel.Id)
}

useChannel := c.GetStringSlice("use_channel")
Expand Down Expand Up @@ -349,6 +358,23 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
if c.Request != nil && c.Request.URL != nil {
other["request_path"] = c.Request.URL.Path
}
if processedBody, ok := common.GetContextKey(c, constant.ContextKeyProcessedRequestBody); ok {
switch body := processedBody.(type) {
case string:
other["request_body"] = body
case []byte:
other["request_body"] = string(body)
default:
other["request_body"] = processedBody
}
} else {
requestBody, bodyErr := common.GetRequestBody(c)
if bodyErr == nil {
other["request_body"] = string(requestBody)
} else {
other["request_body_error"] = bodyErr.Error()
}
}
other["error_type"] = err.GetErrorType()
other["error_code"] = err.GetErrorCode()
other["status_code"] = err.StatusCode
Expand Down Expand Up @@ -454,8 +480,10 @@ func RelayTask(c *gin.Context) {
Retry: common.GetPointer(0),
}
for ; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && retryParam.GetRetry() < retryTimes; retryParam.IncreaseRetry() {
logger.LogDebug(c, "任务渠道重试开始: retry=%d/%d, current_channel=%d, model=%q, status=%d", retryParam.GetRetry(), retryTimes, channelId, relayInfo.OriginModelName, taskErr.StatusCode)
channel, newAPIError := getChannel(c, relayInfo, retryParam)
if newAPIError != nil {
logger.LogDebug(c, "任务渠道选择失败: retry=%d, err=%s", retryParam.GetRetry(), newAPIError.Error())
logger.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", newAPIError.Error()))
taskErr = service.TaskErrorWrapperLocal(newAPIError.Err, "get_channel_failed", http.StatusInternalServerError)
break
Expand All @@ -464,11 +492,13 @@ func RelayTask(c *gin.Context) {
useChannel := c.GetStringSlice("use_channel")
useChannel = append(useChannel, fmt.Sprintf("%d", channelId))
c.Set("use_channel", useChannel)
logger.LogDebug(c, "任务渠道选择成功: retry=%d, channel_id=%d, channel_type=%d, channel_name=%q", retryParam.GetRetry(), channel.Id, channel.Type, channel.Name)
logger.LogInfo(c, fmt.Sprintf("using channel #%d to retry (remain times %d)", channel.Id, retryParam.GetRetry()))
//middleware.SetupContextForSelectedChannel(c, channel, originalModel)

requestBody, err := common.GetRequestBody(c)
if err != nil {
logger.LogDebug(c, "任务读取请求体失败: retry=%d, err=%s", retryParam.GetRetry(), err.Error())
if common.IsRequestBodyTooLargeError(err) || errors.Is(err, common.ErrRequestBodyTooLarge) {
taskErr = service.TaskErrorWrapperLocal(err, "read_request_body_failed", http.StatusRequestEntityTooLarge)
} else {
Expand All @@ -478,6 +508,11 @@ func RelayTask(c *gin.Context) {
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
taskErr = taskRelayHandler(c, relayInfo)
if taskErr == nil {
logger.LogDebug(c, "任务渠道请求成功: retry=%d, channel_id=%d", retryParam.GetRetry(), channelId)
} else {
logger.LogDebug(c, "任务渠道请求失败: retry=%d, channel_id=%d, status=%d, code=%s, message=%s", retryParam.GetRetry(), channelId, taskErr.StatusCode, taskErr.Code, taskErr.Message)
}
}
useChannel := c.GetStringSlice("use_channel")
if len(useChannel) > 1 {
Expand Down
2 changes: 2 additions & 0 deletions relay/audio_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relay/helper"
Expand Down Expand Up @@ -43,6 +44,7 @@ func AudioHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
if err != nil {
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
}
common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, common.GetJsonString(request))

resp, err := adaptor.DoRequest(c, info, ioReader)
if err != nil {
Expand Down
57 changes: 53 additions & 4 deletions relay/channel/api_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,54 @@ func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Hea
}
}

func maskHeaderOverrideValue(value string, apiKey string) string {
if apiKey == "" || value == "" {
return value
}
return strings.ReplaceAll(value, apiKey, "***")
}

func maskHeaderOverrideForLog(headerOverride map[string]interface{}, apiKey string) map[string]interface{} {
if len(headerOverride) == 0 {
return nil
}
masked := make(map[string]interface{}, len(headerOverride))
for key, value := range headerOverride {
if str, ok := value.(string); ok {
masked[key] = maskHeaderOverrideValue(str, apiKey)
} else {
masked[key] = value
}
}
return masked
}

func maskHeaderOverrideForLogString(headerOverride map[string]string, apiKey string) map[string]string {
if len(headerOverride) == 0 {
return nil
}
masked := make(map[string]string, len(headerOverride))
for key, value := range headerOverride {
masked[key] = maskHeaderOverrideValue(value, apiKey)
}
return masked
}

// processHeaderOverride 处理请求头覆盖,支持变量替换
// 支持的变量:{api_key}
func processHeaderOverride(info *common.RelayInfo) (map[string]string, error) {
func processHeaderOverride(c *gin.Context, info *common.RelayInfo) (map[string]string, error) {
channelId := 0
channelType := 0
if info != nil && info.ChannelMeta != nil {
channelId = info.ChannelId
channelType = info.ChannelType
}
headerOverride := make(map[string]string)
if len(info.HeadersOverride) == 0 {
logger.LogDebug(c, "请求头覆盖配置: channel_id=%d, channel_type=%d, headers=空", channelId, channelType)
} else {
logger.LogDebug(c, "请求头覆盖配置: channel_id=%d, channel_type=%d, headers=%s", channelId, channelType, common2.GetJsonString(maskHeaderOverrideForLog(info.HeadersOverride, info.ApiKey)))
}
for k, v := range info.HeadersOverride {
str, ok := v.(string)
if !ok {
Expand All @@ -55,6 +99,11 @@ func processHeaderOverride(info *common.RelayInfo) (map[string]string, error) {

headerOverride[k] = str
}
if len(headerOverride) == 0 {
logger.LogDebug(c, "请求头覆盖结果: channel_id=%d, channel_type=%d, headers=空", channelId, channelType)
} else {
logger.LogDebug(c, "请求头覆盖结果: channel_id=%d, channel_type=%d, headers=%s", channelId, channelType, common2.GetJsonString(maskHeaderOverrideForLogString(headerOverride, info.ApiKey)))
}
return headerOverride, nil
}

Expand All @@ -71,7 +120,7 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
return nil, fmt.Errorf("new request failed: %w", err)
}
headers := req.Header
headerOverride, err := processHeaderOverride(info)
headerOverride, err := processHeaderOverride(c, info)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -104,7 +153,7 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
// set form data
req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type"))
headers := req.Header
headerOverride, err := processHeaderOverride(info)
headerOverride, err := processHeaderOverride(c, info)
if err != nil {
return nil, err
}
Expand All @@ -128,7 +177,7 @@ func DoWssRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody
return nil, fmt.Errorf("get request url failed: %w", err)
}
targetHeader := http.Header{}
headerOverride, err := processHeaderOverride(info)
headerOverride, err := processHeaderOverride(c, info)
if err != nil {
return nil, err
}
Expand Down
4 changes: 4 additions & 0 deletions relay/channel/openai/adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type Adaptor struct {
// support OAI models: o1-mini/o3-mini/o4-mini/o1/o3 etc...
// minimal effort only available in gpt-5
func parseReasoningEffortFromModelSuffix(model string) (string, string) {
// gpt-5.2 系列保留模型后缀,不转换推理力度
if strings.HasPrefix(model, "gpt-5.2") {
return "", model
}
effortSuffixes := []string{"-high", "-minimal", "-low", "-medium", "-none", "-xhigh"}
for _, suffix := range effortSuffixes {
if strings.HasSuffix(model, suffix) {
Expand Down
2 changes: 2 additions & 0 deletions relay/claude_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(body))
requestBody = bytes.NewBuffer(body)
} else {
convertedRequest, err := adaptor.ConvertClaudeRequest(c, info, request)
Expand All @@ -130,6 +131,7 @@ func ClaudeHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
}
}

common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(jsonData))
if common.DebugEnabled {
println("requestBody: ", string(jsonData))
}
Expand Down
2 changes: 2 additions & 0 deletions relay/compatible_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(body))
if common.DebugEnabled {
println("requestBody: ", string(body))
}
Expand Down Expand Up @@ -176,6 +177,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
}
}

common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(jsonData))
logger.LogDebug(c, fmt.Sprintf("text request body: %s", string(jsonData)))

requestBody = bytes.NewBuffer(jsonData)
Expand Down
2 changes: 2 additions & 0 deletions relay/embedding_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/logger"
relaycommon "github.com/QuantumNous/new-api/relay/common"
Expand Down Expand Up @@ -58,6 +59,7 @@ func EmbeddingHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *
}
}

common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(jsonData))
logger.LogDebug(c, fmt.Sprintf("converted embedding request body: %s", string(jsonData)))
requestBody := bytes.NewBuffer(jsonData)
statusCodeMappingStr := c.GetString("status_code_mapping")
Expand Down
3 changes: 3 additions & 0 deletions relay/gemini_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(body))
requestBody = bytes.NewReader(body)
} else {
// 使用 ConvertGeminiRequest 转换请求格式
Expand All @@ -163,6 +164,7 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ
}
}

common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(jsonData))
logger.LogDebug(c, "Gemini request body: "+string(jsonData))

requestBody = bytes.NewReader(jsonData)
Expand Down Expand Up @@ -267,6 +269,7 @@ func GeminiEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo) (newAPI
return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid, types.ErrOptionWithSkipRetry())
}
}
common.SetContextKey(c, constant.ContextKeyProcessedRequestBody, string(jsonData))
logger.LogDebug(c, "Gemini embedding request body: "+string(jsonData))
requestBody = bytes.NewReader(jsonData)

Expand Down
Loading