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
63 changes: 50 additions & 13 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ 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
}
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 {
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 {
Expand All @@ -56,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,
Expand Down Expand Up @@ -430,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,
}
}
}
Expand All @@ -450,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,
Expand Down Expand Up @@ -479,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)
Expand All @@ -501,6 +539,7 @@ func testChannel(channel *model.Channel, testModel string, endpointType string,
context: c,
localErr: nil,
newAPIError: nil,
duration: duration,
}
}

Expand Down Expand Up @@ -753,8 +792,9 @@ func TestChannel(c *gin.Context) {
testModel := c.Query("model")
endpointType := c.Query("endpoint_type")
isStream, _ := strconv.ParseBool(c.Query("stream"))
tik := time.Now()
result := testChannel(channel, testModel, endpointType, isStream)
manualTest, _ := strconv.ParseBool(c.Query("manual_test"))
allowStreamRetry := !manualTest && c.Query("stream") == "" && strings.TrimSpace(endpointType) == ""
result := testChannelWithOptionalStreamRetry(channel, testModel, endpointType, isStream, allowStreamRetry)
if result.localErr != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
Expand All @@ -763,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 {
Expand Down Expand Up @@ -815,10 +854,8 @@ func testAllChannels(notify bool) error {
continue
}
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
tik := time.Now()
result := testChannel(channel, "", "", false)
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
result := testChannelWithOptionalStreamRetry(channel, "", "", false, true)
milliseconds := result.duration.Milliseconds()

shouldBanChannel := false
newAPIError := result.newAPIError
Expand Down
11 changes: 9 additions & 2 deletions controller/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ func GetOptions(c *gin.Context) {
"message": "",
"data": options,
})
return
}

type OptionUpdateRequest struct {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -306,5 +314,4 @@ func UpdateOption(c *gin.Context) {
"success": true,
"message": "",
})
return
}
1 change: 0 additions & 1 deletion controller/topup.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,4 +463,3 @@ func AdminCompleteTopUp(c *gin.Context) {
}
common.ApiSuccess(c, nil)
}

50 changes: 46 additions & 4 deletions setting/operation_setting/monitor_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
}
48 changes: 48 additions & 0 deletions setting/operation_setting/monitor_setting_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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")
}
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") {
t.Fatal("did not expect retry when setting disabled")
}
}
8 changes: 7 additions & 1 deletion web/src/components/settings/OperationSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ const ModelTestModal = ({
record.model,
selectedEndpointType,
isStreamTest,
true,
)
}
loading={isTesting}
Expand Down
5 changes: 5 additions & 0 deletions web/src/hooks/channels/useChannelsData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ export const useChannelsData = () => {
model,
endpointType = '',
stream = false,
manualTest = false,
) => {
const testKey = `${record.id}-${model}`;

Expand All @@ -883,6 +884,9 @@ export const useChannelsData = () => {
if (stream) {
url += `&stream=true`;
}
if (manualTest) {
url += `&manual_test=true`;
}
const res = await API.get(url);

// 检查是否在请求期间被停止
Expand Down Expand Up @@ -1016,6 +1020,7 @@ export const useChannelsData = () => {
model,
selectedEndpointType,
isStreamTest,
true,
),
);
const batchResults = await Promise.allSettled(batchPromises);
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"自动生成:": "Généré automatiquement :",
"自动禁用": "Désactivé automatiquement",
"自动禁用关键词": "Mots-clés de désactivation automatique",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2377,6 +2377,12 @@
"自动检测": "自動テスト",
"自动模式": "自動モード",
"自动测试所有通道间隔时间": "すべてのチャネルの自動テスト間隔",
"测试失败后自动切换流式重试": "テスト失敗後に自動でストリーム再試行へ切り替える",
"测试时自动切换流式重试状态码": "テスト時にストリーム再試行へ切り替えるステータスコード",
"测试时自动切换流式重试状态码格式不正确": "テスト時にストリーム再試行へ切り替えるステータスコードの形式が正しくありません",
"测试时自动切换流式重试关键词": "テスト時にストリーム再試行へ切り替えるキーワード",
"仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "デフォルトのチャネルテストでのみ有効です。最初の非ストリーミング失敗後、これらのステータスコードに一致した場合のみストリーミング再試行を行います",
"仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "デフォルトのチャネルテストでのみ有効です。エラーメッセージにこれらのキーワードが含まれる場合のみ、失敗後にストリーミング再試行へ切り替えます",
"自动生成:": "自動生成:",
"自动禁用": "自動無効化",
"自动禁用关键词": "自動無効化キーワード",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -2410,6 +2410,12 @@
"自动检测": "Автоматическое обнаружение",
"自动模式": "Автоматический режим",
"自动测试所有通道间隔时间": "Интервал автоматического тестирования всех каналов",
"测试失败后自动切换流式重试": "Автоматически переключаться на потоковый повтор после сбоя теста",
"测试时自动切换流式重试状态码": "Коды состояния для переключения на потоковый повтор при тестировании",
"测试时自动切换流式重试状态码格式不正确": "Неверный формат кодов состояния для переключения на потоковый повтор при тестировании",
"测试时自动切换流式重试关键词": "Ключевые слова для переключения на потоковый повтор при тестировании",
"仅在默认渠道测试中生效;首次非流式失败后,命中这些状态码才会尝试改用流式重试": "Действует только для стандартных тестов каналов; после первой неудачи без потока повтор в потоковом режиме выполняется только при совпадении с этими кодами состояния",
"仅在默认渠道测试中生效;错误信息包含这些关键词时,才会在失败后改用流式重试": "Действует только для стандартных тестов каналов; после сбоя повтор в потоковом режиме выполняется только если сообщение об ошибке содержит эти ключевые слова",
"自动生成:": "Автогенерация:",
"自动禁用": "Автоматическое отключение",
"自动禁用关键词": "Ключевые слова для автоматического отключения",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 tạo:",
"自动禁用": "Tự động vô hiệu hóa",
Expand Down
Loading