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
17 changes: 16 additions & 1 deletion controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ type channelTestOptions struct {
Context context.Context
}

// isVideoOnlyChannelType reports whether a channel serves video generation
// exclusively through the async task pipeline (relay.GetTaskAdaptor) and has
// no /v1/chat/completions surface. Probing such a channel with the synchronous
// chat test request makes the upstream reject it, which in turn auto-disables
// a perfectly healthy channel.
func isVideoOnlyChannelType(channelType int) bool {
for _, endpointType := range common.GetEndpointTypesByChannelType(channelType, "") {
switch endpointType {
case constant.EndpointTypeOpenAIVideo, constant.EndpointTypeVideo, constant.EndpointTypeVideoToMusic:
return true
}
}
return false
}

func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointType string) string {
normalized := strings.TrimSpace(endpointType)
// Codex upstream only accepts the Responses protocol. An Anthropic channel
Expand Down Expand Up @@ -109,7 +124,7 @@ func testChannelWithOptions(channel *model.Channel, testUserID int, testModel st
constant.ChannelTypeDoubaoVideo,
constant.ChannelTypeVidu,
}
if lo.Contains(unsupportedTestChannelTypes, channel.Type) {
if lo.Contains(unsupportedTestChannelTypes, channel.Type) || isVideoOnlyChannelType(channel.Type) {
channelTypeName := constant.GetChannelTypeName(channel.Type)
return testResult{
localErr: fmt.Errorf("%s channel test is not supported", channelTypeName),
Expand Down
52 changes: 52 additions & 0 deletions controller/channel_test_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,58 @@ func TestNormalizeChannelTestEndpointCodexKeepsNonAnthropicProtocols(t *testing.
}
}

func TestIsVideoOnlyChannelTypeCoversAsyncVideoChannels(t *testing.T) {
videoOnlyChannelTypes := []int{
constant.ChannelTypeBytePlus,
constant.ChannelTypeTechMobiVideo,
constant.ChannelTypeXaiGrokVideo,
constant.ChannelTypeMiniMaxH3,
constant.ChannelTypeSonilo,
constant.ChannelTypeSora,
constant.ChannelTypeBlockRunVideo,
constant.ChannelTypeBlockRunSeedance,
}

for _, channelType := range videoOnlyChannelTypes {
t.Run(constant.GetChannelTypeName(channelType), func(t *testing.T) {
require.True(t, isVideoOnlyChannelType(channelType),
"video-only channels must not be probed with a text chat request")
})
}
}

func TestIsVideoOnlyChannelTypeExcludesChatChannels(t *testing.T) {
chatChannelTypes := []int{
constant.ChannelTypeOpenAI,
constant.ChannelTypeAnthropic,
constant.ChannelTypeGemini,
constant.ChannelTypeCodex,
constant.ChannelTypeVertexAi,
constant.ChannelTypeJina,
}

for _, channelType := range chatChannelTypes {
t.Run(constant.GetChannelTypeName(channelType), func(t *testing.T) {
require.False(t, isVideoOnlyChannelType(channelType))
})
}
}

func TestTestChannelSkipsVideoOnlyChannelsInsteadOfSendingChatRequest(t *testing.T) {
channel := &model.Channel{
Id: 120,
Name: "BytePlus Seedance 2.0",
Type: constant.ChannelTypeBytePlus,
}

result := testChannel(channel, 1, "", "", false)

require.Error(t, result.localErr)
require.Contains(t, result.localErr.Error(), "channel test is not supported")
require.Nil(t, result.newAPIError,
"an unsupported channel test must not surface as a channel error that trips auto-disable")
}

func TestBuildScheduledChannelTestAlertMarksAutoDisabled(t *testing.T) {
autoBan := 1
now := time.Date(2026, 6, 2, 13, 14, 15, 0, time.UTC)
Expand Down