Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
198 lines
7.1 KiB
Go
198 lines
7.1 KiB
Go
package relay
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/relay/helper"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/relaykit/types"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/model_setting"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
|
|
info.InitChannelMeta(c)
|
|
info.BillingImageCount = nil
|
|
info.ImageRequestCount = 0
|
|
|
|
imageReq, ok := info.Request.(*dto.ImageRequest)
|
|
if !ok {
|
|
return types.NewErrorWithStatusCode(fmt.Errorf("invalid request type, expected dto.ImageRequest, got %T", info.Request), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
request, err := common.DeepCopy(imageReq)
|
|
if err != nil {
|
|
return types.NewError(fmt.Errorf("failed to copy request to ImageRequest: %w", err), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
err = helper.ModelMappedHelper(c, info, request)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeChannelModelMappedError, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
adaptor := GetAdaptor(info.ApiType)
|
|
if adaptor == nil {
|
|
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
|
|
}
|
|
adaptor.Init(info)
|
|
imageCount, err := request.ImageCount(info.ChannelType == constant.ChannelTypeAli)
|
|
if err != nil {
|
|
return types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
promptExtend := request.BillingParameters != nil && request.BillingParameters.PromptExtend != nil && *request.BillingParameters.PromptExtend
|
|
|
|
var requestBody io.Reader
|
|
var jsonData []byte
|
|
|
|
if model_setting.GetGlobalSettings().PassThroughRequestEnabled || info.ChannelSetting.PassThroughBodyEnabled {
|
|
storage, err := common.GetBodyStorage(c)
|
|
if err != nil {
|
|
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
|
requestBody = common.NewReplayableBodyReader(storage)
|
|
} else {
|
|
jsonData, err = storage.Bytes()
|
|
if err != nil {
|
|
return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
}
|
|
} else {
|
|
convertedRequest, err := adaptor.ConvertImageRequest(c, info, *request)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed)
|
|
}
|
|
relaycommon.AppendRequestConversionFromRequest(info, convertedRequest)
|
|
|
|
switch convertedRequest.(type) {
|
|
case *bytes.Buffer:
|
|
requestBody = convertedRequest.(io.Reader)
|
|
default:
|
|
jsonData, err = common.Marshal(convertedRequest)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
|
}
|
|
|
|
// apply param override
|
|
if len(info.ParamOverride) > 0 {
|
|
jsonData, err = relaycommon.ApplyParamOverrideWithRelayInfo(jsonData, info)
|
|
if err != nil {
|
|
return newAPIErrorFromParamOverride(err)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
if jsonData != nil {
|
|
// This is a different trust boundary from ingress: channel overrides
|
|
// and pass-through bodies can change the quantity actually submitted.
|
|
var outbound struct {
|
|
N *uint `json:"n"`
|
|
Parameters *dto.ImageBillingParameters `json:"parameters"`
|
|
}
|
|
if err := common.Unmarshal(jsonData, &outbound); err != nil {
|
|
return types.NewErrorWithStatusCode(fmt.Errorf("invalid image billing parameters: %w", err), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
quantityRequest := dto.ImageRequest{N: outbound.N, BillingParameters: outbound.Parameters}
|
|
if quantityRequest.N == nil {
|
|
quantityRequest.N = common.GetPointer(uint(imageCount))
|
|
}
|
|
imageCount, err = quantityRequest.ImageCount(info.ChannelType == constant.ChannelTypeAli)
|
|
if err != nil {
|
|
return types.NewErrorWithStatusCode(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
|
|
}
|
|
promptExtend = outbound.Parameters != nil && outbound.Parameters.PromptExtend != nil && *outbound.Parameters.PromptExtend
|
|
if info.ChannelType == constant.ChannelTypeAli {
|
|
// Always send the same explicit quantity that is reserved, including
|
|
// when an empty parameters object accompanies a top-level n.
|
|
jsonData, err = sjson.SetBytes(jsonData, "parameters.n", imageCount)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
|
}
|
|
}
|
|
logger.LogDebug(c, "image request body: %s", jsonData)
|
|
body, closer, err := relaycommon.NewOutboundJSONBody(jsonData)
|
|
if err != nil {
|
|
return types.NewError(err, types.ErrorCodeConvertRequestFailed, types.ErrOptionWithSkipRetry())
|
|
}
|
|
defer closer.Close()
|
|
requestBody = body
|
|
}
|
|
if billingErr := service.PrepareImageBillingForRequest(c, info, imageCount, promptExtend); billingErr != nil {
|
|
return billingErr
|
|
}
|
|
|
|
statusCodeMappingStr := c.GetString("status_code_mapping")
|
|
|
|
resp, err := adaptor.DoRequest(c, info, requestBody)
|
|
if err != nil {
|
|
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
|
|
}
|
|
var httpResp *http.Response
|
|
if resp != nil {
|
|
httpResp = resp.(*http.Response)
|
|
info.IsStream = info.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream")
|
|
if httpResp.StatusCode != http.StatusOK {
|
|
if httpResp.StatusCode == http.StatusCreated && info.ApiType == constant.APITypeReplicate {
|
|
// replicate channel returns 201 Created when using Prefer: wait, treat it as success.
|
|
httpResp.StatusCode = http.StatusOK
|
|
} else {
|
|
newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
|
|
// reset status code 重置状态码
|
|
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
|
|
return newAPIError
|
|
}
|
|
}
|
|
}
|
|
|
|
usage, newAPIError := adaptor.DoResponse(c, httpResp, info)
|
|
if newAPIError != nil {
|
|
// reset status code 重置状态码
|
|
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
|
|
return newAPIError
|
|
}
|
|
|
|
imageN := uint(1)
|
|
if request.N != nil {
|
|
imageN = *request.N
|
|
}
|
|
|
|
if usage.(*dto.Usage).TotalTokens == 0 {
|
|
usage.(*dto.Usage).TotalTokens = 1
|
|
}
|
|
if usage.(*dto.Usage).PromptTokens == 0 {
|
|
usage.(*dto.Usage).PromptTokens = 1
|
|
}
|
|
|
|
quality := request.Quality
|
|
if quality == "" {
|
|
quality = "standard"
|
|
}
|
|
|
|
var logContent []string
|
|
|
|
if len(request.Size) > 0 {
|
|
logContent = append(logContent, fmt.Sprintf("大小 %s", request.Size))
|
|
}
|
|
if len(quality) > 0 {
|
|
logContent = append(logContent, fmt.Sprintf("品质 %s", quality))
|
|
}
|
|
if imageN > 0 {
|
|
logContent = append(logContent, fmt.Sprintf("生成数量 %d", imageN))
|
|
}
|
|
|
|
service.PostTextConsumeQuota(c, info, usage.(*dto.Usage), logContent)
|
|
return nil
|
|
}
|