Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
ae6fed15
Unverified
Commit
ae6fed15
authored
Feb 10, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 10, 2026
Browse files
Merge pull request #548 from Edric-Li/main
feat: 错误处理增强、重试优化与性能改进
parents
84ced1c4
378e476e
Changes
32
Hide whitespace changes
Inline
Side-by-side
backend/ent/errorpassthroughrule.go
View file @
ae6fed15
...
...
@@ -44,6 +44,8 @@ type ErrorPassthroughRule struct {
PassthroughBody
bool
`json:"passthrough_body,omitempty"`
// CustomMessage holds the value of the "custom_message" field.
CustomMessage
*
string
`json:"custom_message,omitempty"`
// SkipMonitoring holds the value of the "skip_monitoring" field.
SkipMonitoring
bool
`json:"skip_monitoring,omitempty"`
// Description holds the value of the "description" field.
Description
*
string
`json:"description,omitempty"`
selectValues
sql
.
SelectValues
...
...
@@ -56,7 +58,7 @@ func (*ErrorPassthroughRule) scanValues(columns []string) ([]any, error) {
switch
columns
[
i
]
{
case
errorpassthroughrule
.
FieldErrorCodes
,
errorpassthroughrule
.
FieldKeywords
,
errorpassthroughrule
.
FieldPlatforms
:
values
[
i
]
=
new
([]
byte
)
case
errorpassthroughrule
.
FieldEnabled
,
errorpassthroughrule
.
FieldPassthroughCode
,
errorpassthroughrule
.
FieldPassthroughBody
:
case
errorpassthroughrule
.
FieldEnabled
,
errorpassthroughrule
.
FieldPassthroughCode
,
errorpassthroughrule
.
FieldPassthroughBody
,
errorpassthroughrule
.
FieldSkipMonitoring
:
values
[
i
]
=
new
(
sql
.
NullBool
)
case
errorpassthroughrule
.
FieldID
,
errorpassthroughrule
.
FieldPriority
,
errorpassthroughrule
.
FieldResponseCode
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
...
...
@@ -171,6 +173,12 @@ func (_m *ErrorPassthroughRule) assignValues(columns []string, values []any) err
_m
.
CustomMessage
=
new
(
string
)
*
_m
.
CustomMessage
=
value
.
String
}
case
errorpassthroughrule
.
FieldSkipMonitoring
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullBool
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field skip_monitoring"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
SkipMonitoring
=
value
.
Bool
}
case
errorpassthroughrule
.
FieldDescription
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullString
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field description"
,
values
[
i
])
...
...
@@ -257,6 +265,9 @@ func (_m *ErrorPassthroughRule) String() string {
builder
.
WriteString
(
*
v
)
}
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"skip_monitoring="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
SkipMonitoring
))
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
Description
;
v
!=
nil
{
builder
.
WriteString
(
"description="
)
builder
.
WriteString
(
*
v
)
...
...
backend/ent/errorpassthroughrule/errorpassthroughrule.go
View file @
ae6fed15
...
...
@@ -39,6 +39,8 @@ const (
FieldPassthroughBody
=
"passthrough_body"
// FieldCustomMessage holds the string denoting the custom_message field in the database.
FieldCustomMessage
=
"custom_message"
// FieldSkipMonitoring holds the string denoting the skip_monitoring field in the database.
FieldSkipMonitoring
=
"skip_monitoring"
// FieldDescription holds the string denoting the description field in the database.
FieldDescription
=
"description"
// Table holds the table name of the errorpassthroughrule in the database.
...
...
@@ -61,6 +63,7 @@ var Columns = []string{
FieldResponseCode
,
FieldPassthroughBody
,
FieldCustomMessage
,
FieldSkipMonitoring
,
FieldDescription
,
}
...
...
@@ -95,6 +98,8 @@ var (
DefaultPassthroughCode
bool
// DefaultPassthroughBody holds the default value on creation for the "passthrough_body" field.
DefaultPassthroughBody
bool
// DefaultSkipMonitoring holds the default value on creation for the "skip_monitoring" field.
DefaultSkipMonitoring
bool
)
// OrderOption defines the ordering options for the ErrorPassthroughRule queries.
...
...
@@ -155,6 +160,11 @@ func ByCustomMessage(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldCustomMessage
,
opts
...
)
.
ToFunc
()
}
// BySkipMonitoring orders the results by the skip_monitoring field.
func
BySkipMonitoring
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldSkipMonitoring
,
opts
...
)
.
ToFunc
()
}
// ByDescription orders the results by the description field.
func
ByDescription
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldDescription
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/errorpassthroughrule/where.go
View file @
ae6fed15
...
...
@@ -104,6 +104,11 @@ func CustomMessage(v string) predicate.ErrorPassthroughRule {
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldEQ
(
FieldCustomMessage
,
v
))
}
// SkipMonitoring applies equality check predicate on the "skip_monitoring" field. It's identical to SkipMonitoringEQ.
func
SkipMonitoring
(
v
bool
)
predicate
.
ErrorPassthroughRule
{
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldEQ
(
FieldSkipMonitoring
,
v
))
}
// Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ.
func
Description
(
v
string
)
predicate
.
ErrorPassthroughRule
{
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldEQ
(
FieldDescription
,
v
))
...
...
@@ -544,6 +549,16 @@ func CustomMessageContainsFold(v string) predicate.ErrorPassthroughRule {
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldContainsFold
(
FieldCustomMessage
,
v
))
}
// SkipMonitoringEQ applies the EQ predicate on the "skip_monitoring" field.
func
SkipMonitoringEQ
(
v
bool
)
predicate
.
ErrorPassthroughRule
{
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldEQ
(
FieldSkipMonitoring
,
v
))
}
// SkipMonitoringNEQ applies the NEQ predicate on the "skip_monitoring" field.
func
SkipMonitoringNEQ
(
v
bool
)
predicate
.
ErrorPassthroughRule
{
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldNEQ
(
FieldSkipMonitoring
,
v
))
}
// DescriptionEQ applies the EQ predicate on the "description" field.
func
DescriptionEQ
(
v
string
)
predicate
.
ErrorPassthroughRule
{
return
predicate
.
ErrorPassthroughRule
(
sql
.
FieldEQ
(
FieldDescription
,
v
))
...
...
backend/ent/errorpassthroughrule_create.go
View file @
ae6fed15
...
...
@@ -172,6 +172,20 @@ func (_c *ErrorPassthroughRuleCreate) SetNillableCustomMessage(v *string) *Error
return
_c
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
_c
*
ErrorPassthroughRuleCreate
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleCreate
{
_c
.
mutation
.
SetSkipMonitoring
(
v
)
return
_c
}
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
func
(
_c
*
ErrorPassthroughRuleCreate
)
SetNillableSkipMonitoring
(
v
*
bool
)
*
ErrorPassthroughRuleCreate
{
if
v
!=
nil
{
_c
.
SetSkipMonitoring
(
*
v
)
}
return
_c
}
// SetDescription sets the "description" field.
func
(
_c
*
ErrorPassthroughRuleCreate
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleCreate
{
_c
.
mutation
.
SetDescription
(
v
)
...
...
@@ -249,6 +263,10 @@ func (_c *ErrorPassthroughRuleCreate) defaults() {
v
:=
errorpassthroughrule
.
DefaultPassthroughBody
_c
.
mutation
.
SetPassthroughBody
(
v
)
}
if
_
,
ok
:=
_c
.
mutation
.
SkipMonitoring
();
!
ok
{
v
:=
errorpassthroughrule
.
DefaultSkipMonitoring
_c
.
mutation
.
SetSkipMonitoring
(
v
)
}
}
// check runs all checks and user-defined validators on the builder.
...
...
@@ -287,6 +305,9 @@ func (_c *ErrorPassthroughRuleCreate) check() error {
if
_
,
ok
:=
_c
.
mutation
.
PassthroughBody
();
!
ok
{
return
&
ValidationError
{
Name
:
"passthrough_body"
,
err
:
errors
.
New
(
`ent: missing required field "ErrorPassthroughRule.passthrough_body"`
)}
}
if
_
,
ok
:=
_c
.
mutation
.
SkipMonitoring
();
!
ok
{
return
&
ValidationError
{
Name
:
"skip_monitoring"
,
err
:
errors
.
New
(
`ent: missing required field "ErrorPassthroughRule.skip_monitoring"`
)}
}
return
nil
}
...
...
@@ -366,6 +387,10 @@ func (_c *ErrorPassthroughRuleCreate) createSpec() (*ErrorPassthroughRule, *sqlg
_spec
.
SetField
(
errorpassthroughrule
.
FieldCustomMessage
,
field
.
TypeString
,
value
)
_node
.
CustomMessage
=
&
value
}
if
value
,
ok
:=
_c
.
mutation
.
SkipMonitoring
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldSkipMonitoring
,
field
.
TypeBool
,
value
)
_node
.
SkipMonitoring
=
value
}
if
value
,
ok
:=
_c
.
mutation
.
Description
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldDescription
,
field
.
TypeString
,
value
)
_node
.
Description
=
&
value
...
...
@@ -608,6 +633,18 @@ func (u *ErrorPassthroughRuleUpsert) ClearCustomMessage() *ErrorPassthroughRuleU
return
u
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
u
*
ErrorPassthroughRuleUpsert
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleUpsert
{
u
.
Set
(
errorpassthroughrule
.
FieldSkipMonitoring
,
v
)
return
u
}
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
func
(
u
*
ErrorPassthroughRuleUpsert
)
UpdateSkipMonitoring
()
*
ErrorPassthroughRuleUpsert
{
u
.
SetExcluded
(
errorpassthroughrule
.
FieldSkipMonitoring
)
return
u
}
// SetDescription sets the "description" field.
func
(
u
*
ErrorPassthroughRuleUpsert
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleUpsert
{
u
.
Set
(
errorpassthroughrule
.
FieldDescription
,
v
)
...
...
@@ -888,6 +925,20 @@ func (u *ErrorPassthroughRuleUpsertOne) ClearCustomMessage() *ErrorPassthroughRu
})
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
u
*
ErrorPassthroughRuleUpsertOne
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleUpsertOne
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
s
.
SetSkipMonitoring
(
v
)
})
}
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
func
(
u
*
ErrorPassthroughRuleUpsertOne
)
UpdateSkipMonitoring
()
*
ErrorPassthroughRuleUpsertOne
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
s
.
UpdateSkipMonitoring
()
})
}
// SetDescription sets the "description" field.
func
(
u
*
ErrorPassthroughRuleUpsertOne
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleUpsertOne
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
...
...
@@ -1337,6 +1388,20 @@ func (u *ErrorPassthroughRuleUpsertBulk) ClearCustomMessage() *ErrorPassthroughR
})
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
u
*
ErrorPassthroughRuleUpsertBulk
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleUpsertBulk
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
s
.
SetSkipMonitoring
(
v
)
})
}
// UpdateSkipMonitoring sets the "skip_monitoring" field to the value that was provided on create.
func
(
u
*
ErrorPassthroughRuleUpsertBulk
)
UpdateSkipMonitoring
()
*
ErrorPassthroughRuleUpsertBulk
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
s
.
UpdateSkipMonitoring
()
})
}
// SetDescription sets the "description" field.
func
(
u
*
ErrorPassthroughRuleUpsertBulk
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleUpsertBulk
{
return
u
.
Update
(
func
(
s
*
ErrorPassthroughRuleUpsert
)
{
...
...
backend/ent/errorpassthroughrule_update.go
View file @
ae6fed15
...
...
@@ -227,6 +227,20 @@ func (_u *ErrorPassthroughRuleUpdate) ClearCustomMessage() *ErrorPassthroughRule
return
_u
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
_u
*
ErrorPassthroughRuleUpdate
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleUpdate
{
_u
.
mutation
.
SetSkipMonitoring
(
v
)
return
_u
}
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
func
(
_u
*
ErrorPassthroughRuleUpdate
)
SetNillableSkipMonitoring
(
v
*
bool
)
*
ErrorPassthroughRuleUpdate
{
if
v
!=
nil
{
_u
.
SetSkipMonitoring
(
*
v
)
}
return
_u
}
// SetDescription sets the "description" field.
func
(
_u
*
ErrorPassthroughRuleUpdate
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleUpdate
{
_u
.
mutation
.
SetDescription
(
v
)
...
...
@@ -387,6 +401,9 @@ func (_u *ErrorPassthroughRuleUpdate) sqlSave(ctx context.Context) (_node int, e
if
_u
.
mutation
.
CustomMessageCleared
()
{
_spec
.
ClearField
(
errorpassthroughrule
.
FieldCustomMessage
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
SkipMonitoring
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldSkipMonitoring
,
field
.
TypeBool
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Description
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldDescription
,
field
.
TypeString
,
value
)
}
...
...
@@ -611,6 +628,20 @@ func (_u *ErrorPassthroughRuleUpdateOne) ClearCustomMessage() *ErrorPassthroughR
return
_u
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func
(
_u
*
ErrorPassthroughRuleUpdateOne
)
SetSkipMonitoring
(
v
bool
)
*
ErrorPassthroughRuleUpdateOne
{
_u
.
mutation
.
SetSkipMonitoring
(
v
)
return
_u
}
// SetNillableSkipMonitoring sets the "skip_monitoring" field if the given value is not nil.
func
(
_u
*
ErrorPassthroughRuleUpdateOne
)
SetNillableSkipMonitoring
(
v
*
bool
)
*
ErrorPassthroughRuleUpdateOne
{
if
v
!=
nil
{
_u
.
SetSkipMonitoring
(
*
v
)
}
return
_u
}
// SetDescription sets the "description" field.
func
(
_u
*
ErrorPassthroughRuleUpdateOne
)
SetDescription
(
v
string
)
*
ErrorPassthroughRuleUpdateOne
{
_u
.
mutation
.
SetDescription
(
v
)
...
...
@@ -801,6 +832,9 @@ func (_u *ErrorPassthroughRuleUpdateOne) sqlSave(ctx context.Context) (_node *Er
if
_u
.
mutation
.
CustomMessageCleared
()
{
_spec
.
ClearField
(
errorpassthroughrule
.
FieldCustomMessage
,
field
.
TypeString
)
}
if
value
,
ok
:=
_u
.
mutation
.
SkipMonitoring
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldSkipMonitoring
,
field
.
TypeBool
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
Description
();
ok
{
_spec
.
SetField
(
errorpassthroughrule
.
FieldDescription
,
field
.
TypeString
,
value
)
}
...
...
backend/ent/migrate/schema.go
View file @
ae6fed15
...
...
@@ -325,6 +325,7 @@ var (
{
Name
:
"response_code"
,
Type
:
field
.
TypeInt
,
Nullable
:
true
},
{
Name
:
"passthrough_body"
,
Type
:
field
.
TypeBool
,
Default
:
true
},
{
Name
:
"custom_message"
,
Type
:
field
.
TypeString
,
Nullable
:
true
,
Size
:
2147483647
},
{
Name
:
"skip_monitoring"
,
Type
:
field
.
TypeBool
,
Default
:
false
},
{
Name
:
"description"
,
Type
:
field
.
TypeString
,
Nullable
:
true
,
Size
:
2147483647
},
}
// ErrorPassthroughRulesTable holds the schema information for the "error_passthrough_rules" table.
...
...
backend/ent/mutation.go
View file @
ae6fed15
...
...
@@ -5776,6 +5776,7 @@ type ErrorPassthroughRuleMutation struct {
addresponse_code *int
passthrough_body *bool
custom_message *string
skip_monitoring *bool
description *string
clearedFields map[string]struct{}
done bool
...
...
@@ -6503,6 +6504,42 @@ func (m *ErrorPassthroughRuleMutation) ResetCustomMessage() {
delete(m.clearedFields, errorpassthroughrule.FieldCustomMessage)
}
// SetSkipMonitoring sets the "skip_monitoring" field.
func (m *ErrorPassthroughRuleMutation) SetSkipMonitoring(b bool) {
m.skip_monitoring = &b
}
// SkipMonitoring returns the value of the "skip_monitoring" field in the mutation.
func (m *ErrorPassthroughRuleMutation) SkipMonitoring() (r bool, exists bool) {
v := m.skip_monitoring
if v == nil {
return
}
return *v, true
}
// OldSkipMonitoring returns the old "skip_monitoring" field's value of the ErrorPassthroughRule entity.
// If the ErrorPassthroughRule object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *ErrorPassthroughRuleMutation) OldSkipMonitoring(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldSkipMonitoring is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldSkipMonitoring requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldSkipMonitoring: %w", err)
}
return oldValue.SkipMonitoring, nil
}
// ResetSkipMonitoring resets all changes to the "skip_monitoring" field.
func (m *ErrorPassthroughRuleMutation) ResetSkipMonitoring() {
m.skip_monitoring = nil
}
// SetDescription sets the "description" field.
func (m *ErrorPassthroughRuleMutation) SetDescription(s string) {
m.description = &s
...
...
@@ -6586,7 +6623,7 @@ func (m *ErrorPassthroughRuleMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *ErrorPassthroughRuleMutation) Fields() []string {
fields := make([]string, 0, 1
4
)
fields := make([]string, 0, 1
5
)
if m.created_at != nil {
fields = append(fields, errorpassthroughrule.FieldCreatedAt)
}
...
...
@@ -6626,6 +6663,9 @@ func (m *ErrorPassthroughRuleMutation) Fields() []string {
if m.custom_message != nil {
fields = append(fields, errorpassthroughrule.FieldCustomMessage)
}
if m.skip_monitoring != nil {
fields = append(fields, errorpassthroughrule.FieldSkipMonitoring)
}
if m.description != nil {
fields = append(fields, errorpassthroughrule.FieldDescription)
}
...
...
@@ -6663,6 +6703,8 @@ func (m *ErrorPassthroughRuleMutation) Field(name string) (ent.Value, bool) {
return m.PassthroughBody()
case errorpassthroughrule.FieldCustomMessage:
return m.CustomMessage()
case errorpassthroughrule.FieldSkipMonitoring:
return m.SkipMonitoring()
case errorpassthroughrule.FieldDescription:
return m.Description()
}
...
...
@@ -6700,6 +6742,8 @@ func (m *ErrorPassthroughRuleMutation) OldField(ctx context.Context, name string
return m.OldPassthroughBody(ctx)
case errorpassthroughrule.FieldCustomMessage:
return m.OldCustomMessage(ctx)
case errorpassthroughrule.FieldSkipMonitoring:
return m.OldSkipMonitoring(ctx)
case errorpassthroughrule.FieldDescription:
return m.OldDescription(ctx)
}
...
...
@@ -6802,6 +6846,13 @@ func (m *ErrorPassthroughRuleMutation) SetField(name string, value ent.Value) er
}
m.SetCustomMessage(v)
return nil
case errorpassthroughrule.FieldSkipMonitoring:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetSkipMonitoring(v)
return nil
case errorpassthroughrule.FieldDescription:
v, ok := value.(string)
if !ok {
...
...
@@ -6963,6 +7014,9 @@ func (m *ErrorPassthroughRuleMutation) ResetField(name string) error {
case errorpassthroughrule.FieldCustomMessage:
m.ResetCustomMessage()
return nil
case errorpassthroughrule.FieldSkipMonitoring:
m.ResetSkipMonitoring()
return nil
case errorpassthroughrule.FieldDescription:
m.ResetDescription()
return nil
...
...
backend/ent/runtime/runtime.go
View file @
ae6fed15
...
...
@@ -326,6 +326,10 @@ func init() {
errorpassthroughruleDescPassthroughBody
:=
errorpassthroughruleFields
[
9
]
.
Descriptor
()
// errorpassthroughrule.DefaultPassthroughBody holds the default value on creation for the passthrough_body field.
errorpassthroughrule
.
DefaultPassthroughBody
=
errorpassthroughruleDescPassthroughBody
.
Default
.
(
bool
)
// errorpassthroughruleDescSkipMonitoring is the schema descriptor for skip_monitoring field.
errorpassthroughruleDescSkipMonitoring
:=
errorpassthroughruleFields
[
11
]
.
Descriptor
()
// errorpassthroughrule.DefaultSkipMonitoring holds the default value on creation for the skip_monitoring field.
errorpassthroughrule
.
DefaultSkipMonitoring
=
errorpassthroughruleDescSkipMonitoring
.
Default
.
(
bool
)
groupMixin
:=
schema
.
Group
{}
.
Mixin
()
groupMixinHooks1
:=
groupMixin
[
1
]
.
Hooks
()
group
.
Hooks
[
0
]
=
groupMixinHooks1
[
0
]
...
...
backend/ent/schema/error_passthrough_rule.go
View file @
ae6fed15
...
...
@@ -105,6 +105,12 @@ func (ErrorPassthroughRule) Fields() []ent.Field {
Optional
()
.
Nillable
(),
// skip_monitoring: 是否跳过运维监控记录
// true: 匹配此规则的错误不会被记录到 ops_error_logs
// false: 正常记录到运维监控(默认行为)
field
.
Bool
(
"skip_monitoring"
)
.
Default
(
false
),
// description: 规则描述,用于说明规则的用途
field
.
Text
(
"description"
)
.
Optional
()
.
...
...
backend/internal/handler/admin/error_passthrough_handler.go
View file @
ae6fed15
...
...
@@ -32,6 +32,7 @@ type CreateErrorPassthroughRuleRequest struct {
ResponseCode
*
int
`json:"response_code"`
PassthroughBody
*
bool
`json:"passthrough_body"`
CustomMessage
*
string
`json:"custom_message"`
SkipMonitoring
*
bool
`json:"skip_monitoring"`
Description
*
string
`json:"description"`
}
...
...
@@ -48,6 +49,7 @@ type UpdateErrorPassthroughRuleRequest struct {
ResponseCode
*
int
`json:"response_code"`
PassthroughBody
*
bool
`json:"passthrough_body"`
CustomMessage
*
string
`json:"custom_message"`
SkipMonitoring
*
bool
`json:"skip_monitoring"`
Description
*
string
`json:"description"`
}
...
...
@@ -122,6 +124,9 @@ func (h *ErrorPassthroughHandler) Create(c *gin.Context) {
}
else
{
rule
.
PassthroughBody
=
true
}
if
req
.
SkipMonitoring
!=
nil
{
rule
.
SkipMonitoring
=
*
req
.
SkipMonitoring
}
rule
.
ResponseCode
=
req
.
ResponseCode
rule
.
CustomMessage
=
req
.
CustomMessage
rule
.
Description
=
req
.
Description
...
...
@@ -190,6 +195,7 @@ func (h *ErrorPassthroughHandler) Update(c *gin.Context) {
ResponseCode
:
existing
.
ResponseCode
,
PassthroughBody
:
existing
.
PassthroughBody
,
CustomMessage
:
existing
.
CustomMessage
,
SkipMonitoring
:
existing
.
SkipMonitoring
,
Description
:
existing
.
Description
,
}
...
...
@@ -230,6 +236,9 @@ func (h *ErrorPassthroughHandler) Update(c *gin.Context) {
if
req
.
Description
!=
nil
{
rule
.
Description
=
req
.
Description
}
if
req
.
SkipMonitoring
!=
nil
{
rule
.
SkipMonitoring
=
*
req
.
SkipMonitoring
}
// 确保切片不为 nil
if
rule
.
ErrorCodes
==
nil
{
...
...
backend/internal/handler/gateway_handler.go
View file @
ae6fed15
...
...
@@ -235,6 +235,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
maxAccountSwitches
:=
h
.
maxAccountSwitchesGemini
switchCount
:=
0
failedAccountIDs
:=
make
(
map
[
int64
]
struct
{})
sameAccountRetryCount
:=
make
(
map
[
int64
]
int
)
// 同账号重试计数
var
lastFailoverErr
*
service
.
UpstreamFailoverError
var
forceCacheBilling
bool
// 粘性会话切换时的缓存计费标记
...
...
@@ -359,11 +360,28 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
if
err
!=
nil
{
var
failoverErr
*
service
.
UpstreamFailoverError
if
errors
.
As
(
err
,
&
failoverErr
)
{
failedAccountIDs
[
account
.
ID
]
=
struct
{}{}
lastFailoverErr
=
failoverErr
if
needForceCacheBilling
(
hasBoundSession
,
failoverErr
)
{
forceCacheBilling
=
true
}
// 同账号重试:对 RetryableOnSameAccount 的临时性错误,先在同一账号上重试
if
failoverErr
.
RetryableOnSameAccount
&&
sameAccountRetryCount
[
account
.
ID
]
<
maxSameAccountRetries
{
sameAccountRetryCount
[
account
.
ID
]
++
log
.
Printf
(
"Account %d: retryable error %d, same-account retry %d/%d"
,
account
.
ID
,
failoverErr
.
StatusCode
,
sameAccountRetryCount
[
account
.
ID
],
maxSameAccountRetries
)
if
!
sleepSameAccountRetryDelay
(
c
.
Request
.
Context
())
{
return
}
continue
}
// 同账号重试用尽,执行临时封禁并切换账号
if
failoverErr
.
RetryableOnSameAccount
{
h
.
gatewayService
.
TempUnscheduleRetryableError
(
c
.
Request
.
Context
(),
account
.
ID
,
failoverErr
)
}
failedAccountIDs
[
account
.
ID
]
=
struct
{}{}
if
switchCount
>=
maxAccountSwitches
{
h
.
handleFailoverExhausted
(
c
,
failoverErr
,
service
.
PlatformGemini
,
streamStarted
)
return
...
...
@@ -427,6 +445,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
maxAccountSwitches
:=
h
.
maxAccountSwitches
switchCount
:=
0
failedAccountIDs
:=
make
(
map
[
int64
]
struct
{})
sameAccountRetryCount
:=
make
(
map
[
int64
]
int
)
// 同账号重试计数
var
lastFailoverErr
*
service
.
UpstreamFailoverError
retryWithFallback
:=
false
var
forceCacheBilling
bool
// 粘性会话切换时的缓存计费标记
...
...
@@ -579,11 +598,28 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
}
var
failoverErr
*
service
.
UpstreamFailoverError
if
errors
.
As
(
err
,
&
failoverErr
)
{
failedAccountIDs
[
account
.
ID
]
=
struct
{}{}
lastFailoverErr
=
failoverErr
if
needForceCacheBilling
(
hasBoundSession
,
failoverErr
)
{
forceCacheBilling
=
true
}
// 同账号重试:对 RetryableOnSameAccount 的临时性错误,先在同一账号上重试
if
failoverErr
.
RetryableOnSameAccount
&&
sameAccountRetryCount
[
account
.
ID
]
<
maxSameAccountRetries
{
sameAccountRetryCount
[
account
.
ID
]
++
log
.
Printf
(
"Account %d: retryable error %d, same-account retry %d/%d"
,
account
.
ID
,
failoverErr
.
StatusCode
,
sameAccountRetryCount
[
account
.
ID
],
maxSameAccountRetries
)
if
!
sleepSameAccountRetryDelay
(
c
.
Request
.
Context
())
{
return
}
continue
}
// 同账号重试用尽,执行临时封禁并切换账号
if
failoverErr
.
RetryableOnSameAccount
{
h
.
gatewayService
.
TempUnscheduleRetryableError
(
c
.
Request
.
Context
(),
account
.
ID
,
failoverErr
)
}
failedAccountIDs
[
account
.
ID
]
=
struct
{}{}
if
switchCount
>=
maxAccountSwitches
{
h
.
handleFailoverExhausted
(
c
,
failoverErr
,
account
.
Platform
,
streamStarted
)
return
...
...
@@ -863,6 +899,23 @@ func needForceCacheBilling(hasBoundSession bool, failoverErr *service.UpstreamFa
return
hasBoundSession
||
(
failoverErr
!=
nil
&&
failoverErr
.
ForceCacheBilling
)
}
const
(
// maxSameAccountRetries 同账号重试次数上限(针对 RetryableOnSameAccount 错误)
maxSameAccountRetries
=
2
// sameAccountRetryDelay 同账号重试间隔
sameAccountRetryDelay
=
500
*
time
.
Millisecond
)
// sleepSameAccountRetryDelay 同账号重试固定延时,返回 false 表示 context 已取消。
func
sleepSameAccountRetryDelay
(
ctx
context
.
Context
)
bool
{
select
{
case
<-
ctx
.
Done
()
:
return
false
case
<-
time
.
After
(
sameAccountRetryDelay
)
:
return
true
}
}
// sleepFailoverDelay 账号切换线性递增延时:第1次0s、第2次1s、第3次2s…
// 返回 false 表示 context 已取消。
func
sleepFailoverDelay
(
ctx
context
.
Context
,
switchCount
int
)
bool
{
...
...
@@ -918,6 +971,10 @@ func (h *GatewayHandler) handleFailoverExhausted(c *gin.Context, failoverErr *se
msg
=
*
rule
.
CustomMessage
}
if
rule
.
SkipMonitoring
{
c
.
Set
(
service
.
OpsSkipPassthroughKey
,
true
)
}
h
.
handleStreamingAwareError
(
c
,
respCode
,
"upstream_error"
,
msg
,
streamStarted
)
return
}
...
...
backend/internal/handler/gemini_v1beta_handler.go
View file @
ae6fed15
...
...
@@ -554,6 +554,10 @@ func (h *GatewayHandler) handleGeminiFailoverExhausted(c *gin.Context, failoverE
msg
=
*
rule
.
CustomMessage
}
if
rule
.
SkipMonitoring
{
c
.
Set
(
service
.
OpsSkipPassthroughKey
,
true
)
}
googleError
(
c
,
respCode
,
msg
)
return
}
...
...
backend/internal/handler/openai_gateway_handler.go
View file @
ae6fed15
...
...
@@ -354,6 +354,10 @@ func (h *OpenAIGatewayHandler) handleFailoverExhausted(c *gin.Context, failoverE
msg
=
*
rule
.
CustomMessage
}
if
rule
.
SkipMonitoring
{
c
.
Set
(
service
.
OpsSkipPassthroughKey
,
true
)
}
h
.
handleStreamingAwareError
(
c
,
respCode
,
"upstream_error"
,
msg
,
streamStarted
)
return
}
...
...
backend/internal/handler/ops_error_logger.go
View file @
ae6fed15
...
...
@@ -537,6 +537,13 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
// Store request headers/body only when an upstream error occurred to keep overhead minimal.
entry
.
RequestHeadersJSON
=
extractOpsRetryRequestHeaders
(
c
)
// Skip logging if a passthrough rule with skip_monitoring=true matched.
if
v
,
ok
:=
c
.
Get
(
service
.
OpsSkipPassthroughKey
);
ok
{
if
skip
,
_
:=
v
.
(
bool
);
skip
{
return
}
}
enqueueOpsErrorLog
(
ops
,
entry
,
requestBody
)
return
}
...
...
@@ -544,6 +551,13 @@ func OpsErrorLoggerMiddleware(ops *service.OpsService) gin.HandlerFunc {
body
:=
w
.
buf
.
Bytes
()
parsed
:=
parseOpsErrorResponse
(
body
)
// Skip logging if a passthrough rule with skip_monitoring=true matched.
if
v
,
ok
:=
c
.
Get
(
service
.
OpsSkipPassthroughKey
);
ok
{
if
skip
,
_
:=
v
.
(
bool
);
skip
{
return
}
}
// Skip logging if the error should be filtered based on settings
if
shouldSkipOpsErrorLog
(
c
.
Request
.
Context
(),
ops
,
parsed
.
Message
,
string
(
body
),
c
.
Request
.
URL
.
Path
)
{
return
...
...
backend/internal/model/error_passthrough_rule.go
View file @
ae6fed15
...
...
@@ -18,6 +18,7 @@ type ErrorPassthroughRule struct {
ResponseCode
*
int
`json:"response_code"`
// 自定义状态码(passthrough_code=false 时使用)
PassthroughBody
bool
`json:"passthrough_body"`
// 是否透传原始错误信息
CustomMessage
*
string
`json:"custom_message"`
// 自定义错误信息(passthrough_body=false 时使用)
SkipMonitoring
bool
`json:"skip_monitoring"`
// 是否跳过运维监控记录
Description
*
string
`json:"description"`
// 规则描述
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
...
...
backend/internal/repository/error_passthrough_repo.go
View file @
ae6fed15
...
...
@@ -54,7 +54,8 @@ func (r *errorPassthroughRepository) Create(ctx context.Context, rule *model.Err
SetPriority
(
rule
.
Priority
)
.
SetMatchMode
(
rule
.
MatchMode
)
.
SetPassthroughCode
(
rule
.
PassthroughCode
)
.
SetPassthroughBody
(
rule
.
PassthroughBody
)
SetPassthroughBody
(
rule
.
PassthroughBody
)
.
SetSkipMonitoring
(
rule
.
SkipMonitoring
)
if
len
(
rule
.
ErrorCodes
)
>
0
{
builder
.
SetErrorCodes
(
rule
.
ErrorCodes
)
...
...
@@ -90,7 +91,8 @@ func (r *errorPassthroughRepository) Update(ctx context.Context, rule *model.Err
SetPriority
(
rule
.
Priority
)
.
SetMatchMode
(
rule
.
MatchMode
)
.
SetPassthroughCode
(
rule
.
PassthroughCode
)
.
SetPassthroughBody
(
rule
.
PassthroughBody
)
SetPassthroughBody
(
rule
.
PassthroughBody
)
.
SetSkipMonitoring
(
rule
.
SkipMonitoring
)
// 处理可选字段
if
len
(
rule
.
ErrorCodes
)
>
0
{
...
...
@@ -149,6 +151,7 @@ func (r *errorPassthroughRepository) toModel(e *ent.ErrorPassthroughRule) *model
Platforms
:
e
.
Platforms
,
PassthroughCode
:
e
.
PassthroughCode
,
PassthroughBody
:
e
.
PassthroughBody
,
SkipMonitoring
:
e
.
SkipMonitoring
,
CreatedAt
:
e
.
CreatedAt
,
UpdatedAt
:
e
.
UpdatedAt
,
}
...
...
backend/internal/service/antigravity_gateway_service.go
View file @
ae6fed15
...
...
@@ -16,6 +16,7 @@ import (
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
...
...
@@ -40,6 +41,12 @@ const (
antigravitySmartRetryMaxAttempts
=
1
// 智能重试最大次数(仅重试 1 次,防止重复限流/长期等待)
antigravityDefaultRateLimitDuration
=
30
*
time
.
Second
// 默认限流时间(无 retryDelay 时使用)
// MODEL_CAPACITY_EXHAUSTED 专用重试参数
// 模型容量不足时,所有账号共享同一容量池,切换账号无意义
// 使用固定 1s 间隔重试,最多重试 60 次
antigravityModelCapacityRetryMaxAttempts
=
60
antigravityModelCapacityRetryWait
=
1
*
time
.
Second
// Google RPC 状态和类型常量
googleRPCStatusResourceExhausted
=
"RESOURCE_EXHAUSTED"
googleRPCStatusUnavailable
=
"UNAVAILABLE"
...
...
@@ -60,6 +67,9 @@ const (
// 单账号 503 退避重试:原地重试的总累计等待时间上限
// 超过此上限将不再重试,直接返回 503
antigravitySingleAccountSmartRetryTotalMaxWait
=
30
*
time
.
Second
// MODEL_CAPACITY_EXHAUSTED 全局去重:重试全部失败后的 cooldown 时间
antigravityModelCapacityCooldown
=
10
*
time
.
Second
)
// antigravityPassthroughErrorMessages 透传给客户端的错误消息白名单(小写)
...
...
@@ -68,6 +78,12 @@ var antigravityPassthroughErrorMessages = []string{
"prompt is too long"
,
}
// MODEL_CAPACITY_EXHAUSTED 全局去重:避免多个并发请求同时对同一模型进行容量耗尽重试
var
(
modelCapacityExhaustedMu
sync
.
RWMutex
modelCapacityExhaustedUntil
=
make
(
map
[
string
]
time
.
Time
)
// modelName -> cooldown until
)
const
(
antigravityBillingModelEnv
=
"GATEWAY_ANTIGRAVITY_BILL_WITH_MAPPED_MODEL"
antigravityForwardBaseURLEnv
=
"GATEWAY_ANTIGRAVITY_FORWARD_BASE_URL"
...
...
@@ -173,7 +189,7 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
}
// 判断是否触发智能重试
shouldSmartRetry
,
shouldRateLimitModel
,
waitDuration
,
modelName
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
respBody
)
shouldSmartRetry
,
shouldRateLimitModel
,
waitDuration
,
modelName
,
isModelCapacityExhausted
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
respBody
)
// 情况1: retryDelay >= 阈值,限流模型并切换账号
if
shouldRateLimitModel
{
...
...
@@ -210,20 +226,48 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
}
}
// 情况2: retryDelay < 阈值
,智能重试(最多 antigravitySmartRetryMaxAttempts 次)
// 情况2: retryDelay < 阈值
(或 MODEL_CAPACITY_EXHAUSTED),智能重试
if
shouldSmartRetry
{
var
lastRetryResp
*
http
.
Response
var
lastRetryBody
[]
byte
for
attempt
:=
1
;
attempt
<=
antigravitySmartRetryMaxAttempts
;
attempt
++
{
// MODEL_CAPACITY_EXHAUSTED 使用独立的重试参数(60 次,固定 1s 间隔)
maxAttempts
:=
antigravitySmartRetryMaxAttempts
if
isModelCapacityExhausted
{
maxAttempts
=
antigravityModelCapacityRetryMaxAttempts
waitDuration
=
antigravityModelCapacityRetryWait
// 全局去重:如果其他 goroutine 已在重试同一模型且尚在 cooldown 中,直接返回 503
if
modelName
!=
""
{
modelCapacityExhaustedMu
.
RLock
()
cooldownUntil
,
exists
:=
modelCapacityExhaustedUntil
[
modelName
]
modelCapacityExhaustedMu
.
RUnlock
()
if
exists
&&
time
.
Now
()
.
Before
(
cooldownUntil
)
{
log
.
Printf
(
"%s status=%d model_capacity_exhausted_dedup model=%s account=%d cooldown_until=%v (skip retry)"
,
p
.
prefix
,
resp
.
StatusCode
,
modelName
,
p
.
account
.
ID
,
cooldownUntil
.
Format
(
"15:04:05"
))
return
&
smartRetryResult
{
action
:
smartRetryActionBreakWithResp
,
resp
:
&
http
.
Response
{
StatusCode
:
resp
.
StatusCode
,
Header
:
resp
.
Header
.
Clone
(),
Body
:
io
.
NopCloser
(
bytes
.
NewReader
(
respBody
)),
},
}
}
}
}
for
attempt
:=
1
;
attempt
<=
maxAttempts
;
attempt
++
{
log
.
Printf
(
"%s status=%d oauth_smart_retry attempt=%d/%d delay=%v model=%s account=%d"
,
p
.
prefix
,
resp
.
StatusCode
,
attempt
,
antigravitySmartRetryM
axAttempts
,
waitDuration
,
modelName
,
p
.
account
.
ID
)
p
.
prefix
,
resp
.
StatusCode
,
attempt
,
m
axAttempts
,
waitDuration
,
modelName
,
p
.
account
.
ID
)
timer
:=
time
.
NewTimer
(
waitDuration
)
select
{
case
<-
p
.
ctx
.
Done
()
:
timer
.
Stop
()
log
.
Printf
(
"%s status=context_canceled_during_smart_retry"
,
p
.
prefix
)
return
&
smartRetryResult
{
action
:
smartRetryActionBreakWithResp
,
err
:
p
.
ctx
.
Err
()}
case
<-
time
.
After
(
waitDuration
)
:
case
<-
time
r
.
C
:
}
// 智能重试:创建新请求
...
...
@@ -243,13 +287,19 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
retryResp
,
retryErr
:=
p
.
httpUpstream
.
Do
(
retryReq
,
p
.
proxyURL
,
p
.
account
.
ID
,
p
.
account
.
Concurrency
)
if
retryErr
==
nil
&&
retryResp
!=
nil
&&
retryResp
.
StatusCode
!=
http
.
StatusTooManyRequests
&&
retryResp
.
StatusCode
!=
http
.
StatusServiceUnavailable
{
log
.
Printf
(
"%s status=%d smart_retry_success attempt=%d/%d"
,
p
.
prefix
,
retryResp
.
StatusCode
,
attempt
,
antigravitySmartRetryMaxAttempts
)
log
.
Printf
(
"%s status=%d smart_retry_success attempt=%d/%d"
,
p
.
prefix
,
retryResp
.
StatusCode
,
attempt
,
maxAttempts
)
// 重试成功,清除 MODEL_CAPACITY_EXHAUSTED cooldown
if
isModelCapacityExhausted
&&
modelName
!=
""
{
modelCapacityExhaustedMu
.
Lock
()
delete
(
modelCapacityExhaustedUntil
,
modelName
)
modelCapacityExhaustedMu
.
Unlock
()
}
return
&
smartRetryResult
{
action
:
smartRetryActionBreakWithResp
,
resp
:
retryResp
}
}
// 网络错误时,继续重试
if
retryErr
!=
nil
||
retryResp
==
nil
{
log
.
Printf
(
"%s status=smart_retry_network_error attempt=%d/%d error=%v"
,
p
.
prefix
,
attempt
,
antigravitySmartRetryM
axAttempts
,
retryErr
)
log
.
Printf
(
"%s status=smart_retry_network_error attempt=%d/%d error=%v"
,
p
.
prefix
,
attempt
,
m
axAttempts
,
retryErr
)
continue
}
...
...
@@ -259,13 +309,13 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
}
lastRetryResp
=
retryResp
if
retryResp
!=
nil
{
lastRetryBody
,
_
=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
2
<<
2
0
))
lastRetryBody
,
_
=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
8
<<
1
0
))
_
=
retryResp
.
Body
.
Close
()
}
// 解析新的重试信息,用于下次重试的等待时间
if
attempt
<
antigravitySmartRetryM
axAttempts
&&
lastRetryBody
!=
nil
{
newShouldRetry
,
_
,
newWaitDuration
,
_
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
lastRetryBody
)
// 解析新的重试信息,用于下次重试的等待时间
(MODEL_CAPACITY_EXHAUSTED 使用固定循环,跳过)
if
!
isModelCapacityExhausted
&&
attempt
<
m
axAttempts
&&
lastRetryBody
!=
nil
{
newShouldRetry
,
_
,
newWaitDuration
,
_
,
_
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
lastRetryBody
)
if
newShouldRetry
&&
newWaitDuration
>
0
{
waitDuration
=
newWaitDuration
}
...
...
@@ -282,6 +332,27 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
retryBody
=
respBody
}
// MODEL_CAPACITY_EXHAUSTED:模型容量不足,切换账号无意义
// 直接返回上游错误响应,不设置模型限流,不切换账号
if
isModelCapacityExhausted
{
// 设置 cooldown,让后续请求快速失败,避免重复重试
if
modelName
!=
""
{
modelCapacityExhaustedMu
.
Lock
()
modelCapacityExhaustedUntil
[
modelName
]
=
time
.
Now
()
.
Add
(
antigravityModelCapacityCooldown
)
modelCapacityExhaustedMu
.
Unlock
()
}
log
.
Printf
(
"%s status=%d smart_retry_exhausted_model_capacity attempts=%d model=%s account=%d body=%s (model capacity exhausted, not switching account)"
,
p
.
prefix
,
resp
.
StatusCode
,
maxAttempts
,
modelName
,
p
.
account
.
ID
,
truncateForLog
(
retryBody
,
200
))
return
&
smartRetryResult
{
action
:
smartRetryActionBreakWithResp
,
resp
:
&
http
.
Response
{
StatusCode
:
resp
.
StatusCode
,
Header
:
resp
.
Header
.
Clone
(),
Body
:
io
.
NopCloser
(
bytes
.
NewReader
(
retryBody
)),
},
}
}
// 单账号 503 退避重试模式:智能重试耗尽后不设限流、不切换账号,
// 直接返回 503 让 Handler 层的单账号退避循环做最终处理。
if
resp
.
StatusCode
==
http
.
StatusServiceUnavailable
&&
isSingleAccountRetry
(
p
.
ctx
)
{
...
...
@@ -298,7 +369,7 @@ func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParam
}
log
.
Printf
(
"%s status=%d smart_retry_exhausted attempts=%d model=%s account=%d upstream_retry_delay=%v body=%s (switch account)"
,
p
.
prefix
,
resp
.
StatusCode
,
antigravitySmartRetryM
axAttempts
,
modelName
,
p
.
account
.
ID
,
rateLimitDuration
,
truncateForLog
(
retryBody
,
200
))
p
.
prefix
,
resp
.
StatusCode
,
m
axAttempts
,
modelName
,
p
.
account
.
ID
,
rateLimitDuration
,
truncateForLog
(
retryBody
,
200
))
resetAt
:=
time
.
Now
()
.
Add
(
rateLimitDuration
)
if
p
.
accountRepo
!=
nil
&&
modelName
!=
""
{
...
...
@@ -382,11 +453,13 @@ func (s *AntigravityGatewayService) handleSingleAccountRetryInPlace(
log
.
Printf
(
"%s status=%d single_account_503_retry attempt=%d/%d delay=%v total_waited=%v model=%s account=%d"
,
p
.
prefix
,
resp
.
StatusCode
,
attempt
,
antigravitySingleAccountSmartRetryMaxAttempts
,
waitDuration
,
totalWaited
,
modelName
,
p
.
account
.
ID
)
timer
:=
time
.
NewTimer
(
waitDuration
)
select
{
case
<-
p
.
ctx
.
Done
()
:
timer
.
Stop
()
log
.
Printf
(
"%s status=context_canceled_during_single_account_retry"
,
p
.
prefix
)
return
&
smartRetryResult
{
action
:
smartRetryActionBreakWithResp
,
err
:
p
.
ctx
.
Err
()}
case
<-
time
.
After
(
waitDuration
)
:
case
<-
time
r
.
C
:
}
totalWaited
+=
waitDuration
...
...
@@ -420,12 +493,12 @@ func (s *AntigravityGatewayService) handleSingleAccountRetryInPlace(
_
=
lastRetryResp
.
Body
.
Close
()
}
lastRetryResp
=
retryResp
lastRetryBody
,
_
=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
2
<<
2
0
))
lastRetryBody
,
_
=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
8
<<
1
0
))
_
=
retryResp
.
Body
.
Close
()
// 解析新的重试信息,更新下次等待时间
if
attempt
<
antigravitySingleAccountSmartRetryMaxAttempts
&&
lastRetryBody
!=
nil
{
_
,
_
,
newWaitDuration
,
_
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
lastRetryBody
)
_
,
_
,
newWaitDuration
,
_
,
_
:=
shouldTriggerAntigravitySmartRetry
(
p
.
account
,
lastRetryBody
)
if
newWaitDuration
>
0
{
waitDuration
=
newWaitDuration
if
waitDuration
>
antigravitySingleAccountSmartRetryMaxWait
{
...
...
@@ -1392,7 +1465,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
break
}
retryBody
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
2
<<
2
0
))
retryBody
,
_
:=
io
.
ReadAll
(
io
.
LimitReader
(
retryResp
.
Body
,
8
<<
1
0
))
_
=
retryResp
.
Body
.
Close
()
if
retryResp
.
StatusCode
==
http
.
StatusTooManyRequests
{
retryBaseURL
:=
""
...
...
@@ -1473,6 +1546,27 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
s
.
handleUpstreamError
(
ctx
,
prefix
,
account
,
resp
.
StatusCode
,
resp
.
Header
,
respBody
,
originalModel
,
0
,
""
,
isStickySession
)
// 精确匹配服务端配置类 400 错误,触发同账号重试 + failover
if
resp
.
StatusCode
==
http
.
StatusBadRequest
{
msg
:=
strings
.
ToLower
(
strings
.
TrimSpace
(
extractAntigravityErrorMessage
(
respBody
)))
if
isGoogleProjectConfigError
(
msg
)
{
upstreamMsg
:=
sanitizeUpstreamErrorMessage
(
strings
.
TrimSpace
(
extractAntigravityErrorMessage
(
respBody
)))
upstreamDetail
:=
s
.
getUpstreamErrorDetail
(
respBody
)
log
.
Printf
(
"%s status=400 google_config_error failover=true upstream_message=%q account=%d"
,
prefix
,
upstreamMsg
,
account
.
ID
)
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
Platform
:
account
.
Platform
,
AccountID
:
account
.
ID
,
AccountName
:
account
.
Name
,
UpstreamStatusCode
:
resp
.
StatusCode
,
UpstreamRequestID
:
resp
.
Header
.
Get
(
"x-request-id"
),
Kind
:
"failover"
,
Message
:
upstreamMsg
,
Detail
:
upstreamDetail
,
})
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
,
ResponseBody
:
respBody
,
RetryableOnSameAccount
:
true
}
}
}
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
upstreamMsg
:=
strings
.
TrimSpace
(
extractAntigravityErrorMessage
(
respBody
))
upstreamMsg
=
sanitizeUpstreamErrorMessage
(
upstreamMsg
)
...
...
@@ -2013,6 +2107,22 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
// Always record upstream context for Ops error logs, even when we will failover.
setOpsUpstreamError
(
c
,
resp
.
StatusCode
,
upstreamMsg
,
upstreamDetail
)
// 精确匹配服务端配置类 400 错误,触发同账号重试 + failover
if
resp
.
StatusCode
==
http
.
StatusBadRequest
&&
isGoogleProjectConfigError
(
strings
.
ToLower
(
upstreamMsg
))
{
log
.
Printf
(
"%s status=400 google_config_error failover=true upstream_message=%q account=%d"
,
prefix
,
upstreamMsg
,
account
.
ID
)
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
Platform
:
account
.
Platform
,
AccountID
:
account
.
ID
,
AccountName
:
account
.
Name
,
UpstreamStatusCode
:
resp
.
StatusCode
,
UpstreamRequestID
:
requestID
,
Kind
:
"failover"
,
Message
:
upstreamMsg
,
Detail
:
upstreamDetail
,
})
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
resp
.
StatusCode
,
ResponseBody
:
unwrappedForOps
,
RetryableOnSameAccount
:
true
}
}
if
s
.
shouldFailoverUpstreamError
(
resp
.
StatusCode
)
{
appendOpsUpstreamError
(
c
,
OpsUpstreamErrorEvent
{
Platform
:
account
.
Platform
,
...
...
@@ -2108,6 +2218,44 @@ func (s *AntigravityGatewayService) shouldFailoverUpstreamError(statusCode int)
}
}
// isGoogleProjectConfigError 判断(已提取的小写)错误消息是否属于 Google 服务端配置类问题。
// 只精确匹配已知的服务端侧错误,避免对客户端请求错误做无意义重试。
// 适用于所有走 Google 后端的平台(Antigravity、Gemini)。
func
isGoogleProjectConfigError
(
lowerMsg
string
)
bool
{
// Google 间歇性 Bug:Project ID 有效但被临时识别失败
return
strings
.
Contains
(
lowerMsg
,
"invalid project resource name"
)
}
// googleConfigErrorCooldown 服务端配置类 400 错误的临时封禁时长
const
googleConfigErrorCooldown
=
1
*
time
.
Minute
// tempUnscheduleGoogleConfigError 对服务端配置类 400 错误触发临时封禁,
// 避免短时间内反复调度到同一个有问题的账号。
func
tempUnscheduleGoogleConfigError
(
ctx
context
.
Context
,
repo
AccountRepository
,
accountID
int64
,
logPrefix
string
)
{
until
:=
time
.
Now
()
.
Add
(
googleConfigErrorCooldown
)
reason
:=
"400: invalid project resource name (auto temp-unschedule 1m)"
if
err
:=
repo
.
SetTempUnschedulable
(
ctx
,
accountID
,
until
,
reason
);
err
!=
nil
{
log
.
Printf
(
"%s temp_unschedule_failed account=%d error=%v"
,
logPrefix
,
accountID
,
err
)
}
else
{
log
.
Printf
(
"%s temp_unscheduled account=%d until=%v reason=%q"
,
logPrefix
,
accountID
,
until
.
Format
(
"15:04:05"
),
reason
)
}
}
// emptyResponseCooldown 空流式响应的临时封禁时长
const
emptyResponseCooldown
=
1
*
time
.
Minute
// tempUnscheduleEmptyResponse 对空流式响应触发临时封禁,
// 避免短时间内反复调度到同一个返回空响应的账号。
func
tempUnscheduleEmptyResponse
(
ctx
context
.
Context
,
repo
AccountRepository
,
accountID
int64
,
logPrefix
string
)
{
until
:=
time
.
Now
()
.
Add
(
emptyResponseCooldown
)
reason
:=
"empty stream response (auto temp-unschedule 1m)"
if
err
:=
repo
.
SetTempUnschedulable
(
ctx
,
accountID
,
until
,
reason
);
err
!=
nil
{
log
.
Printf
(
"%s temp_unschedule_failed account=%d error=%v"
,
logPrefix
,
accountID
,
err
)
}
else
{
log
.
Printf
(
"%s temp_unscheduled account=%d until=%v reason=%q"
,
logPrefix
,
accountID
,
until
.
Format
(
"15:04:05"
),
reason
)
}
}
// sleepAntigravityBackoffWithContext 带 context 取消检查的退避等待
// 返回 true 表示正常完成等待,false 表示 context 已取消
func
sleepAntigravityBackoffWithContext
(
ctx
context
.
Context
,
attempt
int
)
bool
{
...
...
@@ -2124,10 +2272,12 @@ func sleepAntigravityBackoffWithContext(ctx context.Context, attempt int) bool {
sleepFor
=
0
}
timer
:=
time
.
NewTimer
(
sleepFor
)
select
{
case
<-
ctx
.
Done
()
:
timer
.
Stop
()
return
false
case
<-
time
.
After
(
sleepFor
)
:
case
<-
time
r
.
C
:
return
true
}
}
...
...
@@ -2172,8 +2322,9 @@ func antigravityFallbackCooldownSeconds() (time.Duration, bool) {
// antigravitySmartRetryInfo 智能重试所需的信息
type
antigravitySmartRetryInfo
struct
{
RetryDelay
time
.
Duration
// 重试延迟时间
ModelName
string
// 限流的模型名称(如 "claude-sonnet-4-5")
RetryDelay
time
.
Duration
// 重试延迟时间
ModelName
string
// 限流的模型名称(如 "claude-sonnet-4-5")
IsModelCapacityExhausted
bool
// 是否为模型容量不足(MODEL_CAPACITY_EXHAUSTED)
}
// parseAntigravitySmartRetryInfo 解析 Google RPC RetryInfo 和 ErrorInfo 信息
...
...
@@ -2288,31 +2439,40 @@ func parseAntigravitySmartRetryInfo(body []byte) *antigravitySmartRetryInfo {
}
return
&
antigravitySmartRetryInfo
{
RetryDelay
:
retryDelay
,
ModelName
:
modelName
,
RetryDelay
:
retryDelay
,
ModelName
:
modelName
,
IsModelCapacityExhausted
:
hasModelCapacityExhausted
,
}
}
// shouldTriggerAntigravitySmartRetry 判断是否应该触发智能重试
// 返回:
// - shouldRetry: 是否应该智能重试(retryDelay < antigravityRateLimitThreshold)
// - shouldRateLimitModel: 是否应该限流模型
(retryDelay >= antigravityRateLimitThreshold
)
// - waitDuration: 等待时间
(智能重试时使用,shouldRateLimitModel=true 时为 0)
// - shouldRetry: 是否应该智能重试(retryDelay < antigravityRateLimitThreshold
,或 MODEL_CAPACITY_EXHAUSTED
)
// - shouldRateLimitModel: 是否应该限流模型
并切换账号(仅 RATE_LIMIT_EXCEEDED 且 retryDelay >= 阈值
)
// - waitDuration: 等待时间
// - modelName: 限流的模型名称
func
shouldTriggerAntigravitySmartRetry
(
account
*
Account
,
respBody
[]
byte
)
(
shouldRetry
bool
,
shouldRateLimitModel
bool
,
waitDuration
time
.
Duration
,
modelName
string
)
{
// - isModelCapacityExhausted: 是否为模型容量不足(MODEL_CAPACITY_EXHAUSTED)
func
shouldTriggerAntigravitySmartRetry
(
account
*
Account
,
respBody
[]
byte
)
(
shouldRetry
bool
,
shouldRateLimitModel
bool
,
waitDuration
time
.
Duration
,
modelName
string
,
isModelCapacityExhausted
bool
)
{
if
account
.
Platform
!=
PlatformAntigravity
{
return
false
,
false
,
0
,
""
return
false
,
false
,
0
,
""
,
false
}
info
:=
parseAntigravitySmartRetryInfo
(
respBody
)
if
info
==
nil
{
return
false
,
false
,
0
,
""
return
false
,
false
,
0
,
""
,
false
}
// MODEL_CAPACITY_EXHAUSTED(模型容量不足):所有账号共享同一模型容量池
// 切换账号无意义,使用固定 1s 间隔重试
if
info
.
IsModelCapacityExhausted
{
return
true
,
false
,
antigravityModelCapacityRetryWait
,
info
.
ModelName
,
true
}
// RATE_LIMIT_EXCEEDED(账号级限流):
// retryDelay >= 阈值:直接限流模型,不重试
// 注意:如果上游未提供 retryDelay,parseAntigravitySmartRetryInfo 已设置为默认 30s
if
info
.
RetryDelay
>=
antigravityRateLimitThreshold
{
return
false
,
true
,
info
.
RetryDelay
,
info
.
ModelName
return
false
,
true
,
info
.
RetryDelay
,
info
.
ModelName
,
false
}
// retryDelay < 阈值:智能重试
...
...
@@ -2321,7 +2481,7 @@ func shouldTriggerAntigravitySmartRetry(account *Account, respBody []byte) (shou
waitDuration
=
antigravitySmartRetryMinWait
}
return
true
,
false
,
waitDuration
,
info
.
ModelName
return
true
,
false
,
waitDuration
,
info
.
ModelName
,
false
}
// handleModelRateLimitParams 模型级限流处理参数
...
...
@@ -2347,8 +2507,9 @@ type handleModelRateLimitResult struct {
// handleModelRateLimit 处理模型级限流(在原有逻辑之前调用)
// 仅处理 429/503,解析模型名和 retryDelay
// - retryDelay < antigravityRateLimitThreshold: 返回 ShouldRetry=true,由调用方等待后重试
// - retryDelay >= antigravityRateLimitThreshold: 设置模型限流 + 清除粘性会话 + 返回 SwitchError
// - MODEL_CAPACITY_EXHAUSTED: 返回 Handled=true(实际重试由 handleSmartRetry 处理)
// - RATE_LIMIT_EXCEEDED + retryDelay < 阈值: 返回 ShouldRetry=true,由调用方等待后重试
// - RATE_LIMIT_EXCEEDED + retryDelay >= 阈值: 设置模型限流 + 清除粘性会话 + 返回 SwitchError
func
(
s
*
AntigravityGatewayService
)
handleModelRateLimit
(
p
*
handleModelRateLimitParams
)
*
handleModelRateLimitResult
{
if
p
.
statusCode
!=
429
&&
p
.
statusCode
!=
503
{
return
&
handleModelRateLimitResult
{
Handled
:
false
}
...
...
@@ -2359,7 +2520,17 @@ func (s *AntigravityGatewayService) handleModelRateLimit(p *handleModelRateLimit
return
&
handleModelRateLimitResult
{
Handled
:
false
}
}
// < antigravityRateLimitThreshold: 等待后重试
// MODEL_CAPACITY_EXHAUSTED:模型容量不足,所有账号共享同一容量池
// 切换账号无意义,不设置模型限流(实际重试由 handleSmartRetry 处理)
if
info
.
IsModelCapacityExhausted
{
log
.
Printf
(
"%s status=%d model_capacity_exhausted model=%s (not switching account, retry handled by smart retry)"
,
p
.
prefix
,
p
.
statusCode
,
info
.
ModelName
)
return
&
handleModelRateLimitResult
{
Handled
:
true
,
}
}
// RATE_LIMIT_EXCEEDED: < antigravityRateLimitThreshold: 等待后重试
if
info
.
RetryDelay
<
antigravityRateLimitThreshold
{
log
.
Printf
(
"%s status=%d model_rate_limit_wait model=%s wait=%v"
,
p
.
prefix
,
p
.
statusCode
,
info
.
ModelName
,
info
.
RetryDelay
)
...
...
@@ -2370,7 +2541,7 @@ func (s *AntigravityGatewayService) handleModelRateLimit(p *handleModelRateLimit
}
}
// >= antigravityRateLimitThreshold: 设置限流 + 清除粘性会话 + 切换账号
//
RATE_LIMIT_EXCEEDED:
>= antigravityRateLimitThreshold: 设置限流 + 清除粘性会话 + 切换账号
s
.
setModelRateLimitAndClearSession
(
p
,
info
)
return
&
handleModelRateLimitResult
{
...
...
@@ -2918,9 +3089,14 @@ returnResponse:
// 选择最后一个有效响应
finalResponse
:=
pickGeminiCollectResult
(
last
,
lastWithParts
)
// 处理空响应情况
// 处理空响应情况
— 触发同账号重试 + failover 切换账号
if
last
==
nil
&&
lastWithParts
==
nil
{
log
.
Printf
(
"[antigravity-Forward] warning: empty stream response, no valid chunks received"
)
log
.
Printf
(
"[antigravity-Forward] warning: empty stream response (gemini non-stream), triggering failover"
)
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
http
.
StatusBadGateway
,
ResponseBody
:
[]
byte
(
`{"error":"empty stream response from upstream"}`
),
RetryableOnSameAccount
:
true
,
}
}
// 如果收集到了图片 parts,需要合并到最终响应中
...
...
@@ -3138,6 +3314,21 @@ func (s *AntigravityGatewayService) writeMappedClaudeError(c *gin.Context, accou
log
.
Printf
(
"[antigravity-Forward] upstream_error status=%d body=%s"
,
upstreamStatus
,
truncateForLog
(
body
,
maxBytes
))
}
// 检查错误透传规则
if
ptStatus
,
ptErrType
,
ptErrMsg
,
matched
:=
applyErrorPassthroughRule
(
c
,
account
.
Platform
,
upstreamStatus
,
body
,
0
,
""
,
""
,
);
matched
{
c
.
JSON
(
ptStatus
,
gin
.
H
{
"type"
:
"error"
,
"error"
:
gin
.
H
{
"type"
:
ptErrType
,
"message"
:
ptErrMsg
},
})
if
upstreamMsg
==
""
{
return
fmt
.
Errorf
(
"upstream error: %d"
,
upstreamStatus
)
}
return
fmt
.
Errorf
(
"upstream error: %d message=%s"
,
upstreamStatus
,
upstreamMsg
)
}
var
statusCode
int
var
errType
,
errMsg
string
...
...
@@ -3333,10 +3524,14 @@ returnResponse:
// 选择最后一个有效响应
finalResponse
:=
pickGeminiCollectResult
(
last
,
lastWithParts
)
// 处理空响应情况
// 处理空响应情况
— 触发同账号重试 + failover 切换账号
if
last
==
nil
&&
lastWithParts
==
nil
{
log
.
Printf
(
"[antigravity-Forward] warning: empty stream response, no valid chunks received"
)
return
nil
,
s
.
writeClaudeError
(
c
,
http
.
StatusBadGateway
,
"upstream_error"
,
"Empty response from upstream"
)
log
.
Printf
(
"[antigravity-Forward] warning: empty stream response (claude non-stream), triggering failover"
)
return
nil
,
&
UpstreamFailoverError
{
StatusCode
:
http
.
StatusBadGateway
,
ResponseBody
:
[]
byte
(
`{"error":"empty stream response from upstream"}`
),
RetryableOnSameAccount
:
true
,
}
}
// 将收集的所有 parts 合并到最终响应中
...
...
backend/internal/service/antigravity_rate_limit_test.go
View file @
ae6fed15
...
...
@@ -191,13 +191,14 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) {
require
.
Equal
(
t
,
"claude-sonnet-4-5"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
}
// TestHandleUpstreamError_503_ModelRateLimit 测试 503 模型限流场景
func
TestHandleUpstreamError_503_ModelRateLimit
(
t
*
testing
.
T
)
{
// TestHandleUpstreamError_503_ModelCapacityExhausted 测试 503 模型容量不足场景
// MODEL_CAPACITY_EXHAUSTED 时应等待重试,不切换账号
func
TestHandleUpstreamError_503_ModelCapacityExhausted
(
t
*
testing
.
T
)
{
repo
:=
&
stubAntigravityAccountRepo
{}
svc
:=
&
AntigravityGatewayService
{
accountRepo
:
repo
}
account
:=
&
Account
{
ID
:
3
,
Name
:
"acc-3"
,
Platform
:
PlatformAntigravity
}
// 503 + MODEL_CAPACITY_EXHAUSTED →
模型限流
// 503 + MODEL_CAPACITY_EXHAUSTED →
等待重试,不切换账号
body
:=
[]
byte
(
`{
"error": {
"status": "UNAVAILABLE",
...
...
@@ -210,13 +211,13 @@ func TestHandleUpstreamError_503_ModelRateLimit(t *testing.T) {
result
:=
svc
.
handleUpstreamError
(
context
.
Background
(),
"[test]"
,
account
,
http
.
StatusServiceUnavailable
,
http
.
Header
{},
body
,
"gemini-3-pro-high"
,
0
,
""
,
false
)
// 应该触发模型限流
// MODEL_CAPACITY_EXHAUSTED 应该标记为已处理,不切换账号,不设置模型限流
// 实际重试由 handleSmartRetry 处理
require
.
NotNil
(
t
,
result
)
require
.
True
(
t
,
result
.
Handled
)
require
.
NotNil
(
t
,
result
.
SwitchError
)
require
.
Equal
(
t
,
"gemini-3-pro-high"
,
result
.
SwitchError
.
RateLimitedModel
)
require
.
Len
(
t
,
repo
.
modelRateLimitCalls
,
1
)
require
.
Equal
(
t
,
"gemini-3-pro-high"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
require
.
False
(
t
,
result
.
ShouldRetry
,
"MODEL_CAPACITY_EXHAUSTED should not trigger retry from handleModelRateLimit path"
)
require
.
Nil
(
t
,
result
.
SwitchError
,
"MODEL_CAPACITY_EXHAUSTED should not trigger account switch"
)
require
.
Empty
(
t
,
repo
.
modelRateLimitCalls
,
"MODEL_CAPACITY_EXHAUSTED should not set model rate limit"
)
}
// TestHandleUpstreamError_503_NonModelRateLimit 测试 503 非模型限流场景(不处理)
...
...
@@ -304,11 +305,12 @@ func TestParseGeminiRateLimitResetTime_QuotaResetDelay_RoundsUp(t *testing.T) {
func
TestParseAntigravitySmartRetryInfo
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
body
string
expectedDelay
time
.
Duration
expectedModel
string
expectedNil
bool
name
string
body
string
expectedDelay
time
.
Duration
expectedModel
string
expectedNil
bool
expectedIsModelCapacityExhausted
bool
}{
{
name
:
"valid complete response with RATE_LIMIT_EXCEEDED"
,
...
...
@@ -371,8 +373,9 @@ func TestParseAntigravitySmartRetryInfo(t *testing.T) {
"message": "No capacity available for model gemini-3-pro-high on the server"
}
}`
,
expectedDelay
:
39
*
time
.
Second
,
expectedModel
:
"gemini-3-pro-high"
,
expectedDelay
:
39
*
time
.
Second
,
expectedModel
:
"gemini-3-pro-high"
,
expectedIsModelCapacityExhausted
:
true
,
},
{
name
:
"503 UNAVAILABLE without MODEL_CAPACITY_EXHAUSTED - should return nil"
,
...
...
@@ -483,6 +486,9 @@ func TestParseAntigravitySmartRetryInfo(t *testing.T) {
if
result
.
ModelName
!=
tt
.
expectedModel
{
t
.
Errorf
(
"ModelName = %q, want %q"
,
result
.
ModelName
,
tt
.
expectedModel
)
}
if
result
.
IsModelCapacityExhausted
!=
tt
.
expectedIsModelCapacityExhausted
{
t
.
Errorf
(
"IsModelCapacityExhausted = %v, want %v"
,
result
.
IsModelCapacityExhausted
,
tt
.
expectedIsModelCapacityExhausted
)
}
})
}
}
...
...
@@ -494,13 +500,14 @@ func TestShouldTriggerAntigravitySmartRetry(t *testing.T) {
apiKeyAccount
:=
&
Account
{
Type
:
AccountTypeAPIKey
}
tests
:=
[]
struct
{
name
string
account
*
Account
body
string
expectedShouldRetry
bool
expectedShouldRateLimit
bool
minWait
time
.
Duration
modelName
string
name
string
account
*
Account
body
string
expectedShouldRetry
bool
expectedShouldRateLimit
bool
expectedIsModelCapacityExhausted
bool
minWait
time
.
Duration
modelName
string
}{
{
name
:
"OAuth account with short delay (< 7s) - smart retry"
,
...
...
@@ -614,13 +621,14 @@ func TestShouldTriggerAntigravitySmartRetry(t *testing.T) {
]
}
}`
,
expectedShouldRetry
:
false
,
expectedShouldRateLimit
:
true
,
minWait
:
39
*
time
.
Second
,
modelName
:
"gemini-3-pro-high"
,
expectedShouldRetry
:
true
,
expectedShouldRateLimit
:
false
,
expectedIsModelCapacityExhausted
:
true
,
minWait
:
1
*
time
.
Second
,
modelName
:
"gemini-3-pro-high"
,
},
{
name
:
"503 UNAVAILABLE with MODEL_CAPACITY_EXHAUSTED - no retryDelay - use
default rate lim
it"
,
name
:
"503 UNAVAILABLE with MODEL_CAPACITY_EXHAUSTED - no retryDelay - use
fixed wa
it"
,
account
:
oauthAccount
,
body
:
`{
"error": {
...
...
@@ -632,10 +640,11 @@ func TestShouldTriggerAntigravitySmartRetry(t *testing.T) {
"message": "No capacity available for model gemini-2.5-flash on the server"
}
}`
,
expectedShouldRetry
:
false
,
expectedShouldRateLimit
:
true
,
minWait
:
30
*
time
.
Second
,
modelName
:
"gemini-2.5-flash"
,
expectedShouldRetry
:
true
,
expectedShouldRateLimit
:
false
,
expectedIsModelCapacityExhausted
:
true
,
minWait
:
1
*
time
.
Second
,
modelName
:
"gemini-2.5-flash"
,
},
{
name
:
"429 RESOURCE_EXHAUSTED with RATE_LIMIT_EXCEEDED - no retryDelay - use default rate limit"
,
...
...
@@ -659,13 +668,16 @@ func TestShouldTriggerAntigravitySmartRetry(t *testing.T) {
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
shouldRetry
,
shouldRateLimit
,
wait
,
model
:=
shouldTriggerAntigravitySmartRetry
(
tt
.
account
,
[]
byte
(
tt
.
body
))
shouldRetry
,
shouldRateLimit
,
wait
,
model
,
isModelCapacityExhausted
:=
shouldTriggerAntigravitySmartRetry
(
tt
.
account
,
[]
byte
(
tt
.
body
))
if
shouldRetry
!=
tt
.
expectedShouldRetry
{
t
.
Errorf
(
"shouldRetry = %v, want %v"
,
shouldRetry
,
tt
.
expectedShouldRetry
)
}
if
shouldRateLimit
!=
tt
.
expectedShouldRateLimit
{
t
.
Errorf
(
"shouldRateLimit = %v, want %v"
,
shouldRateLimit
,
tt
.
expectedShouldRateLimit
)
}
if
isModelCapacityExhausted
!=
tt
.
expectedIsModelCapacityExhausted
{
t
.
Errorf
(
"isModelCapacityExhausted = %v, want %v"
,
isModelCapacityExhausted
,
tt
.
expectedIsModelCapacityExhausted
)
}
if
shouldRetry
{
if
wait
<
tt
.
minWait
{
t
.
Errorf
(
"wait = %v, want >= %v"
,
wait
,
tt
.
minWait
)
...
...
backend/internal/service/antigravity_single_account_retry_test.go
View file @
ae6fed15
...
...
@@ -153,13 +153,14 @@ func TestHandleSmartRetry_503_LongDelay_NoSingleAccountRetry_StillSwitches(t *te
Platform
:
PlatformAntigravity
,
}
// 503 + 39s >= 7s 阈值
// 503 + 39s >= 7s 阈值(使用 RATE_LIMIT_EXCEEDED 而非 MODEL_CAPACITY_EXHAUSTED,
// 因为 MODEL_CAPACITY_EXHAUSTED 走独立的重试路径,不触发 shouldRateLimitModel)
respBody
:=
[]
byte
(
`{
"error": {
"code": 503,
"status": "
UNAVAILABLE
",
"status": "
RESOURCE_EXHAUSTED
",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro-high"}, "reason": "
MODEL_CAPAC
IT
Y
_EX
HAUST
ED"},
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro-high"}, "reason": "
RATE_LIM
IT_EX
CEED
ED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "39s"}
]
}
...
...
@@ -339,13 +340,14 @@ func TestHandleSmartRetry_503_ShortDelay_SingleAccountRetry_NoRateLimit(t *testi
// TestHandleSmartRetry_503_ShortDelay_NoSingleAccountRetry_SetsRateLimit
// 对照组:503 + retryDelay < 7s + 无 SingleAccountRetry → 智能重试耗尽后照常设限流
// 使用 RATE_LIMIT_EXCEEDED 而非 MODEL_CAPACITY_EXHAUSTED,因为后者走独立的 60 次重试路径
func
TestHandleSmartRetry_503_ShortDelay_NoSingleAccountRetry_SetsRateLimit
(
t
*
testing
.
T
)
{
failRespBody
:=
`{
"error": {
"code": 503,
"status": "
UNAVAILABLE
",
"status": "
RESOURCE_EXHAUSTED
",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-flash"}, "reason": "
MODEL_CAPAC
IT
Y
_EX
HAUST
ED"},
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-flash"}, "reason": "
RATE_LIM
IT_EX
CEED
ED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.1s"}
]
}
...
...
@@ -371,9 +373,9 @@ func TestHandleSmartRetry_503_ShortDelay_NoSingleAccountRetry_SetsRateLimit(t *t
respBody
:=
[]
byte
(
`{
"error": {
"code": 503,
"status": "
UNAVAILABLE
",
"status": "
RESOURCE_EXHAUSTED
",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-flash"}, "reason": "
MODEL_CAPAC
IT
Y
_EX
HAUST
ED"},
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-flash"}, "reason": "
RATE_LIM
IT_EX
CEED
ED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.1s"}
]
}
...
...
backend/internal/service/antigravity_smart_retry_test.go
View file @
ae6fed15
...
...
@@ -294,8 +294,9 @@ func TestHandleSmartRetry_ShortDelay_SmartRetryFailed_ReturnsSwitchError(t *test
require
.
Len
(
t
,
upstream
.
calls
,
1
,
"should have made one retry call (max attempts)"
)
}
// TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError 测试 503 MODEL_CAPACITY_EXHAUSTED 返回 switchError
func
TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError
(
t
*
testing
.
T
)
{
// TestHandleSmartRetry_503_ModelCapacityExhausted_RetrySuccess 测试 503 MODEL_CAPACITY_EXHAUSTED 重试成功
// MODEL_CAPACITY_EXHAUSTED 使用固定 1s 间隔重试,不切换账号
func
TestHandleSmartRetry_503_ModelCapacityExhausted_RetrySuccess
(
t
*
testing
.
T
)
{
repo
:=
&
stubAntigravityAccountRepo
{}
account
:=
&
Account
{
ID
:
3
,
...
...
@@ -304,7 +305,7 @@ func TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError(t *testi
Platform
:
PlatformAntigravity
,
}
// 503 + MODEL_CAPACITY_EXHAUSTED + 39s
>= 7s 阈值
// 503 + MODEL_CAPACITY_EXHAUSTED + 39s
(上游 retryDelay 应被忽略,使用固定 1s)
respBody
:=
[]
byte
(
`{
"error": {
"code": 503,
...
...
@@ -322,6 +323,14 @@ func TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError(t *testi
Body
:
io
.
NopCloser
(
bytes
.
NewReader
(
respBody
)),
}
// mock: 第 1 次重试返回 200 成功
upstream
:=
&
mockSmartRetryUpstream
{
responses
:
[]
*
http
.
Response
{
{
StatusCode
:
http
.
StatusOK
,
Header
:
http
.
Header
{},
Body
:
io
.
NopCloser
(
strings
.
NewReader
(
`{"ok":true}`
))},
},
errors
:
[]
error
{
nil
},
}
params
:=
antigravityRetryLoopParams
{
ctx
:
context
.
Background
(),
prefix
:
"[test]"
,
...
...
@@ -330,6 +339,7 @@ func TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError(t *testi
action
:
"generateContent"
,
body
:
[]
byte
(
`{"input":"test"}`
),
accountRepo
:
repo
,
httpUpstream
:
upstream
,
isStickySession
:
true
,
handleError
:
func
(
ctx
context
.
Context
,
prefix
string
,
account
*
Account
,
statusCode
int
,
headers
http
.
Header
,
body
[]
byte
,
requestedModel
string
,
groupID
int64
,
sessionHash
string
,
isStickySession
bool
)
*
handleModelRateLimitResult
{
return
nil
...
...
@@ -343,16 +353,67 @@ func TestHandleSmartRetry_503_ModelCapacityExhausted_ReturnsSwitchError(t *testi
require
.
NotNil
(
t
,
result
)
require
.
Equal
(
t
,
smartRetryActionBreakWithResp
,
result
.
action
)
require
.
Nil
(
t
,
result
.
resp
)
require
.
NotNil
(
t
,
result
.
resp
,
"should return successful response"
)
require
.
Equal
(
t
,
http
.
StatusOK
,
result
.
resp
.
StatusCode
)
require
.
Nil
(
t
,
result
.
err
)
require
.
NotNil
(
t
,
result
.
switchError
,
"should return switchError for 503 model capacity exhausted"
)
require
.
Equal
(
t
,
account
.
ID
,
result
.
switchError
.
OriginalAccountID
)
require
.
Equal
(
t
,
"gemini-3-pro-high"
,
result
.
switchError
.
RateLimitedModel
)
require
.
True
(
t
,
result
.
switchError
.
IsStickySession
)
require
.
Nil
(
t
,
result
.
switchError
,
"MODEL_CAPACITY_EXHAUSTED should not return switchError"
)
// 验证模型限流已设置
require
.
Len
(
t
,
repo
.
modelRateLimitCalls
,
1
)
require
.
Equal
(
t
,
"gemini-3-pro-high"
,
repo
.
modelRateLimitCalls
[
0
]
.
modelKey
)
// 不应设置模型限流
require
.
Empty
(
t
,
repo
.
modelRateLimitCalls
,
"MODEL_CAPACITY_EXHAUSTED should not set model rate limit"
)
require
.
Len
(
t
,
upstream
.
calls
,
1
,
"should have made one retry call before success"
)
}
// TestHandleSmartRetry_503_ModelCapacityExhausted_ContextCancel 测试 MODEL_CAPACITY_EXHAUSTED 上下文取消
func
TestHandleSmartRetry_503_ModelCapacityExhausted_ContextCancel
(
t
*
testing
.
T
)
{
repo
:=
&
stubAntigravityAccountRepo
{}
account
:=
&
Account
{
ID
:
3
,
Name
:
"acc-3"
,
Type
:
AccountTypeOAuth
,
Platform
:
PlatformAntigravity
,
}
respBody
:=
[]
byte
(
`{
"error": {
"code": 503,
"status": "UNAVAILABLE",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro-high"}, "reason": "MODEL_CAPACITY_EXHAUSTED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "39s"}
]
}
}`
)
resp
:=
&
http
.
Response
{
StatusCode
:
http
.
StatusServiceUnavailable
,
Header
:
http
.
Header
{},
Body
:
io
.
NopCloser
(
bytes
.
NewReader
(
respBody
)),
}
// 立即取消上下文,验证重试循环能正确退出
ctx
,
cancel
:=
context
.
WithCancel
(
context
.
Background
())
cancel
()
params
:=
antigravityRetryLoopParams
{
ctx
:
ctx
,
prefix
:
"[test]"
,
account
:
account
,
accessToken
:
"token"
,
action
:
"generateContent"
,
body
:
[]
byte
(
`{"input":"test"}`
),
accountRepo
:
repo
,
handleError
:
func
(
ctx
context
.
Context
,
prefix
string
,
account
*
Account
,
statusCode
int
,
headers
http
.
Header
,
body
[]
byte
,
requestedModel
string
,
groupID
int64
,
sessionHash
string
,
isStickySession
bool
)
*
handleModelRateLimitResult
{
return
nil
},
}
svc
:=
&
AntigravityGatewayService
{}
result
:=
svc
.
handleSmartRetry
(
params
,
resp
,
respBody
,
"https://ag-1.test"
,
0
,
[]
string
{
"https://ag-1.test"
})
require
.
NotNil
(
t
,
result
)
require
.
Equal
(
t
,
smartRetryActionBreakWithResp
,
result
.
action
)
require
.
Error
(
t
,
result
.
err
,
"should return context error"
)
require
.
Nil
(
t
,
result
.
switchError
,
"should not return switchError on context cancel"
)
require
.
Empty
(
t
,
repo
.
modelRateLimitCalls
,
"should not set model rate limit on context cancel"
)
}
// TestHandleSmartRetry_NonAntigravityAccount_ContinuesDefaultLogic 测试非 Antigravity 平台账号走默认逻辑
...
...
@@ -1129,20 +1190,20 @@ func TestHandleSmartRetry_ShortDelay_NetworkError_StickySession_ClearsSession(t
}
// TestHandleSmartRetry_ShortDelay_503_StickySession_FailedRetry_ClearsSession
//
503
+ 短延迟 + 粘性会话 + 重试失败 → 清除粘性绑定
//
429
+ 短延迟 + 粘性会话 + 重试失败 → 清除粘性绑定
func
TestHandleSmartRetry_ShortDelay_503_StickySession_FailedRetry_ClearsSession
(
t
*
testing
.
T
)
{
failRespBody
:=
`{
"error": {
"code":
503
,
"status": "
UNAVAILABLE
",
"code":
429
,
"status": "
RESOURCE_EXHAUSTED
",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro"}, "reason": "
MODEL_CAPAC
IT
Y
_EX
HAUST
ED"},
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro"}, "reason": "
RATE_LIM
IT_EX
CEED
ED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.5s"}
]
}
}`
failResp
:=
&
http
.
Response
{
StatusCode
:
http
.
Status
ServiceUnavailable
,
StatusCode
:
http
.
Status
TooManyRequests
,
Header
:
http
.
Header
{},
Body
:
io
.
NopCloser
(
strings
.
NewReader
(
failRespBody
)),
}
...
...
@@ -1162,16 +1223,16 @@ func TestHandleSmartRetry_ShortDelay_503_StickySession_FailedRetry_ClearsSession
respBody
:=
[]
byte
(
`{
"error": {
"code":
503
,
"status": "
UNAVAILABLE
",
"code":
429
,
"status": "
RESOURCE_EXHAUSTED
",
"details": [
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro"}, "reason": "
MODEL_CAPAC
IT
Y
_EX
HAUST
ED"},
{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "metadata": {"model": "gemini-3-pro"}, "reason": "
RATE_LIM
IT_EX
CEED
ED"},
{"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.5s"}
]
}
}`
)
resp
:=
&
http
.
Response
{
StatusCode
:
http
.
Status
ServiceUnavailable
,
StatusCode
:
http
.
Status
TooManyRequests
,
Header
:
http
.
Header
{},
Body
:
io
.
NopCloser
(
bytes
.
NewReader
(
respBody
)),
}
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment