From 879d8d3295b2f09d9ebc5ded4fa949ade851592a Mon Sep 17 00:00:00 2001 From: Tim Wang Date: Wed, 22 Apr 2026 12:30:56 +0800 Subject: [PATCH 1/4] feat: per-model testing and disable in channel test Instead of testing one representative model per channel and disabling the entire channel on failure, testAllChannels() now iterates every model individually. Model-level errors (timeout, unsupported) only disable that model's ability; channel-level errors (invalid key, quota) still disable the entire channel. Each model test has 120s timeout protection and records its own test history entry. Auto-enable respects the AutomaticEnableChannelEnabled setting. Co-Authored-By: Claude Opus 4.6 --- controller/channel-test.go | 139 ++++++++++++++++++++++++++++------ model/ability.go | 11 +++ model/channel_test_history.go | 40 ++++++++++ model/main.go | 2 + service/channel.go | 51 +++++++++++++ 5 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 model/channel_test_history.go diff --git a/controller/channel-test.go b/controller/channel-test.go index b225585ed7a3..7d62ca8c14bf 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -901,41 +901,130 @@ func testAllChannels(notify bool) error { continue } isChannelEnabled := channel.Status == common.ChannelStatusEnabled - tik := time.Now() - result := testChannel(channel, "", "", shouldUseStreamForAutomaticChannelTest(channel)) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() - - shouldBanChannel := false - newAPIError := result.newAPIError - // request error disables the channel - if newAPIError != nil { - shouldBanChannel = service.ShouldDisableChannel(result.newAPIError) + + models := channel.GetModels() + if len(models) == 0 { + continue } - // 当错误检查通过,才检查响应时间 - if common.AutomaticDisableChannelEnabled && !shouldBanChannel { - if milliseconds > disableThreshold { - err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0) - newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout) - shouldBanChannel = true + var totalMs int64 + channelLevelErrorOccurred := false + + for _, testModelName := range models { + testModelName = strings.TrimSpace(testModelName) + if testModelName == "" { + continue + } + + lowerModelName := strings.ToLower(testModelName) + if strings.Contains(lowerModelName, "seedream") || + strings.Contains(lowerModelName, "image-preview") { + model.RecordChannelTestHistory(channel.Id, channel.Name, testModelName, "unsupported", 0, "image generation model test is not supported") + continue + } + + testTimeout := 120 * time.Second + tik := time.Now() + resultCh := make(chan testResult, 1) + go func() { + resultCh <- testChannel(channel, testModelName, "", false) + }() + var result testResult + select { + case result = <-resultCh: + case <-time.After(testTimeout): + result = testResult{ + localErr: fmt.Errorf("测试超时(%ds),模型「%s」未在限定时间内响应", int(testTimeout.Seconds()), testModelName), + newAPIError: types.NewOpenAIError(fmt.Errorf("test timeout after %ds", int(testTimeout.Seconds())), types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout), + } + } + tok := time.Now() + milliseconds := tok.Sub(tik).Milliseconds() + totalMs += milliseconds + + shouldBanModel := false + newAPIError := result.newAPIError + errMsg := "" + testStatus := "operational" + + if result.localErr != nil { + errMsg = result.localErr.Error() + if strings.Contains(errMsg, "not supported") || + strings.Contains(errMsg, "invalid image request type") || + strings.Contains(errMsg, "invalid embedding request type") || + strings.Contains(errMsg, "invalid rerank request type") { + testStatus = "unsupported" + } + } + + if newAPIError != nil { + shouldBanModel = service.ShouldDisableChannel(newAPIError) + + if shouldBanModel && service.IsChannelLevelError(newAPIError) { + // 通道级错误(API key 无效、余额不足等)→ 禁用整个通道,跳过剩余模型 + if isChannelEnabled && channel.GetAutoBan() { + processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) + } + channelLevelErrorOccurred = true + testStatus = "failed" + model.RecordChannelTestHistory(channel.Id, channel.Name, testModelName, testStatus, milliseconds, errMsg) + break + } } - } - // disable channel - if isChannelEnabled && shouldBanChannel && channel.GetAutoBan() { - processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) + // 检查响应时间是否超阈值 + if common.AutomaticDisableChannelEnabled && !shouldBanModel { + if milliseconds > disableThreshold { + err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0) + newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout) + shouldBanModel = true + testStatus = "timeout" + errMsg = err.Error() + } + } + + if isChannelEnabled && shouldBanModel && channel.GetAutoBan() && testStatus != "unsupported" { + reason := "测试失败" + if newAPIError != nil { + reason = newAPIError.ErrorWithStatusCode() + } + service.DisableChannelModel(channel.Id, channel.Name, testModelName, reason) + testStatus = "failed" + } + + if common.AutomaticEnableChannelEnabled && newAPIError == nil && testStatus == "operational" { + if !model.IsAbilityModelEnabled(channel.Id, testModelName) { + service.EnableChannelModel(channel.Id, channel.Name, testModelName) + } + } + + model.RecordChannelTestHistory(channel.Id, channel.Name, testModelName, testStatus, milliseconds, errMsg) + time.Sleep(common.RequestInterval) } - // enable channel - if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) { - service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name) + // 通道级:如果未发生通道级错误,且通道之前被自动禁用,且本次所有模型都测试通过,则重新启用通道 + if common.AutomaticEnableChannelEnabled && !channelLevelErrorOccurred && !isChannelEnabled && channel.Status == common.ChannelStatusAutoDisabled { + // 检查是否所有模型测试都成功(通过检查是否有任何模型被禁用) + allModelsOk := true + for _, m := range models { + if !model.IsAbilityModelEnabled(channel.Id, strings.TrimSpace(m)) { + allModelsOk = false + break + } + } + if allModelsOk { + service.EnableChannel(channel.Id, "", channel.Name) + } } - channel.UpdateResponseTime(milliseconds) - time.Sleep(common.RequestInterval) + // 使用所有模型的平均响应时间更新通道响应时间 + if len(models) > 0 { + channel.UpdateResponseTime(totalMs / int64(len(models))) + } } + model.PruneChannelTestHistory(30) + if notify { service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成") } diff --git a/model/ability.go b/model/ability.go index 1d7c53fa5805..3ecb9fc39152 100644 --- a/model/ability.go +++ b/model/ability.go @@ -264,6 +264,17 @@ func UpdateAbilityStatus(channelId int, status bool) error { return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error } +// UpdateAbilityModelStatus updates the enabled status of a specific model within a channel +func UpdateAbilityModelStatus(channelId int, modelName string, status bool) error { + return DB.Model(&Ability{}).Where("channel_id = ? AND model = ?", channelId, modelName).Select("enabled").Update("enabled", status).Error +} + +func IsAbilityModelEnabled(channelId int, modelName string) bool { + var count int64 + DB.Model(&Ability{}).Where("channel_id = ? AND model = ? AND enabled = ?", channelId, modelName, true).Count(&count) + return count > 0 +} + func UpdateAbilityStatusByTag(tag string, status bool) error { return DB.Model(&Ability{}).Where("tag = ?", tag).Select("enabled").Update("enabled", status).Error } diff --git a/model/channel_test_history.go b/model/channel_test_history.go new file mode 100644 index 000000000000..83eb5d647876 --- /dev/null +++ b/model/channel_test_history.go @@ -0,0 +1,40 @@ +package model + +import ( + "time" + + "github.com/bytedance/gopkg/util/gopool" +) + +// ChannelTestHistory records per-model test results for availability tracking +type ChannelTestHistory struct { + Id int `json:"id" gorm:"primaryKey;autoIncrement"` + ChannelId int `json:"channel_id" gorm:"index"` + ChannelName string `json:"channel_name"` + TestModel string `json:"test_model"` + Status string `json:"status"` // operational, failed, timeout, unsupported + ResponseTime int64 `json:"response_time"` // ms + ErrorMessage string `json:"error_message"` + TestedAt time.Time `json:"tested_at" gorm:"index"` +} + +func RecordChannelTestHistory(channelId int, channelName, testModel, status string, responseTime int64, errMsg string) { + gopool.Go(func() { + history := ChannelTestHistory{ + ChannelId: channelId, + ChannelName: channelName, + TestModel: testModel, + Status: status, + ResponseTime: responseTime, + ErrorMessage: errMsg, + TestedAt: time.Now(), + } + DB.Create(&history) + }) +} + +func PruneChannelTestHistory(retentionDays int) int64 { + cutoff := time.Now().AddDate(0, 0, -retentionDays) + result := DB.Where("tested_at < ?", cutoff).Delete(&ChannelTestHistory{}) + return result.RowsAffected +} diff --git a/model/main.go b/model/main.go index 16cd373fb203..6c4f07a602ee 100644 --- a/model/main.go +++ b/model/main.go @@ -281,6 +281,7 @@ func migrateDB() error { &CustomOAuthProvider{}, &UserOAuthBinding{}, &PerfMetric{}, + &ChannelTestHistory{}, ) if err != nil { return err @@ -330,6 +331,7 @@ func migrateDBFast() error { {&CustomOAuthProvider{}, "CustomOAuthProvider"}, {&UserOAuthBinding{}, "UserOAuthBinding"}, {&PerfMetric{}, "PerfMetric"}, + {&ChannelTestHistory{}, "ChannelTestHistory"}, } // 动态计算migration数量,确保errChan缓冲区足够大 errChan := make(chan error, len(migrations)) diff --git a/service/channel.go b/service/channel.go index 3fde6e207b68..a3dc61ceec29 100644 --- a/service/channel.go +++ b/service/channel.go @@ -2,6 +2,7 @@ package service import ( "fmt" + "net/http" "strings" "github.com/QuantumNous/new-api/common" @@ -64,6 +65,56 @@ func ShouldDisableChannel(err *types.NewAPIError) bool { return search } +// IsChannelLevelError checks if the error is a channel-level issue (shared resources like API key, account, quota) +func IsChannelLevelError(err *types.NewAPIError) bool { + if err == nil { + return false + } + if err.StatusCode == http.StatusUnauthorized { + return true + } + oaiErr := err.ToOpenAIError() + switch oaiErr.Code { + case "invalid_api_key", "account_deactivated", "billing_not_active", "Arrearage": + return true + } + switch oaiErr.Type { + case "insufficient_quota", "insufficient_user_quota", "authentication_error", "permission_error", "forbidden": + return true + } + errorCode := err.GetErrorCode() + switch errorCode { + case types.ErrorCodeChannelNoAvailableKey, types.ErrorCodeChannelInvalidKey: + return true + } + return false +} + +// DisableChannelModel disables a single model's ability within a channel and sends notification +func DisableChannelModel(channelId int, channelName string, modelName string, reason string) { + common.SysLog(fmt.Sprintf("通道「%s」(#%d)的模型「%s」发生错误,准备禁用,原因:%s", channelName, channelId, modelName, reason)) + err := model.UpdateAbilityModelStatus(channelId, modelName, false) + if err != nil { + common.SysError(fmt.Sprintf("failed to disable model ability: channel=%d, model=%s, error=%v", channelId, modelName, err)) + return + } + subject := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被禁用", channelName, channelId, modelName) + content := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被禁用,原因:%s", channelName, channelId, modelName, reason) + NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusAutoDisabled), subject, content) +} + +// EnableChannelModel enables a single model's ability within a channel and sends notification +func EnableChannelModel(channelId int, channelName string, modelName string) { + err := model.UpdateAbilityModelStatus(channelId, modelName, true) + if err != nil { + common.SysError(fmt.Sprintf("failed to enable model ability: channel=%d, model=%s, error=%v", channelId, modelName, err)) + return + } + subject := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被启用", channelName, channelId, modelName) + content := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被启用", channelName, channelId, modelName) + NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusEnabled), subject, content) +} + func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool { if !common.AutomaticEnableChannelEnabled { return false From e85268cb137cad995c3a4405871b9b7743885d10 Mon Sep 17 00:00:00 2001 From: Tim Wang Date: Wed, 22 Apr 2026 17:19:32 +0800 Subject: [PATCH 2/4] fix: address CodeRabbit review feedback - Fix average response time dividing by actual tested count instead of total model count (avoids bias from skipped/broken models) - Gate model-level disable on AutomaticDisableChannelEnabled for consistency with threshold-based disable path - Add gorm type:text tag to ErrorMessage to prevent truncation on MySQL - Use per-model notify type key to avoid notification deduplication across multiple models in the same channel - Refresh channel cache after updating per-model ability status - Reuse types.IsChannelError() and add HTTP 403 to IsChannelLevelError for broader channel-error classification Co-Authored-By: Claude Opus 4.6 --- controller/channel-test.go | 8 +++++--- model/ability.go | 6 +++++- model/channel_test_history.go | 2 +- service/channel.go | 18 ++++++++++-------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index 7d62ca8c14bf..5c814febdd0a 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -908,6 +908,7 @@ func testAllChannels(notify bool) error { } var totalMs int64 + var testedCount int64 channelLevelErrorOccurred := false for _, testModelName := range models { @@ -941,6 +942,7 @@ func testAllChannels(notify bool) error { tok := time.Now() milliseconds := tok.Sub(tik).Milliseconds() totalMs += milliseconds + testedCount++ shouldBanModel := false newAPIError := result.newAPIError @@ -983,7 +985,7 @@ func testAllChannels(notify bool) error { } } - if isChannelEnabled && shouldBanModel && channel.GetAutoBan() && testStatus != "unsupported" { + if common.AutomaticDisableChannelEnabled && isChannelEnabled && shouldBanModel && channel.GetAutoBan() && testStatus != "unsupported" { reason := "测试失败" if newAPIError != nil { reason = newAPIError.ErrorWithStatusCode() @@ -1018,8 +1020,8 @@ func testAllChannels(notify bool) error { } // 使用所有模型的平均响应时间更新通道响应时间 - if len(models) > 0 { - channel.UpdateResponseTime(totalMs / int64(len(models))) + if testedCount > 0 { + channel.UpdateResponseTime(totalMs / testedCount) } } diff --git a/model/ability.go b/model/ability.go index 3ecb9fc39152..c3ec325a9b13 100644 --- a/model/ability.go +++ b/model/ability.go @@ -266,7 +266,11 @@ func UpdateAbilityStatus(channelId int, status bool) error { // UpdateAbilityModelStatus updates the enabled status of a specific model within a channel func UpdateAbilityModelStatus(channelId int, modelName string, status bool) error { - return DB.Model(&Ability{}).Where("channel_id = ? AND model = ?", channelId, modelName).Select("enabled").Update("enabled", status).Error + err := DB.Model(&Ability{}).Where("channel_id = ? AND model = ?", channelId, modelName).Select("enabled").Update("enabled", status).Error + if err == nil { + InitChannelCache() + } + return err } func IsAbilityModelEnabled(channelId int, modelName string) bool { diff --git a/model/channel_test_history.go b/model/channel_test_history.go index 83eb5d647876..9f7775f92d00 100644 --- a/model/channel_test_history.go +++ b/model/channel_test_history.go @@ -14,7 +14,7 @@ type ChannelTestHistory struct { TestModel string `json:"test_model"` Status string `json:"status"` // operational, failed, timeout, unsupported ResponseTime int64 `json:"response_time"` // ms - ErrorMessage string `json:"error_message"` + ErrorMessage string `json:"error_message" gorm:"type:text"` TestedAt time.Time `json:"tested_at" gorm:"index"` } diff --git a/service/channel.go b/service/channel.go index a3dc61ceec29..f9b4726a7011 100644 --- a/service/channel.go +++ b/service/channel.go @@ -16,6 +16,10 @@ func formatNotifyType(channelId int, status int) string { return fmt.Sprintf("%s_%d_%d", dto.NotifyTypeChannelUpdate, channelId, status) } +func formatModelNotifyType(channelId int, modelName string, status int) string { + return fmt.Sprintf("%s_%d_%s_%d", dto.NotifyTypeChannelUpdate, channelId, modelName, status) +} + // disable & notify func DisableChannel(channelError types.ChannelError, reason string) { common.SysLog(fmt.Sprintf("通道「%s」(#%d)发生错误,准备禁用,原因:%s", channelError.ChannelName, channelError.ChannelId, reason)) @@ -70,7 +74,10 @@ func IsChannelLevelError(err *types.NewAPIError) bool { if err == nil { return false } - if err.StatusCode == http.StatusUnauthorized { + if types.IsChannelError(err) { + return true + } + if err.StatusCode == http.StatusUnauthorized || err.StatusCode == http.StatusForbidden { return true } oaiErr := err.ToOpenAIError() @@ -82,11 +89,6 @@ func IsChannelLevelError(err *types.NewAPIError) bool { case "insufficient_quota", "insufficient_user_quota", "authentication_error", "permission_error", "forbidden": return true } - errorCode := err.GetErrorCode() - switch errorCode { - case types.ErrorCodeChannelNoAvailableKey, types.ErrorCodeChannelInvalidKey: - return true - } return false } @@ -100,7 +102,7 @@ func DisableChannelModel(channelId int, channelName string, modelName string, re } subject := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被禁用", channelName, channelId, modelName) content := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被禁用,原因:%s", channelName, channelId, modelName, reason) - NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusAutoDisabled), subject, content) + NotifyRootUser(formatModelNotifyType(channelId, modelName, common.ChannelStatusAutoDisabled), subject, content) } // EnableChannelModel enables a single model's ability within a channel and sends notification @@ -112,7 +114,7 @@ func EnableChannelModel(channelId int, channelName string, modelName string) { } subject := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被启用", channelName, channelId, modelName) content := fmt.Sprintf("通道「%s」(#%d)的模型「%s」已被启用", channelName, channelId, modelName) - NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusEnabled), subject, content) + NotifyRootUser(formatModelNotifyType(channelId, modelName, common.ChannelStatusEnabled), subject, content) } func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool { From cc0cac7f7264eb931731258377dc1eb817736839 Mon Sep 17 00:00:00 2001 From: Tim Wang Date: Wed, 22 Apr 2026 18:20:30 +0800 Subject: [PATCH 3/4] fix: build channel cache from abilities to honor per-model enabled state The channel cache previously rebuilt group2model2channels by splitting channel.Models, ignoring Ability.Enabled. This meant a model disabled via UpdateAbilityModelStatus remained routable when memory cache was active. Switch to iterating abilities directly so disabled models are excluded from the routing cache. Co-Authored-By: Claude Opus 4.6 --- model/channel_cache.go | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/model/channel_cache.go b/model/channel_cache.go index c9c503576038..0a89b1f965dc 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "sort" - "strings" "sync" "time" @@ -30,28 +29,25 @@ func InitChannelCache() { } var abilities []*Ability DB.Find(&abilities) - groups := make(map[string]bool) - for _, ability := range abilities { - groups[ability.Group] = true - } newGroup2model2channels := make(map[string]map[string][]int) - for group := range groups { - newGroup2model2channels[group] = make(map[string][]int) - } - for _, channel := range channels { - if channel.Status != common.ChannelStatusEnabled { - continue // skip disabled channels + for _, ability := range abilities { + if !ability.Enabled { + continue } - groups := strings.Split(channel.Group, ",") - for _, group := range groups { - models := strings.Split(channel.Models, ",") - for _, model := range models { - if _, ok := newGroup2model2channels[group][model]; !ok { - newGroup2model2channels[group][model] = make([]int, 0) - } - newGroup2model2channels[group][model] = append(newGroup2model2channels[group][model], channel.Id) - } + channel, ok := newChannelId2channel[ability.ChannelId] + if !ok || channel.Status != common.ChannelStatusEnabled { + continue + } + if _, ok := newGroup2model2channels[ability.Group]; !ok { + newGroup2model2channels[ability.Group] = make(map[string][]int) + } + if _, ok := newGroup2model2channels[ability.Group][ability.Model]; !ok { + newGroup2model2channels[ability.Group][ability.Model] = make([]int, 0) } + newGroup2model2channels[ability.Group][ability.Model] = append( + newGroup2model2channels[ability.Group][ability.Model], + ability.ChannelId, + ) } // sort by priority From 05af1494932275b04c66992395f0fb6cc8f83a0a Mon Sep 17 00:00:00 2001 From: Tim Wang Date: Fri, 15 May 2026 14:37:56 +0800 Subject: [PATCH 4/4] fix: resolve goroutine leak, nil context panic, and silent DB errors - Use context.WithTimeout instead of time.After so the HTTP request is cancelled when the 120s deadline fires, preventing goroutine accumulation - Guard processChannelError with result.context != nil to avoid panic if IsChannelLevelError classification changes in the future - Log DB.Create errors in RecordChannelTestHistory instead of silently discarding them Co-Authored-By: Claude Opus 4.6 --- controller/channel-test.go | 17 ++++++++++------- model/channel_test_history.go | 6 +++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index 5c814febdd0a..717e4fe4f072 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -2,6 +2,7 @@ package controller import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -57,7 +58,7 @@ func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointTyp return normalized } -func testChannel(channel *model.Channel, testModel string, endpointType string, isStream bool) testResult { +func testChannel(ctx context.Context, channel *model.Channel, testModel string, endpointType string, isStream bool) testResult { tik := time.Now() var unsupportedTestChannelTypes = []int{ constant.ChannelTypeMidjourney, @@ -138,10 +139,11 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, c.Request = &http.Request{ Method: "POST", - URL: &url.URL{Path: requestPath}, // 使用动态路径 + URL: &url.URL{Path: requestPath}, Body: nil, Header: make(http.Header), } + c.Request = c.Request.WithContext(ctx) cache, err := model.GetUserCache(1) if err != nil { @@ -835,7 +837,7 @@ func TestChannel(c *gin.Context) { endpointType := c.Query("endpoint_type") isStream, _ := strconv.ParseBool(c.Query("stream")) tik := time.Now() - result := testChannel(channel, testModel, endpointType, isStream) + result := testChannel(c.Request.Context(), channel, testModel, endpointType, isStream) if result.localErr != nil { resp := gin.H{ "success": false, @@ -926,19 +928,21 @@ func testAllChannels(notify bool) error { testTimeout := 120 * time.Second tik := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) resultCh := make(chan testResult, 1) go func() { - resultCh <- testChannel(channel, testModelName, "", false) + resultCh <- testChannel(ctx, channel, testModelName, "", false) }() var result testResult select { case result = <-resultCh: - case <-time.After(testTimeout): + case <-ctx.Done(): result = testResult{ localErr: fmt.Errorf("测试超时(%ds),模型「%s」未在限定时间内响应", int(testTimeout.Seconds()), testModelName), newAPIError: types.NewOpenAIError(fmt.Errorf("test timeout after %ds", int(testTimeout.Seconds())), types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout), } } + cancel() tok := time.Now() milliseconds := tok.Sub(tik).Milliseconds() totalMs += milliseconds @@ -963,8 +967,7 @@ func testAllChannels(notify bool) error { shouldBanModel = service.ShouldDisableChannel(newAPIError) if shouldBanModel && service.IsChannelLevelError(newAPIError) { - // 通道级错误(API key 无效、余额不足等)→ 禁用整个通道,跳过剩余模型 - if isChannelEnabled && channel.GetAutoBan() { + if isChannelEnabled && channel.GetAutoBan() && result.context != nil { processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) } channelLevelErrorOccurred = true diff --git a/model/channel_test_history.go b/model/channel_test_history.go index 9f7775f92d00..804b35bc3fbb 100644 --- a/model/channel_test_history.go +++ b/model/channel_test_history.go @@ -1,8 +1,10 @@ package model import ( + "fmt" "time" + "github.com/QuantumNous/new-api/common" "github.com/bytedance/gopkg/util/gopool" ) @@ -29,7 +31,9 @@ func RecordChannelTestHistory(channelId int, channelName, testModel, status stri ErrorMessage: errMsg, TestedAt: time.Now(), } - DB.Create(&history) + if err := DB.Create(&history).Error; err != nil { + common.SysError(fmt.Sprintf("failed to record channel test history: %v", err)) + } }) }