From add81b89b8d887591013b623929915a1b8d454f8 Mon Sep 17 00:00:00 2001 From: zhuhaow Date: Wed, 18 Mar 2026 08:39:15 +0800 Subject: [PATCH 1/2] fix: retry channel tests with stream when configured --- controller/channel-test.go | 28 +++++++- controller/option.go | 11 ++- setting/operation_setting/monitor_setting.go | 50 ++++++++++++-- .../operation_setting/monitor_setting_test.go | 45 +++++++++++++ .../components/settings/OperationSetting.jsx | 8 ++- .../table/channels/modals/ModelTestModal.jsx | 1 + web/src/hooks/channels/useChannelsData.jsx | 5 ++ web/src/i18n/locales/en.json | 6 ++ web/src/i18n/locales/fr.json | 6 ++ web/src/i18n/locales/ja.json | 6 ++ web/src/i18n/locales/ru.json | 6 ++ web/src/i18n/locales/vi.json | 6 ++ web/src/i18n/locales/zh-CN.json | 6 ++ web/src/i18n/locales/zh-TW.json | 6 ++ .../Setting/Operation/SettingsMonitoring.jsx | 67 +++++++++++++++++++ 15 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 setting/operation_setting/monitor_setting_test.go diff --git a/controller/channel-test.go b/controller/channel-test.go index bdd67d27a90d..e762b4af2d34 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -42,6 +42,28 @@ type testResult struct { newAPIError *types.NewAPIError } +func shouldRetryChannelTestWithStream(result testResult) bool { + if result.newAPIError == nil { + return false + } + return operation_setting.ShouldRetryChannelTestWithStream(result.newAPIError.StatusCode, result.newAPIError.Error()) +} + +func testChannelWithOptionalStreamRetry(channel *model.Channel, testModel string, endpointType string, isStream bool, allowStreamRetry bool) testResult { + result := testChannel(channel, testModel, endpointType, isStream) + if !allowStreamRetry || isStream || !shouldRetryChannelTestWithStream(result) { + return result + } + common.SysLog(fmt.Sprintf( + "channel test retrying with stream enabled: channel_id=%d name=%s model=%s endpoint_type=%s", + channel.Id, + channel.Name, + strings.TrimSpace(testModel), + strings.TrimSpace(endpointType), + )) + return testChannel(channel, testModel, endpointType, true) +} + func normalizeChannelTestEndpoint(channel *model.Channel, modelName, endpointType string) string { normalized := strings.TrimSpace(endpointType) if normalized != "" { @@ -753,8 +775,10 @@ func TestChannel(c *gin.Context) { testModel := c.Query("model") endpointType := c.Query("endpoint_type") isStream, _ := strconv.ParseBool(c.Query("stream")) + manualTest, _ := strconv.ParseBool(c.Query("manual_test")) + allowStreamRetry := !manualTest && c.Query("stream") == "" && strings.TrimSpace(endpointType) == "" tik := time.Now() - result := testChannel(channel, testModel, endpointType, isStream) + result := testChannelWithOptionalStreamRetry(channel, testModel, endpointType, isStream, allowStreamRetry) if result.localErr != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -816,7 +840,7 @@ func testAllChannels(notify bool) error { } isChannelEnabled := channel.Status == common.ChannelStatusEnabled tik := time.Now() - result := testChannel(channel, "", "", false) + result := testChannelWithOptionalStreamRetry(channel, "", "", false, true) tok := time.Now() milliseconds := tok.Sub(tik).Milliseconds() diff --git a/controller/option.go b/controller/option.go index ecb1e25e8677..98c30a26ac32 100644 --- a/controller/option.go +++ b/controller/option.go @@ -94,7 +94,6 @@ func GetOptions(c *gin.Context) { "message": "", "data": options, }) - return } type OptionUpdateRequest struct { @@ -260,6 +259,15 @@ func UpdateOption(c *gin.Context) { }) return } + case "monitor_setting.channel_test_stream_retry_status_codes": + _, err = operation_setting.ParseHTTPStatusCodeRanges(option.Value.(string)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } case "console_setting.api_info": err = console_setting.ValidateConsoleSettings(option.Value.(string), "ApiInfo") if err != nil { @@ -306,5 +314,4 @@ func UpdateOption(c *gin.Context) { "success": true, "message": "", }) - return } diff --git a/setting/operation_setting/monitor_setting.go b/setting/operation_setting/monitor_setting.go index 541e25f8a105..459047118f47 100644 --- a/setting/operation_setting/monitor_setting.go +++ b/setting/operation_setting/monitor_setting.go @@ -3,19 +3,26 @@ package operation_setting import ( "os" "strconv" + "strings" "github.com/QuantumNous/new-api/setting/config" ) type MonitorSetting struct { - AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"` - AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"` + AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"` + AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"` + ChannelTestStreamRetryEnabled bool `json:"channel_test_stream_retry_enabled"` + ChannelTestStreamRetryStatusCodes string `json:"channel_test_stream_retry_status_codes"` + ChannelTestStreamRetryKeywords string `json:"channel_test_stream_retry_keywords"` } // 默认配置 var monitorSetting = MonitorSetting{ - AutoTestChannelEnabled: false, - AutoTestChannelMinutes: 10, + AutoTestChannelEnabled: false, + AutoTestChannelMinutes: 10, + ChannelTestStreamRetryEnabled: true, + ChannelTestStreamRetryStatusCodes: "400", + ChannelTestStreamRetryKeywords: "stream must be set to true", } func init() { @@ -33,3 +40,38 @@ func GetMonitorSetting() *MonitorSetting { } return &monitorSetting } + +func ParseMonitorKeywords(input string) []string { + input = strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(input) + parts := strings.Split(input, "\n") + keywords := make([]string, 0, len(parts)) + for _, part := range parts { + keyword := strings.ToLower(strings.TrimSpace(part)) + if keyword == "" { + continue + } + keywords = append(keywords, keyword) + } + return keywords +} + +func ShouldRetryChannelTestWithStream(statusCode int, errText string) bool { + setting := GetMonitorSetting() + if !setting.ChannelTestStreamRetryEnabled { + return false + } + ranges, err := ParseHTTPStatusCodeRanges(setting.ChannelTestStreamRetryStatusCodes) + if err != nil || !shouldMatchStatusCodeRanges(ranges, statusCode) { + return false + } + lowerErrText := strings.ToLower(strings.TrimSpace(errText)) + if lowerErrText == "" { + return false + } + for _, keyword := range ParseMonitorKeywords(setting.ChannelTestStreamRetryKeywords) { + if strings.Contains(lowerErrText, keyword) { + return true + } + } + return false +} diff --git a/setting/operation_setting/monitor_setting_test.go b/setting/operation_setting/monitor_setting_test.go new file mode 100644 index 000000000000..e6baeb8f7f78 --- /dev/null +++ b/setting/operation_setting/monitor_setting_test.go @@ -0,0 +1,45 @@ +package operation_setting + +import "testing" + +func TestParseMonitorKeywords(t *testing.T) { + keywords := ParseMonitorKeywords(" Stream must be set to true \n\nFoo\r\n BAR ") + if len(keywords) != 3 { + t.Fatalf("expected 3 keywords, got %d", len(keywords)) + } + if keywords[0] != "stream must be set to true" { + t.Fatalf("unexpected first keyword: %q", keywords[0]) + } + if keywords[1] != "foo" { + t.Fatalf("unexpected second keyword: %q", keywords[1]) + } + if keywords[2] != "bar" { + t.Fatalf("unexpected third keyword: %q", keywords[2]) + } +} + +func TestShouldRetryChannelTestWithStream(t *testing.T) { + original := monitorSetting + t.Cleanup(func() { + monitorSetting = original + }) + + monitorSetting.ChannelTestStreamRetryEnabled = true + monitorSetting.ChannelTestStreamRetryStatusCodes = "400,429" + monitorSetting.ChannelTestStreamRetryKeywords = "stream must be set to true\nretry with stream" + + if !ShouldRetryChannelTestWithStream(400, "bad response status code 400, message: Stream must be set to true") { + t.Fatal("expected retry to be enabled for matching status code and keyword") + } + if ShouldRetryChannelTestWithStream(500, "bad response status code 500, message: Stream must be set to true") { + t.Fatal("did not expect retry for non-matching status code") + } + if ShouldRetryChannelTestWithStream(400, "bad response status code 400, message: invalid request") { + t.Fatal("did not expect retry for non-matching keyword") + } + + monitorSetting.ChannelTestStreamRetryEnabled = false + if ShouldRetryChannelTestWithStream(400, "bad response status code 400, message: Stream must be set to true") { + t.Fatal("did not expect retry when setting disabled") + } +} \ No newline at end of file diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx index 8585a3e90278..c02e032e1b27 100644 --- a/web/src/components/settings/OperationSetting.jsx +++ b/web/src/components/settings/OperationSetting.jsx @@ -74,7 +74,13 @@ const OperationSetting = () => { AutomaticRetryStatusCodes: '100-199,300-399,401-407,409-499,500-503,505-523,525-599', 'monitor_setting.auto_test_channel_enabled': false, - 'monitor_setting.auto_test_channel_minutes': 10 /* 签到设置 */, + 'monitor_setting.auto_test_channel_minutes': 10, + 'monitor_setting.channel_test_stream_retry_enabled': true, + 'monitor_setting.channel_test_stream_retry_status_codes': '400', + 'monitor_setting.channel_test_stream_retry_keywords': + 'stream must be set to true', + + /* 签到设置 */ 'checkin_setting.enabled': false, 'checkin_setting.min_quota': 1000, 'checkin_setting.max_quota': 10000, diff --git a/web/src/components/table/channels/modals/ModelTestModal.jsx b/web/src/components/table/channels/modals/ModelTestModal.jsx index 490cf54be116..e5931faf2017 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -198,6 +198,7 @@ const ModelTestModal = ({ record.model, selectedEndpointType, isStreamTest, + true, ) } loading={isTesting} diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 37ee5010b201..086d60f002e1 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -864,6 +864,7 @@ export const useChannelsData = () => { model, endpointType = '', stream = false, + manualTest = false, ) => { const testKey = `${record.id}-${model}`; @@ -883,6 +884,9 @@ export const useChannelsData = () => { if (stream) { url += `&stream=true`; } + if (manualTest) { + url += `&manual_test=true`; + } const res = await API.get(url); // 检查是否在请求期间被停止 @@ -1016,6 +1020,7 @@ export const useChannelsData = () => { model, selectedEndpointType, isStreamTest, + true, ), ); const batchResults = await Promise.allSettled(batchPromises); diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index e7213cd363aa..761e89f4a063 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2429,6 +2429,12 @@ "自动检测": "Auto-detect", "自动模式": "Auto Mode", "自动测试所有通道间隔时间": "Auto test interval for all channels", + "测试失败后自动切换流式重试": "Automatically retry with streaming after test failure", + "测试时自动切换流式重试状态码": "Streaming retry status codes for tests", + "测试时自动切换流式重试状态码格式不正确": "Invalid streaming retry status code format for tests", + "测试时自动切换流式重试关键词": "Streaming retry keywords for tests", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "Only applies to default channel tests; after a non-streaming failure, retry with streaming only when these status codes match", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "Only applies to default channel tests; retry with streaming only when the error message contains these keywords", "自动生成:": "Auto-generated: ", "自动禁用": "Auto disabled", "自动禁用关键词": "Automatic disable keywords", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 54ecd673feca..da7dc153fdd3 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2396,6 +2396,12 @@ "自动检测": "Détection automatique", "自动模式": "Mode automatique", "自动测试所有通道间隔时间": "Intervalle de test automatique pour tous les canaux", + "测试失败后自动切换流式重试": "Réessayer automatiquement en mode flux après un échec de test", + "测试时自动切换流式重试状态码": "Codes d'état de bascule vers le mode flux pendant les tests", + "测试时自动切换流式重试状态码格式不正确": "Format invalide des codes d'état de bascule vers le mode flux pendant les tests", + "测试时自动切换流式重试关键词": "Mots-clés de bascule vers le mode flux pendant les tests", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "S'applique uniquement aux tests de canal par défaut ; après un premier échec hors flux, un nouvel essai en mode flux n'est tenté que si ces codes d'état correspondent", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "S'applique uniquement aux tests de canal par défaut ; un nouvel essai en mode flux n'est tenté que si le message d'erreur contient ces mots-clés", "自动生成:": "", "自动禁用": "Désactivé automatiquement", "自动禁用关键词": "Mots-clés de désactivation automatique", diff --git a/web/src/i18n/locales/ja.json b/web/src/i18n/locales/ja.json index e7e1ff467938..c79c49b97178 100644 --- a/web/src/i18n/locales/ja.json +++ b/web/src/i18n/locales/ja.json @@ -2377,6 +2377,12 @@ "自动检测": "自動テスト", "自动模式": "自動モード", "自动测试所有通道间隔时间": "すべてのチャネルの自動テスト間隔", + "测试失败后自动切换流式重试": "テスト失敗後に自動でストリーム再試行へ切り替える", + "测试时自动切换流式重试状态码": "テスト時にストリーム再試行へ切り替えるステータスコード", + "测试时自动切换流式重试状态码格式不正确": "テスト時にストリーム再試行へ切り替えるステータスコードの形式が正しくありません", + "测试时自动切换流式重试关键词": "テスト時にストリーム再試行へ切り替えるキーワード", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "デフォルトのチャネルテストでのみ有効です。最初の非ストリーミング失敗後、これらのステータスコードに一致した場合のみストリーミング再試行を行います", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "デフォルトのチャネルテストでのみ有効です。エラーメッセージにこれらのキーワードが含まれる場合のみ、失敗後にストリーミング再試行へ切り替えます", "自动生成:": "", "自动禁用": "自動無効化", "自动禁用关键词": "自動無効化キーワード", diff --git a/web/src/i18n/locales/ru.json b/web/src/i18n/locales/ru.json index 447ad3bbd408..ff83f4c07c6f 100644 --- a/web/src/i18n/locales/ru.json +++ b/web/src/i18n/locales/ru.json @@ -2410,6 +2410,12 @@ "自动检测": "Автоматическое обнаружение", "自动模式": "Автоматический режим", "自动测试所有通道间隔时间": "Интервал автоматического тестирования всех каналов", + "测试失败后自动切换流式重试": "Автоматически переключаться на потоковый повтор после сбоя теста", + "测试时自动切换流式重试状态码": "Коды состояния для переключения на потоковый повтор при тестировании", + "测试时自动切换流式重试状态码格式不正确": "Неверный формат кодов состояния для переключения на потоковый повтор при тестировании", + "测试时自动切换流式重试关键词": "Ключевые слова для переключения на потоковый повтор при тестировании", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "Действует только для стандартных тестов каналов; после первой неудачи без потока повтор в потоковом режиме выполняется только при совпадении с этими кодами состояния", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "Действует только для стандартных тестов каналов; после сбоя повтор в потоковом режиме выполняется только если сообщение об ошибке содержит эти ключевые слова", "自动生成:": "", "自动禁用": "Автоматическое отключение", "自动禁用关键词": "Ключевые слова для автоматического отключения", diff --git a/web/src/i18n/locales/vi.json b/web/src/i18n/locales/vi.json index e533237aa8b9..7a75974a98d7 100644 --- a/web/src/i18n/locales/vi.json +++ b/web/src/i18n/locales/vi.json @@ -2700,6 +2700,12 @@ "自动检测": "Tự động phát hiện", "自动模式": "Chế độ tự động", "自动测试所有通道间隔时间": "Khoảng thời gian tự động kiểm tra tất cả các kênh", + "测试失败后自动切换流式重试": "Tự động chuyển sang thử lại bằng luồng sau khi kiểm tra thất bại", + "测试时自动切换流式重试状态码": "Mã trạng thái chuyển sang thử lại bằng luồng khi kiểm tra", + "测试时自动切换流式重试状态码格式不正确": "Định dạng mã trạng thái chuyển sang thử lại bằng luồng khi kiểm tra không hợp lệ", + "测试时自动切换流式重试关键词": "Từ khóa chuyển sang thử lại bằng luồng khi kiểm tra", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "Chỉ áp dụng cho kiểm tra kênh mặc định; sau khi yêu cầu không luồng thất bại lần đầu, chỉ thử lại bằng luồng khi khớp các mã trạng thái này", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "Chỉ áp dụng cho kiểm tra kênh mặc định; chỉ chuyển sang thử lại bằng luồng sau khi thất bại nếu thông báo lỗi chứa các từ khóa này", "自动生成": "Tự động tạo", "自动生成:": "", "自动禁用": "Tự động vô hiệu hóa", diff --git a/web/src/i18n/locales/zh-CN.json b/web/src/i18n/locales/zh-CN.json index 02681108c3e6..f89895095507 100644 --- a/web/src/i18n/locales/zh-CN.json +++ b/web/src/i18n/locales/zh-CN.json @@ -1938,6 +1938,12 @@ "自动刷新中": "自动刷新中", "自动模式": "自动模式", "自动测试所有通道间隔时间": "自动测试所有通道间隔时间", + "测试失败后自动切换流式重试": "测试失败后自动切换流式重试", + "测试时自动切换流式重试状态码": "测试时自动切换流式重试状态码", + "测试时自动切换流式重试状态码格式不正确": "测试时自动切换流式重试状态码格式不正确", + "测试时自动切换流式重试关键词": "测试时自动切换流式重试关键词", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试", "自动禁用": "自动禁用", "自动禁用关键词": "自动禁用关键词", "自动禁用状态码": "自动禁用状态码", diff --git a/web/src/i18n/locales/zh-TW.json b/web/src/i18n/locales/zh-TW.json index 6ffa630ad196..92a77b49d4d8 100644 --- a/web/src/i18n/locales/zh-TW.json +++ b/web/src/i18n/locales/zh-TW.json @@ -1946,6 +1946,12 @@ "自动检测": "自動檢測", "自动模式": "自動模式", "自动测试所有通道间隔时间": "自動測試所有通道間隔時間", + "测试失败后自动切换流式重试": "測試失敗後自動切換為流式重試", + "测试时自动切换流式重试状态码": "測試時自動切換為流式重試的狀態碼", + "测试时自动切换流式重试状态码格式不正确": "測試時自動切換為流式重試的狀態碼格式不正確", + "测试时自动切换流式重试关键词": "測試時自動切換為流式重試的關鍵詞", + "仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "僅在預設渠道測試中生效;首次非流式失敗後,命中這些狀態碼才會嘗試改用流式重試", + "仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "僅在預設渠道測試中生效;錯誤訊息包含這些關鍵詞時,才會在失敗後改用流式重試", "自动禁用": "自動禁用", "自动禁用关键词": "自動禁用關鍵詞", "自动禁用状态码": "自動禁用狀態碼", diff --git a/web/src/pages/Setting/Operation/SettingsMonitoring.jsx b/web/src/pages/Setting/Operation/SettingsMonitoring.jsx index e4ee116f2945..0733848624ff 100644 --- a/web/src/pages/Setting/Operation/SettingsMonitoring.jsx +++ b/web/src/pages/Setting/Operation/SettingsMonitoring.jsx @@ -44,6 +44,10 @@ export default function SettingsMonitoring(props) { '100-199,300-399,401-407,409-499,500-503,505-523,525-599', 'monitor_setting.auto_test_channel_enabled': false, 'monitor_setting.auto_test_channel_minutes': 10, + 'monitor_setting.channel_test_stream_retry_enabled': true, + 'monitor_setting.channel_test_stream_retry_status_codes': '400', + 'monitor_setting.channel_test_stream_retry_keywords': + 'stream must be set to true', }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -53,6 +57,9 @@ export default function SettingsMonitoring(props) { const parsedAutoRetryStatusCodes = parseHttpStatusCodeRules( inputs.AutomaticRetryStatusCodes || '', ); + const parsedChannelTestStreamRetryStatusCodes = parseHttpStatusCodeRules( + inputs['monitor_setting.channel_test_stream_retry_status_codes'] || '', + ); function onSubmit() { const updateArray = compareObjects(inputs, inputsRow); @@ -73,6 +80,16 @@ export default function SettingsMonitoring(props) { : ''; return showError(`${t('自动重试状态码格式不正确')}${details}`); } + if (!parsedChannelTestStreamRetryStatusCodes.ok) { + const details = + parsedChannelTestStreamRetryStatusCodes.invalidTokens && + parsedChannelTestStreamRetryStatusCodes.invalidTokens.length > 0 + ? `: ${parsedChannelTestStreamRetryStatusCodes.invalidTokens.join(', ')}` + : ''; + return showError( + `${t('测试时自动切换流式重试状态码格式不正确')}${details}`, + ); + } const requestQueue = updateArray.map((item) => { let value = ''; if (typeof inputs[item.key] === 'boolean') { @@ -81,6 +98,8 @@ export default function SettingsMonitoring(props) { const normalizedMap = { AutomaticDisableStatusCodes: parsedAutoDisableStatusCodes.normalized, AutomaticRetryStatusCodes: parsedAutoRetryStatusCodes.normalized, + 'monitor_setting.channel_test_stream_retry_status_codes': + parsedChannelTestStreamRetryStatusCodes.normalized, }; value = normalizedMap[item.key] ?? inputs[item.key]; } @@ -235,8 +254,56 @@ export default function SettingsMonitoring(props) { /> + + + + setInputs({ + ...inputs, + 'monitor_setting.channel_test_stream_retry_enabled': value, + }) + } + /> + + + + setInputs({ + ...inputs, + 'monitor_setting.channel_test_stream_retry_status_codes': value, + }) + } + parsed={parsedChannelTestStreamRetryStatusCodes} + invalidText={t('测试时自动切换流式重试状态码格式不正确')} + /> + + setInputs({ + ...inputs, + 'monitor_setting.channel_test_stream_retry_keywords': value, + }) + } + /> Date: Wed, 18 Mar 2026 09:17:59 +0800 Subject: [PATCH 2/2] fix: preserve retry status and latency in channel tests --- controller/channel-test.go | 37 +++++++++++++------ .../operation_setting/monitor_setting_test.go | 3 ++ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index e762b4af2d34..ee0d96d2f53b 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -40,13 +40,19 @@ type testResult struct { context *gin.Context localErr error newAPIError *types.NewAPIError + statusCode int + duration time.Duration } func shouldRetryChannelTestWithStream(result testResult) bool { if result.newAPIError == nil { return false } - return operation_setting.ShouldRetryChannelTestWithStream(result.newAPIError.StatusCode, result.newAPIError.Error()) + statusCode := result.statusCode + if statusCode == 0 { + statusCode = result.newAPIError.StatusCode + } + return operation_setting.ShouldRetryChannelTestWithStream(statusCode, result.newAPIError.Error()) } func testChannelWithOptionalStreamRetry(channel *model.Channel, testModel string, endpointType string, isStream bool, allowStreamRetry bool) testResult { @@ -78,8 +84,16 @@ 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, isStream bool) (result testResult) { tik := time.Now() + defer func() { + if result.duration == 0 { + result.duration = time.Since(tik) + } + if result.statusCode == 0 && result.newAPIError != nil { + result.statusCode = result.newAPIError.StatusCode + } + }() var unsupportedTestChannelTypes = []int{ constant.ChannelTypeMidjourney, constant.ChannelTypeMidjourneyPlus, @@ -452,7 +466,8 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, return testResult{ context: c, localErr: err, - newAPIError: types.NewOpenAIError(err, types.ErrorCodeBadResponse, http.StatusInternalServerError), + newAPIError: err, + statusCode: httpResp.StatusCode, } } } @@ -472,8 +487,8 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, newAPIError: types.NewOpenAIError(usageErr, types.ErrorCodeBadResponseBody, http.StatusInternalServerError), } } - result := w.Result() - respBody, err := readTestResponseBody(result.Body, isStream) + httpResult := w.Result() + respBody, err := readTestResponseBody(httpResult.Body, isStream) if err != nil { return testResult{ context: c, @@ -501,7 +516,8 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, quota = int(priceData.ModelPrice * common.QuotaPerUnit) } tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() + duration := tok.Sub(tik) + milliseconds := duration.Milliseconds() consumedTime := float64(milliseconds) / 1000.0 other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatioInfo.GroupRatio, priceData.CompletionRatio, usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.GroupRatioInfo.GroupSpecialRatio) @@ -523,6 +539,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string, context: c, localErr: nil, newAPIError: nil, + duration: duration, } } @@ -777,7 +794,6 @@ func TestChannel(c *gin.Context) { isStream, _ := strconv.ParseBool(c.Query("stream")) manualTest, _ := strconv.ParseBool(c.Query("manual_test")) allowStreamRetry := !manualTest && c.Query("stream") == "" && strings.TrimSpace(endpointType) == "" - tik := time.Now() result := testChannelWithOptionalStreamRetry(channel, testModel, endpointType, isStream, allowStreamRetry) if result.localErr != nil { c.JSON(http.StatusOK, gin.H{ @@ -787,8 +803,7 @@ func TestChannel(c *gin.Context) { }) return } - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() + milliseconds := result.duration.Milliseconds() go channel.UpdateResponseTime(milliseconds) consumedTime := float64(milliseconds) / 1000.0 if result.newAPIError != nil { @@ -839,10 +854,8 @@ func testAllChannels(notify bool) error { continue } isChannelEnabled := channel.Status == common.ChannelStatusEnabled - tik := time.Now() result := testChannelWithOptionalStreamRetry(channel, "", "", false, true) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() + milliseconds := result.duration.Milliseconds() shouldBanChannel := false newAPIError := result.newAPIError diff --git a/setting/operation_setting/monitor_setting_test.go b/setting/operation_setting/monitor_setting_test.go index e6baeb8f7f78..faf752b5b0ed 100644 --- a/setting/operation_setting/monitor_setting_test.go +++ b/setting/operation_setting/monitor_setting_test.go @@ -37,6 +37,9 @@ func TestShouldRetryChannelTestWithStream(t *testing.T) { if ShouldRetryChannelTestWithStream(400, "bad response status code 400, message: invalid request") { t.Fatal("did not expect retry for non-matching keyword") } + if !ShouldRetryChannelTestWithStream(400, "bad response status code 500, message: Stream must be set to true") { + t.Fatal("expected preserved upstream status code to allow retry even if wrapped error text mentions 500") + } monitorSetting.ChannelTestStreamRetryEnabled = false if ShouldRetryChannelTestWithStream(400, "bad response status code 400, message: Stream must be set to true") {