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
33 changes: 17 additions & 16 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@ const (
ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry"

/* channel related keys */
ContextKeyChannelId ContextKey = "channel_id"
ContextKeyChannelName ContextKey = "channel_name"
ContextKeyChannelCreateTime ContextKey = "channel_create_time"
ContextKeyChannelBaseUrl ContextKey = "base_url"
ContextKeyChannelType ContextKey = "channel_type"
ContextKeyChannelSetting ContextKey = "channel_setting"
ContextKeyChannelOtherSetting ContextKey = "channel_other_setting"
ContextKeyChannelParamOverride ContextKey = "param_override"
ContextKeyChannelHeaderOverride ContextKey = "header_override"
ContextKeyChannelOrganization ContextKey = "channel_organization"
ContextKeyChannelAutoBan ContextKey = "auto_ban"
ContextKeyChannelModelMapping ContextKey = "model_mapping"
ContextKeyChannelStatusCodeMapping ContextKey = "status_code_mapping"
ContextKeyChannelIsMultiKey ContextKey = "channel_is_multi_key"
ContextKeyChannelMultiKeyIndex ContextKey = "channel_multi_key_index"
ContextKeyChannelKey ContextKey = "channel_key"
ContextKeyChannelId ContextKey = "channel_id"
ContextKeyChannelName ContextKey = "channel_name"
ContextKeyChannelCreateTime ContextKey = "channel_create_time"
ContextKeyChannelBaseUrl ContextKey = "base_url"
ContextKeyChannelType ContextKey = "channel_type"
ContextKeyChannelSetting ContextKey = "channel_setting"
ContextKeyChannelOtherSetting ContextKey = "channel_other_setting"
ContextKeyChannelParamOverride ContextKey = "param_override"
ContextKeyChannelParamOverrideContext ContextKey = "param_override_context"
ContextKeyChannelHeaderOverride ContextKey = "header_override"
ContextKeyChannelOrganization ContextKey = "channel_organization"
ContextKeyChannelAutoBan ContextKey = "auto_ban"
ContextKeyChannelModelMapping ContextKey = "model_mapping"
ContextKeyChannelStatusCodeMapping ContextKey = "status_code_mapping"
ContextKeyChannelIsMultiKey ContextKey = "channel_is_multi_key"
ContextKeyChannelMultiKeyIndex ContextKey = "channel_multi_key_index"
ContextKeyChannelKey ContextKey = "channel_key"

ContextKeyAutoGroup ContextKey = "auto_group"
ContextKeyAutoGroupIndex ContextKey = "auto_group_index"
Expand Down
80 changes: 72 additions & 8 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,65 @@ type testResult struct {
newAPIError *types.NewAPIError
}

func channelTypeDefaultsToStreamTest(channel *model.Channel) bool {
return channel != nil && channel.Type == constant.ChannelTypeCodex
}

func getStoredChannelTestStream(channel *model.Channel) (bool, bool) {
if channel == nil {
return false, false
}
raw := strings.TrimSpace(channel.OtherSettings)
if raw == "" {
return false, false
}

settings := map[string]interface{}{}
if err := common.UnmarshalJsonStr(raw, &settings); err != nil {
return false, false
}

value, ok := settings["test_stream_enabled"]
if !ok {
return false, false
}

switch typed := value.(type) {
case bool:
return typed, true
case string:
lower := strings.ToLower(strings.TrimSpace(typed))
if lower == "true" {
return true, true
}
if lower == "false" {
return false, true
}
}

return false, false
}

func resolveChannelTestStream(channel *model.Channel, streamOverride *bool) bool {
if streamOverride != nil {
return *streamOverride
}
if stored, ok := getStoredChannelTestStream(channel); ok {
return stored
}
return channelTypeDefaultsToStreamTest(channel)
}

func shouldSkipChannelAutoTest(channel *model.Channel, includeAutoDisabled bool) bool {
if channel == nil {
return true
}
if channel.Status == common.ChannelStatusManuallyDisabled {
return true
}
return channel.Status == common.ChannelStatusAutoDisabled && !includeAutoDisabled
}

func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointType string) string {
normalized := strings.TrimSpace(endpointType)
if normalized != "" {
Expand All @@ -56,7 +115,7 @@ func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointTyp
return normalized
}

func testChannel(channel *model.Channel, testModel string, endpointType string, isStream bool) testResult {
func testChannel(channel *model.Channel, testModel string, endpointType string, streamOverride *bool) testResult {
tik := time.Now()
var unsupportedTestChannelTypes = []int{
constant.ChannelTypeMidjourney,
Expand Down Expand Up @@ -134,6 +193,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
if strings.HasPrefix(requestPath, "/v1/responses/compact") {
testModel = ratio_setting.WithCompactModelSuffix(testModel)
}
isStream := resolveChannelTestStream(channel, streamOverride)

c.Request = &http.Request{
Method: "POST",
Expand Down Expand Up @@ -752,9 +812,13 @@ func TestChannel(c *gin.Context) {
//}()
testModel := c.Query("model")
endpointType := c.Query("endpoint_type")
isStream, _ := strconv.ParseBool(c.Query("stream"))
var streamOverride *bool
if raw, exists := c.GetQuery("stream"); exists {
parsed, _ := strconv.ParseBool(raw)
streamOverride = lo.ToPtr(parsed)
}
tik := time.Now()
result := testChannel(channel, testModel, endpointType, isStream)
result := testChannel(channel, testModel, endpointType, streamOverride)
if result.localErr != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
Expand Down Expand Up @@ -785,7 +849,7 @@ func TestChannel(c *gin.Context) {
var testAllChannelsLock sync.Mutex
var testAllChannelsRunning bool = false

func testAllChannels(notify bool) error {
func testAllChannels(notify bool, includeAutoDisabled bool) error {

testAllChannelsLock.Lock()
if testAllChannelsRunning {
Expand All @@ -811,12 +875,12 @@ func testAllChannels(notify bool) error {
}()

for _, channel := range channels {
if channel.Status == common.ChannelStatusManuallyDisabled {
if shouldSkipChannelAutoTest(channel, includeAutoDisabled) {
continue
}
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
tik := time.Now()
result := testChannel(channel, "", "", false)
result := testChannel(channel, "", "", nil)
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()

Expand Down Expand Up @@ -858,7 +922,7 @@ func testAllChannels(notify bool) error {
}

func TestAllChannels(c *gin.Context) {
err := testAllChannels(true)
err := testAllChannels(true, true)
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -887,7 +951,7 @@ func AutomaticallyTestChannels() {
time.Sleep(time.Duration(int(math.Round(frequency))) * time.Minute)
common.SysLog(fmt.Sprintf("automatically test channels with interval %f minutes", frequency))
common.SysLog("automatically testing all channels")
_ = testAllChannels(false)
_ = testAllChannels(false, operation_setting.GetMonitorSetting().AutoTestAutoDisabledChannelsEnabled)
common.SysLog("automatically channel test finished")
if !operation_setting.GetMonitorSetting().AutoTestChannelEnabled {
break
Expand Down
109 changes: 109 additions & 0 deletions controller/channel_test_logic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package controller

import (
"testing"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
)

func TestResolveChannelTestStream(t *testing.T) {
settingsBytes, err := common.Marshal(dto.ChannelOtherSettings{
TestStreamEnabled: true,
})
if err != nil {
t.Fatalf("marshal settings failed: %v", err)
}

channel := &model.Channel{OtherSettings: string(settingsBytes)}
if !resolveChannelTestStream(channel, nil) {
t.Fatal("expected channel default stream test setting to be used when override is nil")
}

overrideFalse := false
if resolveChannelTestStream(channel, &overrideFalse) {
t.Fatal("expected explicit false override to disable stream test")
}

overrideTrue := true
if !resolveChannelTestStream(channel, &overrideTrue) {
t.Fatal("expected explicit true override to enable stream test")
}

codexChannel := &model.Channel{Type: constant.ChannelTypeCodex}
if !resolveChannelTestStream(codexChannel, nil) {
t.Fatal("expected codex channels without stored setting to default to stream test")
}

codexChannelWithExplicitFalse := &model.Channel{
Type: constant.ChannelTypeCodex,
OtherSettings: `{"test_stream_enabled":false}`,
}
if resolveChannelTestStream(codexChannelWithExplicitFalse, nil) {
t.Fatal("expected explicit false stream setting to override codex default")
}

nonCodexChannel := &model.Channel{Type: 1}
if resolveChannelTestStream(nonCodexChannel, nil) {
t.Fatal("expected non-codex channels without stored setting to default to non-stream test")
}
}

func TestShouldSkipChannelAutoTest(t *testing.T) {
tests := []struct {
name string
channel *model.Channel
includeAutoDisabled bool
want bool
}{
{
name: "nil channel",
channel: nil,
includeAutoDisabled: true,
want: true,
},
{
name: "manual disabled always skipped",
channel: &model.Channel{
Status: common.ChannelStatusManuallyDisabled,
},
includeAutoDisabled: true,
want: true,
},
{
name: "auto disabled skipped when disabled in monitor setting",
channel: &model.Channel{
Status: common.ChannelStatusAutoDisabled,
},
includeAutoDisabled: false,
want: true,
},
{
name: "auto disabled included when enabled in monitor setting",
channel: &model.Channel{
Status: common.ChannelStatusAutoDisabled,
},
includeAutoDisabled: true,
want: false,
},
{
name: "enabled channel is included",
channel: &model.Channel{
Status: common.ChannelStatusEnabled,
},
includeAutoDisabled: false,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := shouldSkipChannelAutoTest(tt.channel, tt.includeAutoDisabled)
if got != tt.want {
t.Fatalf("shouldSkipChannelAutoTest() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
if openaiErr == nil {
return false
}
if service.ShouldSkipRetryAfterChannelAffinityFailure(c) {
if service.ShouldSkipRetryAfterChannelAffinityError(c, openaiErr) {
return false
}
if types.IsChannelError(openaiErr) {
Expand Down
1 change: 1 addition & 0 deletions dto/channel_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ChannelOtherSettings struct {
AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私)
DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用)
AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护)
TestStreamEnabled bool `json:"test_stream_enabled,omitempty"` // 渠道测试默认是否使用流式请求
AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"`
UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新
UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新
Expand Down
2 changes: 1 addition & 1 deletion middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func Distribute() func(c *gin.Context) {
preferred, err := model.CacheGetChannel(preferredChannelID)
if err == nil && preferred != nil {
if preferred.Status != common.ChannelStatusEnabled {
if service.ShouldSkipRetryAfterChannelAffinityFailure(c) {
if service.ShouldSkipRetryAfterChannelAffinityDisabledChannel(c) {
abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled))
return
}
Expand Down
Loading