diff --git a/controller/option.go b/controller/option.go
index a2db95326..959f2f9b8 100644
--- a/controller/option.go
+++ b/controller/option.go
@@ -187,6 +187,15 @@ func UpdateOption(c *gin.Context) {
})
return
}
+ case "AutomaticRetryStatusCodes":
+ _, 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 {
diff --git a/controller/relay.go b/controller/relay.go
index 72ea3e24c..4fba947f7 100644
--- a/controller/relay.go
+++ b/controller/relay.go
@@ -21,6 +21,7 @@ import (
"github.com/QuantumNous/new-api/relay/helper"
"github.com/QuantumNous/new-api/service"
"github.com/QuantumNous/new-api/setting"
+ "github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/types"
"github.com/bytedance/gopkg/util/gopool"
@@ -316,30 +317,14 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
if _, ok := c.Get("specific_channel_id"); ok {
return false
}
- if openaiErr.StatusCode == http.StatusTooManyRequests {
- return true
- }
- if openaiErr.StatusCode == 307 {
- return true
- }
- if openaiErr.StatusCode/100 == 5 {
- // 超时不重试
- if openaiErr.StatusCode == 504 || openaiErr.StatusCode == 524 {
- return false
- }
- return true
- }
- if openaiErr.StatusCode == http.StatusBadRequest {
+ code := openaiErr.StatusCode
+ if code >= 200 && code < 300 {
return false
}
- if openaiErr.StatusCode == 408 {
- // azure处理超时不重试
- return false
+ if code < 100 || code > 599 {
+ return true
}
- if openaiErr.StatusCode/100 == 2 {
- return false
- }
- return true
+ return operation_setting.ShouldRetryByStatusCode(code)
}
func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
diff --git a/model/option.go b/model/option.go
index 24cf7862d..e268cf577 100644
--- a/model/option.go
+++ b/model/option.go
@@ -144,6 +144,7 @@ func InitOptionMap() {
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
+ common.OptionMap["AutomaticRetryStatusCodes"] = operation_setting.AutomaticRetryStatusCodesToString()
common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled())
// 自动添加所有注册的模型配置
@@ -447,6 +448,8 @@ func updateOptionMap(key string, value string) (err error) {
operation_setting.AutomaticDisableKeywordsFromString(value)
case "AutomaticDisableStatusCodes":
err = operation_setting.AutomaticDisableStatusCodesFromString(value)
+ case "AutomaticRetryStatusCodes":
+ err = operation_setting.AutomaticRetryStatusCodesFromString(value)
case "StreamCacheQueueLength":
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
case "PayMethods":
diff --git a/setting/operation_setting/status_code_ranges.go b/setting/operation_setting/status_code_ranges.go
index 7a763008e..698c87c91 100644
--- a/setting/operation_setting/status_code_ranges.go
+++ b/setting/operation_setting/status_code_ranges.go
@@ -14,19 +14,20 @@ type StatusCodeRange struct {
var AutomaticDisableStatusCodeRanges = []StatusCodeRange{{Start: 401, End: 401}}
+// Default behavior matches legacy hardcoded retry rules in controller/relay.go shouldRetry:
+// retry for 1xx, 3xx, 4xx(except 400/408), 5xx(except 504/524), and no retry for 2xx.
+var AutomaticRetryStatusCodeRanges = []StatusCodeRange{
+ {Start: 100, End: 199},
+ {Start: 300, End: 399},
+ {Start: 401, End: 407},
+ {Start: 409, End: 499},
+ {Start: 500, End: 503},
+ {Start: 505, End: 523},
+ {Start: 525, End: 599},
+}
+
func AutomaticDisableStatusCodesToString() string {
- if len(AutomaticDisableStatusCodeRanges) == 0 {
- return ""
- }
- parts := make([]string, 0, len(AutomaticDisableStatusCodeRanges))
- for _, r := range AutomaticDisableStatusCodeRanges {
- if r.Start == r.End {
- parts = append(parts, strconv.Itoa(r.Start))
- continue
- }
- parts = append(parts, fmt.Sprintf("%d-%d", r.Start, r.End))
- }
- return strings.Join(parts, ",")
+ return statusCodeRangesToString(AutomaticDisableStatusCodeRanges)
}
func AutomaticDisableStatusCodesFromString(s string) error {
@@ -39,10 +40,46 @@ func AutomaticDisableStatusCodesFromString(s string) error {
}
func ShouldDisableByStatusCode(code int) bool {
+ return shouldMatchStatusCodeRanges(AutomaticDisableStatusCodeRanges, code)
+}
+
+func AutomaticRetryStatusCodesToString() string {
+ return statusCodeRangesToString(AutomaticRetryStatusCodeRanges)
+}
+
+func AutomaticRetryStatusCodesFromString(s string) error {
+ ranges, err := ParseHTTPStatusCodeRanges(s)
+ if err != nil {
+ return err
+ }
+ AutomaticRetryStatusCodeRanges = ranges
+ return nil
+}
+
+func ShouldRetryByStatusCode(code int) bool {
+ return shouldMatchStatusCodeRanges(AutomaticRetryStatusCodeRanges, code)
+}
+
+func statusCodeRangesToString(ranges []StatusCodeRange) string {
+ if len(ranges) == 0 {
+ return ""
+ }
+ parts := make([]string, 0, len(ranges))
+ for _, r := range ranges {
+ if r.Start == r.End {
+ parts = append(parts, strconv.Itoa(r.Start))
+ continue
+ }
+ parts = append(parts, fmt.Sprintf("%d-%d", r.Start, r.End))
+ }
+ return strings.Join(parts, ",")
+}
+
+func shouldMatchStatusCodeRanges(ranges []StatusCodeRange, code int) bool {
if code < 100 || code > 599 {
return false
}
- for _, r := range AutomaticDisableStatusCodeRanges {
+ for _, r := range ranges {
if code < r.Start {
return false
}
diff --git a/setting/operation_setting/status_code_ranges_test.go b/setting/operation_setting/status_code_ranges_test.go
index 1712efd75..5801824ac 100644
--- a/setting/operation_setting/status_code_ranges_test.go
+++ b/setting/operation_setting/status_code_ranges_test.go
@@ -50,3 +50,30 @@ func TestShouldDisableByStatusCode(t *testing.T) {
require.True(t, ShouldDisableByStatusCode(500))
require.False(t, ShouldDisableByStatusCode(200))
}
+
+func TestShouldRetryByStatusCode(t *testing.T) {
+ orig := AutomaticRetryStatusCodeRanges
+ t.Cleanup(func() { AutomaticRetryStatusCodeRanges = orig })
+
+ AutomaticRetryStatusCodeRanges = []StatusCodeRange{
+ {Start: 429, End: 429},
+ {Start: 500, End: 599},
+ }
+
+ require.True(t, ShouldRetryByStatusCode(429))
+ require.True(t, ShouldRetryByStatusCode(500))
+ require.False(t, ShouldRetryByStatusCode(400))
+ require.False(t, ShouldRetryByStatusCode(200))
+}
+
+func TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior(t *testing.T) {
+ require.False(t, ShouldRetryByStatusCode(200))
+ require.False(t, ShouldRetryByStatusCode(400))
+ require.True(t, ShouldRetryByStatusCode(401))
+ require.False(t, ShouldRetryByStatusCode(408))
+ require.True(t, ShouldRetryByStatusCode(429))
+ require.True(t, ShouldRetryByStatusCode(500))
+ require.False(t, ShouldRetryByStatusCode(504))
+ require.False(t, ShouldRetryByStatusCode(524))
+ require.True(t, ShouldRetryByStatusCode(599))
+}
diff --git a/web/src/components/settings/HttpStatusCodeRulesInput.jsx b/web/src/components/settings/HttpStatusCodeRulesInput.jsx
new file mode 100644
index 000000000..361bc19e6
--- /dev/null
+++ b/web/src/components/settings/HttpStatusCodeRulesInput.jsx
@@ -0,0 +1,71 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see