Skip to content
Open
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
2 changes: 1 addition & 1 deletion common/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func GetEndpointTypesByChannelType(channelType int, modelName string) []constant
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI}
case constant.ChannelTypeXai:
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAI, constant.EndpointTypeOpenAIResponse}
case constant.ChannelTypeSora:
case constant.ChannelTypeSora, constant.ChannelTypeYike:
endpointTypes = []constant.EndpointType{constant.EndpointTypeOpenAIVideo}
case constant.ChannelTypeSub2API, constant.ChannelTypeNewAPI:
endpointTypes = []constant.EndpointType{
Expand Down
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const (
ChannelTypeAdvancedCustom = 58
ChannelTypeSub2API = 59
ChannelTypeNewAPI = 60
ChannelTypeYike = 61
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -124,6 +125,7 @@ var ChannelBaseURLs = []string{
"", //58
"", //59
"", //60
"https://yike.cn-shanghai.aliyuncs.com", //61
}

var ChannelTypeNames = map[int]string{
Expand Down Expand Up @@ -184,6 +186,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeAdvancedCustom: "Advanced Custom",
ChannelTypeSub2API: "Sub2API",
ChannelTypeNewAPI: "New API",
ChannelTypeYike: "Yike",
}

func GetChannelTypeName(channelType int) string {
Expand Down
33 changes: 31 additions & 2 deletions controller/channel-billing.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controller

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -12,6 +13,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
taskyike "github.com/QuantumNous/new-api/relay/channel/task/yike"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/operation_setting"
Expand Down Expand Up @@ -356,6 +358,27 @@ func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) {
return availableBalanceUsd, nil
}

func updateChannelYikeBalance(channel *model.Channel) (float64, error) {
client, err := service.GetHttpClientWithProxy(channel.GetSetting().Proxy)
if err != nil {
return 0, err
}
credit, err := taskyike.FetchAccountCredit(context.Background(), channel.GetBaseURL(), channel.Key, client)
if err != nil {
return 0, err
}
balance := credit.Remaining.InexactFloat64()
channel.UpdateBalance(balance)
return balance, nil
}

func channelBalanceUnit(channelType int) string {
if channelType == constant.ChannelTypeYike {
return "credits"
}
return ""
}

func updateChannelBalance(channel *model.Channel) (float64, error) {
baseURL := constant.ChannelBaseURLs[channel.Type]
if channel.GetBaseURL() == "" {
Expand Down Expand Up @@ -386,6 +409,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
return updateChannelOpenRouterBalance(channel)
case constant.ChannelTypeMoonshot:
return updateChannelMoonshotBalance(channel)
case constant.ChannelTypeYike:
return updateChannelYikeBalance(channel)
default:
return 0, errors.New("尚未实现")
}
Expand Down Expand Up @@ -444,11 +469,15 @@ func UpdateChannelBalance(c *gin.Context) {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
response := gin.H{
"success": true,
"message": "",
"balance": balance,
})
}
if unit := channelBalanceUnit(channel.Type); unit != "" {
response["unit"] = unit
}
c.JSON(http.StatusOK, response)
}

func updateAllChannelsBalance() error {
Expand Down
27 changes: 27 additions & 0 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/pkg/billingexpr"
"github.com/QuantumNous/new-api/relay"
taskyike "github.com/QuantumNous/new-api/relay/channel/task/yike"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relay/helper"
Expand Down Expand Up @@ -78,6 +79,9 @@ func testChannel(ctx context.Context, channel *model.Channel, testUserID int, te
ctx = context.Background()
}
tik := time.Now()
if channel.Type == constant.ChannelTypeYike {
return testYikeChannel(ctx, channel)
}
var unsupportedTestChannelTypes = []int{
constant.ChannelTypeMidjourney,
constant.ChannelTypeMidjourneyPlus,
Expand Down Expand Up @@ -527,6 +531,29 @@ func testChannel(ctx context.Context, channel *model.Channel, testUserID int, te
}
}

func testYikeChannel(ctx context.Context, channel *model.Channel) testResult {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
key, _, keyErr := channel.GetNextEnabledKey()
var err error
if keyErr != nil {
err = keyErr
} else if strings.TrimSpace(key) == "" {
err = fmt.Errorf("Yike channel key is empty")
} else {
common.SetContextKey(c, constant.ContextKeyChannelKey, key)
err = taskyike.CheckChannelAccountCredit(ctx, channel.GetBaseURL(), key, channel.GetSetting().Proxy)
}
if err == nil {
return testResult{context: c}
}
return testResult{
context: c,
localErr: err,
newAPIError: types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusBadGateway),
}
}

func attachTestBillingRequestInput(info *relaycommon.RelayInfo, request dto.Request) error {
if info == nil {
return nil
Expand Down
11 changes: 11 additions & 0 deletions controller/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/QuantumNous/new-api/model"
relaychannel "github.com/QuantumNous/new-api/relay/channel"
"github.com/QuantumNous/new-api/relay/channel/ollama"
taskyike "github.com/QuantumNous/new-api/relay/channel/task/yike"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/service"
Expand Down Expand Up @@ -484,6 +485,16 @@ func validateChannel(channel *model.Channel, isAdd bool) error {
if channel.Type == constant.ChannelTypeNewAPI && strings.TrimSpace(channel.GetBaseURL()) == "" {
return fmt.Errorf("New API channel base URL cannot be empty")
}
if channel.Type == constant.ChannelTypeYike {
if err := taskyike.ValidateChannelEndpoint(channel.GetBaseURL()); err != nil {
return err
}
if isAdd || strings.TrimSpace(channel.Key) != "" {
if err := taskyike.ValidateChannelCredentials(channel.Key); err != nil {
return err
}
}
}

// 如果是添加操作,检查 channel 和 key 是否为空
if isAdd {
Expand Down
13 changes: 13 additions & 0 deletions controller/channel_billing_yike_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controller

import (
"testing"

"github.com/QuantumNous/new-api/constant"
"github.com/stretchr/testify/assert"
)

func TestChannelBalanceUnit(t *testing.T) {
assert.Equal(t, "credits", channelBalanceUnit(constant.ChannelTypeYike))
assert.Empty(t, channelBalanceUnit(constant.ChannelTypeOpenAI))
}
5 changes: 5 additions & 0 deletions controller/channel_upstream_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/advancedcustom"
"github.com/QuantumNous/new-api/relay/channel/gemini"
"github.com/QuantumNous/new-api/relay/channel/ollama"
taskyike "github.com/QuantumNous/new-api/relay/channel/task/yike"
relaycommon "github.com/QuantumNous/new-api/relay/common"
relayconstant "github.com/QuantumNous/new-api/relay/constant"
"github.com/QuantumNous/new-api/relaykit/dto"
Expand Down Expand Up @@ -337,6 +338,10 @@ func fetchChannelUpstreamModelIDs(channel *model.Channel) ([]string, error) {
if channel.GetBaseURL() != "" {
baseURL = channel.GetBaseURL()
}
if channel.Type == constant.ChannelTypeYike {
// Yike exposes task RPCs but no OpenAI-compatible /v1/models endpoint.
return (&taskyike.TaskAdaptor{}).GetModelList(), nil
}

if channel.Type == constant.ChannelTypeOllama {
key := strings.TrimSpace(strings.Split(channel.Key, "\n")[0])
Expand Down
25 changes: 25 additions & 0 deletions controller/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package controller
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -97,8 +98,32 @@ func init() {
for i := 1; i <= constant.ChannelTypeDummy; i++ {
apiType, success := common.ChannelType2APIType(i)
if !success || apiType == constant.APITypeAIProxyLibrary {
if i != constant.ChannelTypeYike {
continue
}

// Yike is intentionally task-only, so it has no Chat APIType adaptor.
meta := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{ChannelType: i}}
taskAdaptor := relay.GetTaskAdaptor(constant.TaskPlatform(strconv.Itoa(i)))
if taskAdaptor == nil {
continue
}
taskAdaptor.Init(meta)
models := taskAdaptor.GetModelList()
channelId2Models[i] = models
for _, modelName := range models {
aiModel := dto.OpenAIModels{
Id: modelName,
Object: "model",
Created: 1626777600,
OwnedBy: taskAdaptor.GetChannelName(),
}
openAIModels = append(openAIModels, aiModel)
openAIModelsMap[modelName] = aiModel
}
continue
}

meta := &relaycommon.RelayInfo{ChannelMeta: &relaycommon.ChannelMeta{
ChannelType: i,
}}
Expand Down
7 changes: 6 additions & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ func RelayTask(c *gin.Context) {

if lockedCh, ok := relayInfo.LockedChannel.(*model.Channel); ok && lockedCh != nil {
channel = lockedCh
if retryParam.GetRetry() > 0 {
if retryParam.GetRetry() > 0 && channel.Type != constant.ChannelTypeYike {
if setupErr := middleware.SetupContextForSelectedChannel(c, channel, relayInfo.OriginModelName); setupErr != nil {
taskErr = service.TaskErrorWrapperLocal(setupErr.Err, "setup_locked_channel_failed", http.StatusInternalServerError)
break
Expand All @@ -541,6 +541,11 @@ func RelayTask(c *gin.Context) {
break
}
}
if channel.Type == constant.ChannelTypeYike && relayInfo.LockedChannel == nil {
// A Yike submit may have reached the provider even when its response is
// ambiguous. Keep the same account and already-selected AK|SK on retry.
relayInfo.LockedChannel = channel
}

addUsedChannel(c, channel.Id)
bodyStorage, bodyErr := common.GetBodyStorage(c)
Expand Down
24 changes: 19 additions & 5 deletions controller/video_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting/system_setting"

Expand Down Expand Up @@ -125,6 +126,10 @@ func VideoProxy(c *gin.Context) {
videoProxyError(c, http.StatusBadGateway, "server_error", "Failed to fetch video content")
return
}
logVideoURL := videoURL
if channel.Type == constant.ChannelTypeYike {
logVideoURL = relaycommon.SanitizeURLForLog(videoURL)
}

if strings.HasPrefix(videoURL, "data:") {
if err := writeVideoDataURL(c, videoURL); err != nil {
Expand All @@ -142,28 +147,37 @@ func VideoProxy(c *gin.Context) {
validateErr = common.ValidateURLWithFetchSetting(videoURL, fetchSetting.EnableSSRFProtection, fetchSetting.AllowPrivateIp, fetchSetting.DomainFilterMode, fetchSetting.IpFilterMode, fetchSetting.DomainList, fetchSetting.IpList, fetchSetting.AllowedPorts, fetchSetting.ApplyIPFilterForDomain)
}
if validateErr != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, validateErr))
videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", validateErr))
if channel.Type == constant.ChannelTypeYike {
logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s", taskID))
videoProxyError(c, http.StatusForbidden, "server_error", "request blocked by security policy")
} else {
logger.LogError(c.Request.Context(), fmt.Sprintf("Video URL blocked for task %s: %v", taskID, validateErr))
videoProxyError(c, http.StatusForbidden, "server_error", fmt.Sprintf("request blocked: %v", validateErr))
}
return
}

req.URL, err = url.Parse(videoURL)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to parse URL %s: %s", videoURL, err.Error()))
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to parse URL %s: %s", logVideoURL, err.Error()))
videoProxyError(c, http.StatusInternalServerError, "server_error", "Failed to create proxy request")
return
}

resp, err := client.Do(req)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to fetch video from %s: %s", videoURL, err.Error()))
errorMessage := err.Error()
if channel.Type == constant.ChannelTypeYike {
errorMessage = relaycommon.SanitizeErrorForLog(err)
}
logger.LogError(c.Request.Context(), fmt.Sprintf("Failed to fetch video from %s: %s", logVideoURL, errorMessage))
videoProxyError(c, http.StatusBadGateway, "server_error", "Failed to fetch video content")
return
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
logger.LogError(c.Request.Context(), fmt.Sprintf("Upstream returned status %d for %s", resp.StatusCode, videoURL))
logger.LogError(c.Request.Context(), fmt.Sprintf("Upstream returned status %d for %s", resp.StatusCode, logVideoURL))
videoProxyError(c, http.StatusBadGateway, "server_error",
fmt.Sprintf("Upstream service returned status %d", resp.StatusCode))
return
Expand Down
Loading