| @@ -191,10 +191,11 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { | |||||
| }() | }() | ||||
| retryParam := &service.RetryParam{ | retryParam := &service.RetryParam{ | ||||
| Ctx: c, | |||||
| TokenGroup: relayInfo.TokenGroup, | |||||
| ModelName: relayInfo.OriginModelName, | |||||
| Retry: common.GetPointer(0), | |||||
| Ctx: c, | |||||
| TokenGroup: relayInfo.TokenGroup, | |||||
| ModelName: relayInfo.OriginModelName, | |||||
| Retry: common.GetPointer(0), | |||||
| RequireMatrixUsageBilling: relayInfo.RequireMatrixUsageBilling, | |||||
| } | } | ||||
| for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { | for ; retryParam.GetRetry() <= common.RetryTimes; retryParam.IncreaseRetry() { | ||||
| @@ -314,6 +315,9 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service | |||||
| info.PriceData.GroupRatioInfo = helper.HandleGroupRatio(c, info) | info.PriceData.GroupRatioInfo = helper.HandleGroupRatio(c, info) | ||||
| if err != nil { | if err != nil { | ||||
| if apiErr, ok := err.(*types.NewAPIError); ok { | |||||
| return nil, apiErr | |||||
| } | |||||
| return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, info.OriginModelName, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) | return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, info.OriginModelName, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) | ||||
| } | } | ||||
| if channel == nil { | if channel == nil { | ||||
| @@ -0,0 +1,60 @@ | |||||
| package helper | |||||
| import ( | |||||
| "errors" | |||||
| commonpkg "github.com/QuantumNous/new-api/common" | |||||
| "github.com/QuantumNous/new-api/constant" | |||||
| ) | |||||
| type MatrixUsageCapability struct { | |||||
| BillingModelName string | |||||
| UpstreamModelName string | |||||
| ChannelType int | |||||
| } | |||||
| var matrixUsageCapabilities = []MatrixUsageCapability{ | |||||
| {BillingModelName: "seedance-2", UpstreamModelName: "seedance-2", ChannelType: constant.ChannelTypeDoubaoVideo}, | |||||
| {BillingModelName: "doubao-video", UpstreamModelName: "doubao-video", ChannelType: constant.ChannelTypeDoubaoVideo}, | |||||
| {BillingModelName: "doubao-video-seedance", UpstreamModelName: "doubao-video-seedance", ChannelType: constant.ChannelTypeDoubaoVideo}, | |||||
| } | |||||
| func SupportsMatrixUsageBilling(modelName string, channelType int, mappedModel string) bool { | |||||
| if mappedModel == "" { | |||||
| mappedModel = modelName | |||||
| } | |||||
| for _, capability := range matrixUsageCapabilities { | |||||
| if capability.BillingModelName == modelName && | |||||
| capability.UpstreamModelName == mappedModel && | |||||
| capability.ChannelType == channelType { | |||||
| return true | |||||
| } | |||||
| } | |||||
| return false | |||||
| } | |||||
| func ResolvePricingModelMapping(modelName string, modelMapping string) (string, error) { | |||||
| if modelMapping == "" || modelMapping == "{}" { | |||||
| return modelName, nil | |||||
| } | |||||
| modelMap := map[string]string{} | |||||
| if err := commonpkg.Unmarshal([]byte(modelMapping), &modelMap); err != nil { | |||||
| return "", err | |||||
| } | |||||
| current := modelName | |||||
| visited := map[string]bool{current: true} | |||||
| for { | |||||
| next := modelMap[current] | |||||
| if next == "" { | |||||
| return current, nil | |||||
| } | |||||
| if visited[next] { | |||||
| if next == current { | |||||
| return current, nil | |||||
| } | |||||
| return "", errors.New("model_mapping_contains_cycle") | |||||
| } | |||||
| visited[next] = true | |||||
| current = next | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,27 @@ | |||||
| package helper | |||||
| import ( | |||||
| "testing" | |||||
| "github.com/QuantumNous/new-api/constant" | |||||
| "github.com/stretchr/testify/require" | |||||
| ) | |||||
| func TestSupportsMatrixUsageBilling(t *testing.T) { | |||||
| require.True(t, SupportsMatrixUsageBilling("seedance-2", constant.ChannelTypeDoubaoVideo, "seedance-2")) | |||||
| require.False(t, SupportsMatrixUsageBilling("seedance-2", constant.ChannelTypeOpenAI, "seedance-2")) | |||||
| require.False(t, SupportsMatrixUsageBilling("unknown", constant.ChannelTypeDoubaoVideo, "unknown")) | |||||
| } | |||||
| func TestResolvePricingModelMapping(t *testing.T) { | |||||
| mapped, err := ResolvePricingModelMapping("seedance-2", `{"seedance-2":"doubao-video-seedance"}`) | |||||
| require.NoError(t, err) | |||||
| require.Equal(t, "doubao-video-seedance", mapped) | |||||
| mapped, err = ResolvePricingModelMapping("seedance-2", `{"seedance-2":"a","a":"b"}`) | |||||
| require.NoError(t, err) | |||||
| require.Equal(t, "b", mapped) | |||||
| _, err = ResolvePricingModelMapping("seedance-2", `{"seedance-2":"a","a":"seedance-2"}`) | |||||
| require.ErrorContains(t, err, "model_mapping_contains_cycle") | |||||
| } | |||||
| @@ -2,21 +2,26 @@ package service | |||||
| import ( | import ( | ||||
| "errors" | "errors" | ||||
| "fmt" | |||||
| "net/http" | |||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/constant" | "github.com/QuantumNous/new-api/constant" | ||||
| "github.com/QuantumNous/new-api/logger" | "github.com/QuantumNous/new-api/logger" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/relay/helper" | |||||
| "github.com/QuantumNous/new-api/setting" | "github.com/QuantumNous/new-api/setting" | ||||
| "github.com/QuantumNous/new-api/types" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| ) | ) | ||||
| type RetryParam struct { | type RetryParam struct { | ||||
| Ctx *gin.Context | |||||
| TokenGroup string | |||||
| ModelName string | |||||
| Retry *int | |||||
| resetNextTry bool | |||||
| Ctx *gin.Context | |||||
| TokenGroup string | |||||
| ModelName string | |||||
| Retry *int | |||||
| RequireMatrixUsageBilling bool | |||||
| resetNextTry bool | |||||
| } | } | ||||
| func (p *RetryParam) GetRetry() int { | func (p *RetryParam) GetRetry() int { | ||||
| @@ -158,5 +163,19 @@ func CacheGetRandomSatisfiedChannel(param *RetryParam) (*model.Channel, string, | |||||
| return nil, param.TokenGroup, err | return nil, param.TokenGroup, err | ||||
| } | } | ||||
| } | } | ||||
| if channel != nil && param.RequireMatrixUsageBilling { | |||||
| mappedModel, mapErr := helper.ResolvePricingModelMapping(param.ModelName, channel.GetModelMapping()) | |||||
| if mapErr != nil { | |||||
| return nil, selectGroup, mapErr | |||||
| } | |||||
| if !helper.SupportsMatrixUsageBilling(param.ModelName, channel.Type, mappedModel) { | |||||
| return nil, selectGroup, types.NewErrorWithStatusCode( | |||||
| fmt.Errorf("matrix_usage_billing_channel_unavailable"), | |||||
| types.ErrorCode("matrix_usage_billing_channel_unavailable"), | |||||
| http.StatusBadRequest, | |||||
| types.ErrOptionWithSkipRetry(), | |||||
| ) | |||||
| } | |||||
| } | |||||
| return channel, selectGroup, nil | return channel, selectGroup, nil | ||||
| } | } | ||||