mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-08 17:14:45 +08:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cce0a8877 | ||
|
|
225fd035ae | ||
|
|
fb7d1346b5 | ||
|
|
491a744481 | ||
|
|
f366026435 | ||
|
|
1a0d4ed668 | ||
|
|
63a8c76946 | ||
|
|
f355a68bc9 | ||
|
|
c87e6526c1 | ||
|
|
af3a5076d6 | ||
|
|
18f2e21414 | ||
|
|
8a8cdeebb4 | ||
|
|
12b33f4ea4 | ||
|
|
01b3a09d7d | ||
|
|
0d6c1c7790 | ||
|
|
95e366b6c6 | ||
|
|
77701143bf | ||
|
|
02dea7b09b | ||
|
|
c26f93c4a0 | ||
|
|
c826ac28ef | ||
|
|
1893b0eb30 | ||
|
|
05527b13db | ||
|
|
ae5d9c8bfc | ||
|
|
9117c2a4ec |
@@ -41,6 +41,8 @@ type Account struct {
|
|||||||
ProxyID *int64 `json:"proxy_id,omitempty"`
|
ProxyID *int64 `json:"proxy_id,omitempty"`
|
||||||
// Concurrency holds the value of the "concurrency" field.
|
// Concurrency holds the value of the "concurrency" field.
|
||||||
Concurrency int `json:"concurrency,omitempty"`
|
Concurrency int `json:"concurrency,omitempty"`
|
||||||
|
// LoadFactor holds the value of the "load_factor" field.
|
||||||
|
LoadFactor *int `json:"load_factor,omitempty"`
|
||||||
// Priority holds the value of the "priority" field.
|
// Priority holds the value of the "priority" field.
|
||||||
Priority int `json:"priority,omitempty"`
|
Priority int `json:"priority,omitempty"`
|
||||||
// RateMultiplier holds the value of the "rate_multiplier" field.
|
// RateMultiplier holds the value of the "rate_multiplier" field.
|
||||||
@@ -143,7 +145,7 @@ func (*Account) scanValues(columns []string) ([]any, error) {
|
|||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case account.FieldRateMultiplier:
|
case account.FieldRateMultiplier:
|
||||||
values[i] = new(sql.NullFloat64)
|
values[i] = new(sql.NullFloat64)
|
||||||
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldLoadFactor, account.FieldPriority:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldTempUnschedulableReason, account.FieldSessionWindowStatus:
|
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldTempUnschedulableReason, account.FieldSessionWindowStatus:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
@@ -243,6 +245,13 @@ func (_m *Account) assignValues(columns []string, values []any) error {
|
|||||||
} else if value.Valid {
|
} else if value.Valid {
|
||||||
_m.Concurrency = int(value.Int64)
|
_m.Concurrency = int(value.Int64)
|
||||||
}
|
}
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field load_factor", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.LoadFactor = new(int)
|
||||||
|
*_m.LoadFactor = int(value.Int64)
|
||||||
|
}
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field priority", values[i])
|
return fmt.Errorf("unexpected type %T for field priority", values[i])
|
||||||
@@ -445,6 +454,11 @@ func (_m *Account) String() string {
|
|||||||
builder.WriteString("concurrency=")
|
builder.WriteString("concurrency=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Concurrency))
|
builder.WriteString(fmt.Sprintf("%v", _m.Concurrency))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.LoadFactor; v != nil {
|
||||||
|
builder.WriteString("load_factor=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("priority=")
|
builder.WriteString("priority=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Priority))
|
builder.WriteString(fmt.Sprintf("%v", _m.Priority))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ const (
|
|||||||
FieldProxyID = "proxy_id"
|
FieldProxyID = "proxy_id"
|
||||||
// FieldConcurrency holds the string denoting the concurrency field in the database.
|
// FieldConcurrency holds the string denoting the concurrency field in the database.
|
||||||
FieldConcurrency = "concurrency"
|
FieldConcurrency = "concurrency"
|
||||||
|
// FieldLoadFactor holds the string denoting the load_factor field in the database.
|
||||||
|
FieldLoadFactor = "load_factor"
|
||||||
// FieldPriority holds the string denoting the priority field in the database.
|
// FieldPriority holds the string denoting the priority field in the database.
|
||||||
FieldPriority = "priority"
|
FieldPriority = "priority"
|
||||||
// FieldRateMultiplier holds the string denoting the rate_multiplier field in the database.
|
// FieldRateMultiplier holds the string denoting the rate_multiplier field in the database.
|
||||||
@@ -121,6 +123,7 @@ var Columns = []string{
|
|||||||
FieldExtra,
|
FieldExtra,
|
||||||
FieldProxyID,
|
FieldProxyID,
|
||||||
FieldConcurrency,
|
FieldConcurrency,
|
||||||
|
FieldLoadFactor,
|
||||||
FieldPriority,
|
FieldPriority,
|
||||||
FieldRateMultiplier,
|
FieldRateMultiplier,
|
||||||
FieldStatus,
|
FieldStatus,
|
||||||
@@ -250,6 +253,11 @@ func ByConcurrency(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldConcurrency, opts...).ToFunc()
|
return sql.OrderByField(FieldConcurrency, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByLoadFactor orders the results by the load_factor field.
|
||||||
|
func ByLoadFactor(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldLoadFactor, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// ByPriority orders the results by the priority field.
|
// ByPriority orders the results by the priority field.
|
||||||
func ByPriority(opts ...sql.OrderTermOption) OrderOption {
|
func ByPriority(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldPriority, opts...).ToFunc()
|
return sql.OrderByField(FieldPriority, opts...).ToFunc()
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ func Concurrency(v int) predicate.Account {
|
|||||||
return predicate.Account(sql.FieldEQ(FieldConcurrency, v))
|
return predicate.Account(sql.FieldEQ(FieldConcurrency, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadFactor applies equality check predicate on the "load_factor" field. It's identical to LoadFactorEQ.
|
||||||
|
func LoadFactor(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
// Priority applies equality check predicate on the "priority" field. It's identical to PriorityEQ.
|
// Priority applies equality check predicate on the "priority" field. It's identical to PriorityEQ.
|
||||||
func Priority(v int) predicate.Account {
|
func Priority(v int) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldPriority, v))
|
return predicate.Account(sql.FieldEQ(FieldPriority, v))
|
||||||
@@ -650,6 +655,56 @@ func ConcurrencyLTE(v int) predicate.Account {
|
|||||||
return predicate.Account(sql.FieldLTE(FieldConcurrency, v))
|
return predicate.Account(sql.FieldLTE(FieldConcurrency, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadFactorEQ applies the EQ predicate on the "load_factor" field.
|
||||||
|
func LoadFactorEQ(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorNEQ applies the NEQ predicate on the "load_factor" field.
|
||||||
|
func LoadFactorNEQ(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNEQ(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorIn applies the In predicate on the "load_factor" field.
|
||||||
|
func LoadFactorIn(vs ...int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIn(FieldLoadFactor, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorNotIn applies the NotIn predicate on the "load_factor" field.
|
||||||
|
func LoadFactorNotIn(vs ...int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotIn(FieldLoadFactor, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorGT applies the GT predicate on the "load_factor" field.
|
||||||
|
func LoadFactorGT(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGT(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorGTE applies the GTE predicate on the "load_factor" field.
|
||||||
|
func LoadFactorGTE(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGTE(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorLT applies the LT predicate on the "load_factor" field.
|
||||||
|
func LoadFactorLT(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLT(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorLTE applies the LTE predicate on the "load_factor" field.
|
||||||
|
func LoadFactorLTE(v int) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLTE(FieldLoadFactor, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorIsNil applies the IsNil predicate on the "load_factor" field.
|
||||||
|
func LoadFactorIsNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIsNull(FieldLoadFactor))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorNotNil applies the NotNil predicate on the "load_factor" field.
|
||||||
|
func LoadFactorNotNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotNull(FieldLoadFactor))
|
||||||
|
}
|
||||||
|
|
||||||
// PriorityEQ applies the EQ predicate on the "priority" field.
|
// PriorityEQ applies the EQ predicate on the "priority" field.
|
||||||
func PriorityEQ(v int) predicate.Account {
|
func PriorityEQ(v int) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldPriority, v))
|
return predicate.Account(sql.FieldEQ(FieldPriority, v))
|
||||||
|
|||||||
@@ -139,6 +139,20 @@ func (_c *AccountCreate) SetNillableConcurrency(v *int) *AccountCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (_c *AccountCreate) SetLoadFactor(v int) *AccountCreate {
|
||||||
|
_c.mutation.SetLoadFactor(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLoadFactor sets the "load_factor" field if the given value is not nil.
|
||||||
|
func (_c *AccountCreate) SetNillableLoadFactor(v *int) *AccountCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetLoadFactor(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (_c *AccountCreate) SetPriority(v int) *AccountCreate {
|
func (_c *AccountCreate) SetPriority(v int) *AccountCreate {
|
||||||
_c.mutation.SetPriority(v)
|
_c.mutation.SetPriority(v)
|
||||||
@@ -623,6 +637,10 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(account.FieldConcurrency, field.TypeInt, value)
|
_spec.SetField(account.FieldConcurrency, field.TypeInt, value)
|
||||||
_node.Concurrency = value
|
_node.Concurrency = value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.LoadFactor(); ok {
|
||||||
|
_spec.SetField(account.FieldLoadFactor, field.TypeInt, value)
|
||||||
|
_node.LoadFactor = &value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.Priority(); ok {
|
if value, ok := _c.mutation.Priority(); ok {
|
||||||
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
||||||
_node.Priority = value
|
_node.Priority = value
|
||||||
@@ -936,6 +954,30 @@ func (u *AccountUpsert) AddConcurrency(v int) *AccountUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (u *AccountUpsert) SetLoadFactor(v int) *AccountUpsert {
|
||||||
|
u.Set(account.FieldLoadFactor, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLoadFactor sets the "load_factor" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsert) UpdateLoadFactor() *AccountUpsert {
|
||||||
|
u.SetExcluded(account.FieldLoadFactor)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds v to the "load_factor" field.
|
||||||
|
func (u *AccountUpsert) AddLoadFactor(v int) *AccountUpsert {
|
||||||
|
u.Add(account.FieldLoadFactor, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (u *AccountUpsert) ClearLoadFactor() *AccountUpsert {
|
||||||
|
u.SetNull(account.FieldLoadFactor)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (u *AccountUpsert) SetPriority(v int) *AccountUpsert {
|
func (u *AccountUpsert) SetPriority(v int) *AccountUpsert {
|
||||||
u.Set(account.FieldPriority, v)
|
u.Set(account.FieldPriority, v)
|
||||||
@@ -1419,6 +1461,34 @@ func (u *AccountUpsertOne) UpdateConcurrency() *AccountUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (u *AccountUpsertOne) SetLoadFactor(v int) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetLoadFactor(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds v to the "load_factor" field.
|
||||||
|
func (u *AccountUpsertOne) AddLoadFactor(v int) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.AddLoadFactor(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLoadFactor sets the "load_factor" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertOne) UpdateLoadFactor() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateLoadFactor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (u *AccountUpsertOne) ClearLoadFactor() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearLoadFactor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (u *AccountUpsertOne) SetPriority(v int) *AccountUpsertOne {
|
func (u *AccountUpsertOne) SetPriority(v int) *AccountUpsertOne {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
@@ -2113,6 +2183,34 @@ func (u *AccountUpsertBulk) UpdateConcurrency() *AccountUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (u *AccountUpsertBulk) SetLoadFactor(v int) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetLoadFactor(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds v to the "load_factor" field.
|
||||||
|
func (u *AccountUpsertBulk) AddLoadFactor(v int) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.AddLoadFactor(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLoadFactor sets the "load_factor" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertBulk) UpdateLoadFactor() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateLoadFactor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (u *AccountUpsertBulk) ClearLoadFactor() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearLoadFactor()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (u *AccountUpsertBulk) SetPriority(v int) *AccountUpsertBulk {
|
func (u *AccountUpsertBulk) SetPriority(v int) *AccountUpsertBulk {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
|||||||
@@ -172,6 +172,33 @@ func (_u *AccountUpdate) AddConcurrency(v int) *AccountUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (_u *AccountUpdate) SetLoadFactor(v int) *AccountUpdate {
|
||||||
|
_u.mutation.ResetLoadFactor()
|
||||||
|
_u.mutation.SetLoadFactor(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLoadFactor sets the "load_factor" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdate) SetNillableLoadFactor(v *int) *AccountUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetLoadFactor(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds value to the "load_factor" field.
|
||||||
|
func (_u *AccountUpdate) AddLoadFactor(v int) *AccountUpdate {
|
||||||
|
_u.mutation.AddLoadFactor(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (_u *AccountUpdate) ClearLoadFactor() *AccountUpdate {
|
||||||
|
_u.mutation.ClearLoadFactor()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (_u *AccountUpdate) SetPriority(v int) *AccountUpdate {
|
func (_u *AccountUpdate) SetPriority(v int) *AccountUpdate {
|
||||||
_u.mutation.ResetPriority()
|
_u.mutation.ResetPriority()
|
||||||
@@ -684,6 +711,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if value, ok := _u.mutation.AddedConcurrency(); ok {
|
if value, ok := _u.mutation.AddedConcurrency(); ok {
|
||||||
_spec.AddField(account.FieldConcurrency, field.TypeInt, value)
|
_spec.AddField(account.FieldConcurrency, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.LoadFactor(); ok {
|
||||||
|
_spec.SetField(account.FieldLoadFactor, field.TypeInt, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedLoadFactor(); ok {
|
||||||
|
_spec.AddField(account.FieldLoadFactor, field.TypeInt, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.LoadFactorCleared() {
|
||||||
|
_spec.ClearField(account.FieldLoadFactor, field.TypeInt)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Priority(); ok {
|
if value, ok := _u.mutation.Priority(); ok {
|
||||||
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
@@ -1063,6 +1099,33 @@ func (_u *AccountUpdateOne) AddConcurrency(v int) *AccountUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (_u *AccountUpdateOne) SetLoadFactor(v int) *AccountUpdateOne {
|
||||||
|
_u.mutation.ResetLoadFactor()
|
||||||
|
_u.mutation.SetLoadFactor(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableLoadFactor sets the "load_factor" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdateOne) SetNillableLoadFactor(v *int) *AccountUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetLoadFactor(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds value to the "load_factor" field.
|
||||||
|
func (_u *AccountUpdateOne) AddLoadFactor(v int) *AccountUpdateOne {
|
||||||
|
_u.mutation.AddLoadFactor(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (_u *AccountUpdateOne) ClearLoadFactor() *AccountUpdateOne {
|
||||||
|
_u.mutation.ClearLoadFactor()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (_u *AccountUpdateOne) SetPriority(v int) *AccountUpdateOne {
|
func (_u *AccountUpdateOne) SetPriority(v int) *AccountUpdateOne {
|
||||||
_u.mutation.ResetPriority()
|
_u.mutation.ResetPriority()
|
||||||
@@ -1605,6 +1668,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
|
|||||||
if value, ok := _u.mutation.AddedConcurrency(); ok {
|
if value, ok := _u.mutation.AddedConcurrency(); ok {
|
||||||
_spec.AddField(account.FieldConcurrency, field.TypeInt, value)
|
_spec.AddField(account.FieldConcurrency, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.LoadFactor(); ok {
|
||||||
|
_spec.SetField(account.FieldLoadFactor, field.TypeInt, value)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AddedLoadFactor(); ok {
|
||||||
|
_spec.AddField(account.FieldLoadFactor, field.TypeInt, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.LoadFactorCleared() {
|
||||||
|
_spec.ClearField(account.FieldLoadFactor, field.TypeInt)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Priority(); ok {
|
if value, ok := _u.mutation.Priority(); ok {
|
||||||
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
_spec.SetField(account.FieldPriority, field.TypeInt, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ var (
|
|||||||
{Name: "credentials", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
{Name: "credentials", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||||
{Name: "extra", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
{Name: "extra", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||||
{Name: "concurrency", Type: field.TypeInt, Default: 3},
|
{Name: "concurrency", Type: field.TypeInt, Default: 3},
|
||||||
|
{Name: "load_factor", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "priority", Type: field.TypeInt, Default: 50},
|
{Name: "priority", Type: field.TypeInt, Default: 50},
|
||||||
{Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
{Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||||
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
||||||
@@ -132,7 +133,7 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "accounts_proxies_proxy",
|
Symbol: "accounts_proxies_proxy",
|
||||||
Columns: []*schema.Column{AccountsColumns[27]},
|
Columns: []*schema.Column{AccountsColumns[28]},
|
||||||
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
@@ -151,52 +152,52 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "account_status",
|
Name: "account_status",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[13]},
|
Columns: []*schema.Column{AccountsColumns[14]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_proxy_id",
|
Name: "account_proxy_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[27]},
|
Columns: []*schema.Column{AccountsColumns[28]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_priority",
|
Name: "account_priority",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[11]},
|
Columns: []*schema.Column{AccountsColumns[12]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_last_used_at",
|
Name: "account_last_used_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[15]},
|
Columns: []*schema.Column{AccountsColumns[16]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_schedulable",
|
Name: "account_schedulable",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[18]},
|
Columns: []*schema.Column{AccountsColumns[19]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limited_at",
|
Name: "account_rate_limited_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[19]},
|
Columns: []*schema.Column{AccountsColumns[20]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limit_reset_at",
|
Name: "account_rate_limit_reset_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[20]},
|
Columns: []*schema.Column{AccountsColumns[21]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_overload_until",
|
Name: "account_overload_until",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[21]},
|
Columns: []*schema.Column{AccountsColumns[22]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_platform_priority",
|
Name: "account_platform_priority",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[6], AccountsColumns[11]},
|
Columns: []*schema.Column{AccountsColumns[6], AccountsColumns[12]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_priority_status",
|
Name: "account_priority_status",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[11], AccountsColumns[13]},
|
Columns: []*schema.Column{AccountsColumns[12], AccountsColumns[14]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_deleted_at",
|
Name: "account_deleted_at",
|
||||||
|
|||||||
@@ -2260,6 +2260,8 @@ type AccountMutation struct {
|
|||||||
extra *map[string]interface{}
|
extra *map[string]interface{}
|
||||||
concurrency *int
|
concurrency *int
|
||||||
addconcurrency *int
|
addconcurrency *int
|
||||||
|
load_factor *int
|
||||||
|
addload_factor *int
|
||||||
priority *int
|
priority *int
|
||||||
addpriority *int
|
addpriority *int
|
||||||
rate_multiplier *float64
|
rate_multiplier *float64
|
||||||
@@ -2845,6 +2847,76 @@ func (m *AccountMutation) ResetConcurrency() {
|
|||||||
m.addconcurrency = nil
|
m.addconcurrency = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLoadFactor sets the "load_factor" field.
|
||||||
|
func (m *AccountMutation) SetLoadFactor(i int) {
|
||||||
|
m.load_factor = &i
|
||||||
|
m.addload_factor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactor returns the value of the "load_factor" field in the mutation.
|
||||||
|
func (m *AccountMutation) LoadFactor() (r int, exists bool) {
|
||||||
|
v := m.load_factor
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldLoadFactor returns the old "load_factor" field's value of the Account entity.
|
||||||
|
// If the Account 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 *AccountMutation) OldLoadFactor(ctx context.Context) (v *int, err error) {
|
||||||
|
if !m.op.Is(OpUpdateOne) {
|
||||||
|
return v, errors.New("OldLoadFactor is only allowed on UpdateOne operations")
|
||||||
|
}
|
||||||
|
if m.id == nil || m.oldValue == nil {
|
||||||
|
return v, errors.New("OldLoadFactor requires an ID field in the mutation")
|
||||||
|
}
|
||||||
|
oldValue, err := m.oldValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("querying old value for OldLoadFactor: %w", err)
|
||||||
|
}
|
||||||
|
return oldValue.LoadFactor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLoadFactor adds i to the "load_factor" field.
|
||||||
|
func (m *AccountMutation) AddLoadFactor(i int) {
|
||||||
|
if m.addload_factor != nil {
|
||||||
|
*m.addload_factor += i
|
||||||
|
} else {
|
||||||
|
m.addload_factor = &i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddedLoadFactor returns the value that was added to the "load_factor" field in this mutation.
|
||||||
|
func (m *AccountMutation) AddedLoadFactor() (r int, exists bool) {
|
||||||
|
v := m.addload_factor
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearLoadFactor clears the value of the "load_factor" field.
|
||||||
|
func (m *AccountMutation) ClearLoadFactor() {
|
||||||
|
m.load_factor = nil
|
||||||
|
m.addload_factor = nil
|
||||||
|
m.clearedFields[account.FieldLoadFactor] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFactorCleared returns if the "load_factor" field was cleared in this mutation.
|
||||||
|
func (m *AccountMutation) LoadFactorCleared() bool {
|
||||||
|
_, ok := m.clearedFields[account.FieldLoadFactor]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetLoadFactor resets all changes to the "load_factor" field.
|
||||||
|
func (m *AccountMutation) ResetLoadFactor() {
|
||||||
|
m.load_factor = nil
|
||||||
|
m.addload_factor = nil
|
||||||
|
delete(m.clearedFields, account.FieldLoadFactor)
|
||||||
|
}
|
||||||
|
|
||||||
// SetPriority sets the "priority" field.
|
// SetPriority sets the "priority" field.
|
||||||
func (m *AccountMutation) SetPriority(i int) {
|
func (m *AccountMutation) SetPriority(i int) {
|
||||||
m.priority = &i
|
m.priority = &i
|
||||||
@@ -3773,7 +3845,7 @@ func (m *AccountMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *AccountMutation) Fields() []string {
|
func (m *AccountMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 27)
|
fields := make([]string, 0, 28)
|
||||||
if m.created_at != nil {
|
if m.created_at != nil {
|
||||||
fields = append(fields, account.FieldCreatedAt)
|
fields = append(fields, account.FieldCreatedAt)
|
||||||
}
|
}
|
||||||
@@ -3807,6 +3879,9 @@ func (m *AccountMutation) Fields() []string {
|
|||||||
if m.concurrency != nil {
|
if m.concurrency != nil {
|
||||||
fields = append(fields, account.FieldConcurrency)
|
fields = append(fields, account.FieldConcurrency)
|
||||||
}
|
}
|
||||||
|
if m.load_factor != nil {
|
||||||
|
fields = append(fields, account.FieldLoadFactor)
|
||||||
|
}
|
||||||
if m.priority != nil {
|
if m.priority != nil {
|
||||||
fields = append(fields, account.FieldPriority)
|
fields = append(fields, account.FieldPriority)
|
||||||
}
|
}
|
||||||
@@ -3885,6 +3960,8 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.ProxyID()
|
return m.ProxyID()
|
||||||
case account.FieldConcurrency:
|
case account.FieldConcurrency:
|
||||||
return m.Concurrency()
|
return m.Concurrency()
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
return m.LoadFactor()
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
return m.Priority()
|
return m.Priority()
|
||||||
case account.FieldRateMultiplier:
|
case account.FieldRateMultiplier:
|
||||||
@@ -3948,6 +4025,8 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
|
|||||||
return m.OldProxyID(ctx)
|
return m.OldProxyID(ctx)
|
||||||
case account.FieldConcurrency:
|
case account.FieldConcurrency:
|
||||||
return m.OldConcurrency(ctx)
|
return m.OldConcurrency(ctx)
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
return m.OldLoadFactor(ctx)
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
return m.OldPriority(ctx)
|
return m.OldPriority(ctx)
|
||||||
case account.FieldRateMultiplier:
|
case account.FieldRateMultiplier:
|
||||||
@@ -4066,6 +4145,13 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.SetConcurrency(v)
|
m.SetConcurrency(v)
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
v, ok := value.(int)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.SetLoadFactor(v)
|
||||||
|
return nil
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
v, ok := value.(int)
|
v, ok := value.(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -4189,6 +4275,9 @@ func (m *AccountMutation) AddedFields() []string {
|
|||||||
if m.addconcurrency != nil {
|
if m.addconcurrency != nil {
|
||||||
fields = append(fields, account.FieldConcurrency)
|
fields = append(fields, account.FieldConcurrency)
|
||||||
}
|
}
|
||||||
|
if m.addload_factor != nil {
|
||||||
|
fields = append(fields, account.FieldLoadFactor)
|
||||||
|
}
|
||||||
if m.addpriority != nil {
|
if m.addpriority != nil {
|
||||||
fields = append(fields, account.FieldPriority)
|
fields = append(fields, account.FieldPriority)
|
||||||
}
|
}
|
||||||
@@ -4205,6 +4294,8 @@ func (m *AccountMutation) AddedField(name string) (ent.Value, bool) {
|
|||||||
switch name {
|
switch name {
|
||||||
case account.FieldConcurrency:
|
case account.FieldConcurrency:
|
||||||
return m.AddedConcurrency()
|
return m.AddedConcurrency()
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
return m.AddedLoadFactor()
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
return m.AddedPriority()
|
return m.AddedPriority()
|
||||||
case account.FieldRateMultiplier:
|
case account.FieldRateMultiplier:
|
||||||
@@ -4225,6 +4316,13 @@ func (m *AccountMutation) AddField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.AddConcurrency(v)
|
m.AddConcurrency(v)
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
v, ok := value.(int)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.AddLoadFactor(v)
|
||||||
|
return nil
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
v, ok := value.(int)
|
v, ok := value.(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -4256,6 +4354,9 @@ func (m *AccountMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(account.FieldProxyID) {
|
if m.FieldCleared(account.FieldProxyID) {
|
||||||
fields = append(fields, account.FieldProxyID)
|
fields = append(fields, account.FieldProxyID)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(account.FieldLoadFactor) {
|
||||||
|
fields = append(fields, account.FieldLoadFactor)
|
||||||
|
}
|
||||||
if m.FieldCleared(account.FieldErrorMessage) {
|
if m.FieldCleared(account.FieldErrorMessage) {
|
||||||
fields = append(fields, account.FieldErrorMessage)
|
fields = append(fields, account.FieldErrorMessage)
|
||||||
}
|
}
|
||||||
@@ -4312,6 +4413,9 @@ func (m *AccountMutation) ClearField(name string) error {
|
|||||||
case account.FieldProxyID:
|
case account.FieldProxyID:
|
||||||
m.ClearProxyID()
|
m.ClearProxyID()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
m.ClearLoadFactor()
|
||||||
|
return nil
|
||||||
case account.FieldErrorMessage:
|
case account.FieldErrorMessage:
|
||||||
m.ClearErrorMessage()
|
m.ClearErrorMessage()
|
||||||
return nil
|
return nil
|
||||||
@@ -4386,6 +4490,9 @@ func (m *AccountMutation) ResetField(name string) error {
|
|||||||
case account.FieldConcurrency:
|
case account.FieldConcurrency:
|
||||||
m.ResetConcurrency()
|
m.ResetConcurrency()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldLoadFactor:
|
||||||
|
m.ResetLoadFactor()
|
||||||
|
return nil
|
||||||
case account.FieldPriority:
|
case account.FieldPriority:
|
||||||
m.ResetPriority()
|
m.ResetPriority()
|
||||||
return nil
|
return nil
|
||||||
@@ -10191,7 +10298,7 @@ func (m *GroupMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *GroupMutation) Fields() []string {
|
func (m *GroupMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 30)
|
fields := make([]string, 0, 31)
|
||||||
if m.created_at != nil {
|
if m.created_at != nil {
|
||||||
fields = append(fields, group.FieldCreatedAt)
|
fields = append(fields, group.FieldCreatedAt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,29 +212,29 @@ func init() {
|
|||||||
// account.DefaultConcurrency holds the default value on creation for the concurrency field.
|
// account.DefaultConcurrency holds the default value on creation for the concurrency field.
|
||||||
account.DefaultConcurrency = accountDescConcurrency.Default.(int)
|
account.DefaultConcurrency = accountDescConcurrency.Default.(int)
|
||||||
// accountDescPriority is the schema descriptor for priority field.
|
// accountDescPriority is the schema descriptor for priority field.
|
||||||
accountDescPriority := accountFields[8].Descriptor()
|
accountDescPriority := accountFields[9].Descriptor()
|
||||||
// account.DefaultPriority holds the default value on creation for the priority field.
|
// account.DefaultPriority holds the default value on creation for the priority field.
|
||||||
account.DefaultPriority = accountDescPriority.Default.(int)
|
account.DefaultPriority = accountDescPriority.Default.(int)
|
||||||
// accountDescRateMultiplier is the schema descriptor for rate_multiplier field.
|
// accountDescRateMultiplier is the schema descriptor for rate_multiplier field.
|
||||||
accountDescRateMultiplier := accountFields[9].Descriptor()
|
accountDescRateMultiplier := accountFields[10].Descriptor()
|
||||||
// account.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
|
// account.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
|
||||||
account.DefaultRateMultiplier = accountDescRateMultiplier.Default.(float64)
|
account.DefaultRateMultiplier = accountDescRateMultiplier.Default.(float64)
|
||||||
// accountDescStatus is the schema descriptor for status field.
|
// accountDescStatus is the schema descriptor for status field.
|
||||||
accountDescStatus := accountFields[10].Descriptor()
|
accountDescStatus := accountFields[11].Descriptor()
|
||||||
// account.DefaultStatus holds the default value on creation for the status field.
|
// account.DefaultStatus holds the default value on creation for the status field.
|
||||||
account.DefaultStatus = accountDescStatus.Default.(string)
|
account.DefaultStatus = accountDescStatus.Default.(string)
|
||||||
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||||
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
|
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
|
||||||
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
|
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
|
||||||
accountDescAutoPauseOnExpired := accountFields[14].Descriptor()
|
accountDescAutoPauseOnExpired := accountFields[15].Descriptor()
|
||||||
// account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field.
|
// account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field.
|
||||||
account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool)
|
account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool)
|
||||||
// accountDescSchedulable is the schema descriptor for schedulable field.
|
// accountDescSchedulable is the schema descriptor for schedulable field.
|
||||||
accountDescSchedulable := accountFields[15].Descriptor()
|
accountDescSchedulable := accountFields[16].Descriptor()
|
||||||
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
||||||
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
||||||
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
||||||
accountDescSessionWindowStatus := accountFields[23].Descriptor()
|
accountDescSessionWindowStatus := accountFields[24].Descriptor()
|
||||||
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
||||||
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
||||||
accountgroupFields := schema.AccountGroup{}.Fields()
|
accountgroupFields := schema.AccountGroup{}.Fields()
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ func (Account) Fields() []ent.Field {
|
|||||||
field.Int("concurrency").
|
field.Int("concurrency").
|
||||||
Default(3),
|
Default(3),
|
||||||
|
|
||||||
|
field.Int("load_factor").Optional().Nillable(),
|
||||||
|
|
||||||
// priority: 账户优先级,数值越小优先级越高
|
// priority: 账户优先级,数值越小优先级越高
|
||||||
// 调度器会优先使用高优先级的账户
|
// 调度器会优先使用高优先级的账户
|
||||||
field.Int("priority").
|
field.Int("priority").
|
||||||
|
|||||||
@@ -124,6 +124,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||||
|
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
|
||||||
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
@@ -171,8 +173,6 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -182,7 +182,6 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17
|
|||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
|
||||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -203,6 +202,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
|||||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
|
||||||
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -285,6 +286,10 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo=
|
||||||
|
github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
||||||
|
github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmhSkRxsIvsK4=
|
||||||
|
github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -398,8 +403,6 @@ go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/Wgbsd
|
|||||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
|
||||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
@@ -455,8 +458,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
|
||||||
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ type CreateAccountRequest struct {
|
|||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||||
|
LoadFactor *int `json:"load_factor"`
|
||||||
GroupIDs []int64 `json:"group_ids"`
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
ExpiresAt *int64 `json:"expires_at"`
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
@@ -120,6 +121,7 @@ type UpdateAccountRequest struct {
|
|||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||||
|
LoadFactor *int `json:"load_factor"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
ExpiresAt *int64 `json:"expires_at"`
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
@@ -135,6 +137,7 @@ type BulkUpdateAccountsRequest struct {
|
|||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
RateMultiplier *float64 `json:"rate_multiplier"`
|
RateMultiplier *float64 `json:"rate_multiplier"`
|
||||||
|
LoadFactor *int `json:"load_factor"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
|
||||||
Schedulable *bool `json:"schedulable"`
|
Schedulable *bool `json:"schedulable"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
@@ -285,32 +288,48 @@ func (h *AccountHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅非 lite 模式获取窗口费用(PostgreSQL 聚合查询,高开销)
|
// 窗口费用获取:lite 模式从快照缓存读取,非 lite 模式执行 PostgreSQL 查询后写入缓存
|
||||||
if !lite && len(windowCostAccountIDs) > 0 {
|
if len(windowCostAccountIDs) > 0 {
|
||||||
windowCosts = make(map[int64]float64)
|
if lite {
|
||||||
var mu sync.Mutex
|
// lite 模式:尝试从快照缓存读取
|
||||||
g, gctx := errgroup.WithContext(c.Request.Context())
|
cacheKey := buildWindowCostCacheKey(windowCostAccountIDs)
|
||||||
g.SetLimit(10) // 限制并发数
|
if cached, ok := accountWindowCostCache.Get(cacheKey); ok {
|
||||||
|
if costs, ok := cached.Payload.(map[int64]float64); ok {
|
||||||
for i := range accounts {
|
windowCosts = costs
|
||||||
acc := &accounts[i]
|
|
||||||
if !acc.IsAnthropicOAuthOrSetupToken() || acc.GetWindowCostLimit() <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
accCopy := acc // 闭包捕获
|
|
||||||
g.Go(func() error {
|
|
||||||
// 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况)
|
|
||||||
startTime := accCopy.GetCurrentWindowStartTime()
|
|
||||||
stats, err := h.accountUsageService.GetAccountWindowStats(gctx, accCopy.ID, startTime)
|
|
||||||
if err == nil && stats != nil {
|
|
||||||
mu.Lock()
|
|
||||||
windowCosts[accCopy.ID] = stats.StandardCost // 使用标准费用
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
}
|
||||||
return nil // 不返回错误,允许部分失败
|
}
|
||||||
})
|
// 缓存未命中则 windowCosts 保持 nil(仅发生在服务刚启动时)
|
||||||
|
} else {
|
||||||
|
// 非 lite 模式:执行 PostgreSQL 聚合查询(高开销)
|
||||||
|
windowCosts = make(map[int64]float64)
|
||||||
|
var mu sync.Mutex
|
||||||
|
g, gctx := errgroup.WithContext(c.Request.Context())
|
||||||
|
g.SetLimit(10) // 限制并发数
|
||||||
|
|
||||||
|
for i := range accounts {
|
||||||
|
acc := &accounts[i]
|
||||||
|
if !acc.IsAnthropicOAuthOrSetupToken() || acc.GetWindowCostLimit() <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accCopy := acc // 闭包捕获
|
||||||
|
g.Go(func() error {
|
||||||
|
// 使用统一的窗口开始时间计算逻辑(考虑窗口过期情况)
|
||||||
|
startTime := accCopy.GetCurrentWindowStartTime()
|
||||||
|
stats, err := h.accountUsageService.GetAccountWindowStats(gctx, accCopy.ID, startTime)
|
||||||
|
if err == nil && stats != nil {
|
||||||
|
mu.Lock()
|
||||||
|
windowCosts[accCopy.ID] = stats.StandardCost // 使用标准费用
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
return nil // 不返回错误,允许部分失败
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ = g.Wait()
|
||||||
|
|
||||||
|
// 查询完毕后写入快照缓存,供 lite 模式使用
|
||||||
|
cacheKey := buildWindowCostCacheKey(windowCostAccountIDs)
|
||||||
|
accountWindowCostCache.Set(cacheKey, windowCosts)
|
||||||
}
|
}
|
||||||
_ = g.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build response with concurrency info
|
// Build response with concurrency info
|
||||||
@@ -506,6 +525,7 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
RateMultiplier: req.RateMultiplier,
|
RateMultiplier: req.RateMultiplier,
|
||||||
|
LoadFactor: req.LoadFactor,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
ExpiresAt: req.ExpiresAt,
|
ExpiresAt: req.ExpiresAt,
|
||||||
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
@@ -575,6 +595,7 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency, // 指针类型,nil 表示未提供
|
Concurrency: req.Concurrency, // 指针类型,nil 表示未提供
|
||||||
Priority: req.Priority, // 指针类型,nil 表示未提供
|
Priority: req.Priority, // 指针类型,nil 表示未提供
|
||||||
RateMultiplier: req.RateMultiplier,
|
RateMultiplier: req.RateMultiplier,
|
||||||
|
LoadFactor: req.LoadFactor,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
ExpiresAt: req.ExpiresAt,
|
ExpiresAt: req.ExpiresAt,
|
||||||
@@ -1101,6 +1122,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
req.Concurrency != nil ||
|
req.Concurrency != nil ||
|
||||||
req.Priority != nil ||
|
req.Priority != nil ||
|
||||||
req.RateMultiplier != nil ||
|
req.RateMultiplier != nil ||
|
||||||
|
req.LoadFactor != nil ||
|
||||||
req.Status != "" ||
|
req.Status != "" ||
|
||||||
req.Schedulable != nil ||
|
req.Schedulable != nil ||
|
||||||
req.GroupIDs != nil ||
|
req.GroupIDs != nil ||
|
||||||
@@ -1119,6 +1141,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
RateMultiplier: req.RateMultiplier,
|
RateMultiplier: req.RateMultiplier,
|
||||||
|
LoadFactor: req.LoadFactor,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
Schedulable: req.Schedulable,
|
Schedulable: req.Schedulable,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
@@ -1328,6 +1351,29 @@ func (h *AccountHandler) ClearRateLimit(c *gin.Context) {
|
|||||||
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetQuota handles resetting account quota usage
|
||||||
|
// POST /api/v1/admin/accounts/:id/reset-quota
|
||||||
|
func (h *AccountHandler) ResetQuota(c *gin.Context) {
|
||||||
|
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, "Invalid account ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.adminService.ResetAccountQuota(c.Request.Context(), accountID); err != nil {
|
||||||
|
response.InternalError(c, "Failed to reset account quota: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := h.adminService.GetAccount(c.Request.Context(), accountID)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account))
|
||||||
|
}
|
||||||
|
|
||||||
// GetTempUnschedulable handles getting temporary unschedulable status
|
// GetTempUnschedulable handles getting temporary unschedulable status
|
||||||
// GET /api/v1/admin/accounts/:id/temp-unschedulable
|
// GET /api/v1/admin/accounts/:id/temp-unschedulable
|
||||||
func (h *AccountHandler) GetTempUnschedulable(c *gin.Context) {
|
func (h *AccountHandler) GetTempUnschedulable(c *gin.Context) {
|
||||||
|
|||||||
25
backend/internal/handler/admin/account_window_cost_cache.go
Normal file
25
backend/internal/handler/admin/account_window_cost_cache.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var accountWindowCostCache = newSnapshotCache(30 * time.Second)
|
||||||
|
|
||||||
|
func buildWindowCostCacheKey(accountIDs []int64) string {
|
||||||
|
if len(accountIDs) == 0 {
|
||||||
|
return "accounts_window_cost_empty"
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(accountIDs) * 6)
|
||||||
|
_, _ = b.WriteString("accounts_window_cost:")
|
||||||
|
for i, id := range accountIDs {
|
||||||
|
if i > 0 {
|
||||||
|
_ = b.WriteByte(',')
|
||||||
|
}
|
||||||
|
_, _ = b.WriteString(strconv.FormatInt(id, 10))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -425,5 +425,9 @@ func (s *stubAdminService) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
|
|||||||
return nil, service.ErrAPIKeyNotFound
|
return nil, service.ErrAPIKeyNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) ResetAccountQuota(ctx context.Context, id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure stub implements interface.
|
// Ensure stub implements interface.
|
||||||
var _ service.AdminService = (*stubAdminService)(nil)
|
var _ service.AdminService = (*stubAdminService)(nil)
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
Extra: a.Extra,
|
Extra: a.Extra,
|
||||||
ProxyID: a.ProxyID,
|
ProxyID: a.ProxyID,
|
||||||
Concurrency: a.Concurrency,
|
Concurrency: a.Concurrency,
|
||||||
|
LoadFactor: a.LoadFactor,
|
||||||
Priority: a.Priority,
|
Priority: a.Priority,
|
||||||
RateMultiplier: a.BillingRateMultiplier(),
|
RateMultiplier: a.BillingRateMultiplier(),
|
||||||
Status: a.Status,
|
Status: a.Status,
|
||||||
@@ -248,6 +249,17 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取 API Key 账号配额限制(仅 apikey 类型有效)
|
||||||
|
if a.Type == service.AccountTypeAPIKey {
|
||||||
|
if limit := a.GetQuotaLimit(); limit > 0 {
|
||||||
|
out.QuotaLimit = &limit
|
||||||
|
}
|
||||||
|
used := a.GetQuotaUsed()
|
||||||
|
if out.QuotaLimit != nil {
|
||||||
|
out.QuotaUsed = &used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ type Account struct {
|
|||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
|
LoadFactor *int `json:"load_factor,omitempty"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
RateMultiplier float64 `json:"rate_multiplier"`
|
RateMultiplier float64 `json:"rate_multiplier"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -185,6 +186,10 @@ type Account struct {
|
|||||||
CacheTTLOverrideEnabled *bool `json:"cache_ttl_override_enabled,omitempty"`
|
CacheTTLOverrideEnabled *bool `json:"cache_ttl_override_enabled,omitempty"`
|
||||||
CacheTTLOverrideTarget *string `json:"cache_ttl_override_target,omitempty"`
|
CacheTTLOverrideTarget *string `json:"cache_ttl_override_target,omitempty"`
|
||||||
|
|
||||||
|
// API Key 账号配额限制
|
||||||
|
QuotaLimit *float64 `json:"quota_limit,omitempty"`
|
||||||
|
QuotaUsed *float64 `json:"quota_used,omitempty"`
|
||||||
|
|
||||||
Proxy *Proxy `json:"proxy,omitempty"`
|
Proxy *Proxy `json:"proxy,omitempty"`
|
||||||
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
AccountGroups []AccountGroup `json:"account_groups,omitempty"`
|
||||||
|
|
||||||
|
|||||||
@@ -2132,6 +2132,14 @@ func (r *stubAccountRepoForHandler) BulkUpdate(context.Context, []int64, service
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *stubAccountRepoForHandler) IncrementQuotaUsed(context.Context, int64, float64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *stubAccountRepoForHandler) ResetQuotaUsed(context.Context, int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Stub: SoraClient (用于 SoraGatewayService) ====================
|
// ==================== Stub: SoraClient (用于 SoraGatewayService) ====================
|
||||||
|
|
||||||
var _ service.SoraClient = (*stubSoraClientForHandler)(nil)
|
var _ service.SoraClient = (*stubSoraClientForHandler)(nil)
|
||||||
|
|||||||
@@ -216,6 +216,14 @@ func (r *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates s
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *stubAccountRepo) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *stubAccountRepo) listSchedulable() []service.Account {
|
func (r *stubAccountRepo) listSchedulable() []service.Account {
|
||||||
var result []service.Account
|
var result []service.Account
|
||||||
for _, acc := range r.accounts {
|
for _, acc := range r.accounts {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type Model struct {
|
|||||||
|
|
||||||
// DefaultModels OpenAI models list
|
// DefaultModels OpenAI models list
|
||||||
var DefaultModels = []Model{
|
var DefaultModels = []Model{
|
||||||
|
{ID: "gpt-5.4", Object: "model", Created: 1738368000, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.4"},
|
||||||
{ID: "gpt-5.3-codex", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex"},
|
{ID: "gpt-5.3-codex", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex"},
|
||||||
{ID: "gpt-5.3-codex-spark", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex Spark"},
|
{ID: "gpt-5.3-codex-spark", Object: "model", Created: 1735689600, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.3 Codex Spark"},
|
||||||
{ID: "gpt-5.2", Object: "model", Created: 1733875200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.2"},
|
{ID: "gpt-5.2", Object: "model", Created: 1733875200, OwnedBy: "openai", Type: "model", DisplayName: "GPT-5.2"},
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
|||||||
if account.RateMultiplier != nil {
|
if account.RateMultiplier != nil {
|
||||||
builder.SetRateMultiplier(*account.RateMultiplier)
|
builder.SetRateMultiplier(*account.RateMultiplier)
|
||||||
}
|
}
|
||||||
|
if account.LoadFactor != nil {
|
||||||
|
builder.SetLoadFactor(*account.LoadFactor)
|
||||||
|
}
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -318,6 +321,11 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
|||||||
if account.RateMultiplier != nil {
|
if account.RateMultiplier != nil {
|
||||||
builder.SetRateMultiplier(*account.RateMultiplier)
|
builder.SetRateMultiplier(*account.RateMultiplier)
|
||||||
}
|
}
|
||||||
|
if account.LoadFactor != nil {
|
||||||
|
builder.SetLoadFactor(*account.LoadFactor)
|
||||||
|
} else {
|
||||||
|
builder.ClearLoadFactor()
|
||||||
|
}
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -1223,6 +1231,15 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
|
|||||||
args = append(args, *updates.RateMultiplier)
|
args = append(args, *updates.RateMultiplier)
|
||||||
idx++
|
idx++
|
||||||
}
|
}
|
||||||
|
if updates.LoadFactor != nil {
|
||||||
|
if *updates.LoadFactor <= 0 {
|
||||||
|
setClauses = append(setClauses, "load_factor = NULL")
|
||||||
|
} else {
|
||||||
|
setClauses = append(setClauses, "load_factor = $"+itoa(idx))
|
||||||
|
args = append(args, *updates.LoadFactor)
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
if updates.Status != nil {
|
if updates.Status != nil {
|
||||||
setClauses = append(setClauses, "status = $"+itoa(idx))
|
setClauses = append(setClauses, "status = $"+itoa(idx))
|
||||||
args = append(args, *updates.Status)
|
args = append(args, *updates.Status)
|
||||||
@@ -1545,6 +1562,7 @@ func accountEntityToService(m *dbent.Account) *service.Account {
|
|||||||
Concurrency: m.Concurrency,
|
Concurrency: m.Concurrency,
|
||||||
Priority: m.Priority,
|
Priority: m.Priority,
|
||||||
RateMultiplier: &rateMultiplier,
|
RateMultiplier: &rateMultiplier,
|
||||||
|
LoadFactor: m.LoadFactor,
|
||||||
Status: m.Status,
|
Status: m.Status,
|
||||||
ErrorMessage: derefString(m.ErrorMessage),
|
ErrorMessage: derefString(m.ErrorMessage),
|
||||||
LastUsedAt: m.LastUsedAt,
|
LastUsedAt: m.LastUsedAt,
|
||||||
@@ -1657,3 +1675,60 @@ func (r *accountRepository) FindByExtraField(ctx context.Context, key string, va
|
|||||||
|
|
||||||
return r.accountsToService(ctx, accounts)
|
return r.accountsToService(ctx, accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IncrementQuotaUsed 原子递增账号的 extra.quota_used 字段
|
||||||
|
func (r *accountRepository) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
rows, err := r.sql.QueryContext(ctx,
|
||||||
|
`UPDATE accounts SET extra = jsonb_set(
|
||||||
|
COALESCE(extra, '{}'::jsonb),
|
||||||
|
'{quota_used}',
|
||||||
|
to_jsonb(COALESCE((extra->>'quota_used')::numeric, 0) + $1)
|
||||||
|
), updated_at = NOW()
|
||||||
|
WHERE id = $2 AND deleted_at IS NULL
|
||||||
|
RETURNING
|
||||||
|
COALESCE((extra->>'quota_used')::numeric, 0),
|
||||||
|
COALESCE((extra->>'quota_limit')::numeric, 0)`,
|
||||||
|
amount, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
var newUsed, limit float64
|
||||||
|
if rows.Next() {
|
||||||
|
if err := rows.Scan(&newUsed, &limit); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配额刚超限时触发调度快照刷新,使账号及时从调度候选中移除
|
||||||
|
if limit > 0 && newUsed >= limit && (newUsed-amount) < limit {
|
||||||
|
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
|
||||||
|
logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue quota exceeded failed: account=%d err=%v", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetQuotaUsed 重置账号的 extra.quota_used 为 0
|
||||||
|
func (r *accountRepository) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
_, err := r.sql.ExecContext(ctx,
|
||||||
|
`UPDATE accounts SET extra = jsonb_set(
|
||||||
|
COALESCE(extra, '{}'::jsonb),
|
||||||
|
'{quota_used}',
|
||||||
|
'0'::jsonb
|
||||||
|
), updated_at = NOW()
|
||||||
|
WHERE id = $1 AND deleted_at IS NULL`,
|
||||||
|
id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 重置配额后触发调度快照刷新,使账号重新参与调度
|
||||||
|
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &id, nil, nil); err != nil {
|
||||||
|
logger.LegacyPrintf("repository.account", "[SchedulerOutbox] enqueue quota reset failed: account=%d err=%v", id, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
)
|
)
|
||||||
@@ -95,7 +96,8 @@ func (s *claudeUsageService) FetchUsageWithOptions(ctx context.Context, opts *se
|
|||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
msg := fmt.Sprintf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||||
|
return nil, infraerrors.New(http.StatusInternalServerError, "UPSTREAM_ERROR", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
var usageResp service.ClaudeUsageResponse
|
var usageResp service.ClaudeUsageResponse
|
||||||
|
|||||||
@@ -1096,6 +1096,14 @@ func (s *stubAccountRepo) UpdateExtra(ctx context.Context, id int64, updates map
|
|||||||
return errors.New("not implemented")
|
return errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAccountRepo) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
return errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAccountRepo) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
return errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
|
func (s *stubAccountRepo) BulkUpdate(ctx context.Context, ids []int64, updates service.AccountBulkUpdate) (int64, error) {
|
||||||
s.bulkUpdateIDs = append([]int64{}, ids...)
|
s.bulkUpdateIDs = append([]int64{}, ids...)
|
||||||
return int64(len(ids)), nil
|
return int64(len(ids)), nil
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats)
|
accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats)
|
||||||
accounts.POST("/today-stats/batch", h.Admin.Account.GetBatchTodayStats)
|
accounts.POST("/today-stats/batch", h.Admin.Account.GetBatchTodayStats)
|
||||||
accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit)
|
accounts.POST("/:id/clear-rate-limit", h.Admin.Account.ClearRateLimit)
|
||||||
|
accounts.POST("/:id/reset-quota", h.Admin.Account.ResetQuota)
|
||||||
accounts.GET("/:id/temp-unschedulable", h.Admin.Account.GetTempUnschedulable)
|
accounts.GET("/:id/temp-unschedulable", h.Admin.Account.GetTempUnschedulable)
|
||||||
accounts.DELETE("/:id/temp-unschedulable", h.Admin.Account.ClearTempUnschedulable)
|
accounts.DELETE("/:id/temp-unschedulable", h.Admin.Account.ClearTempUnschedulable)
|
||||||
accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable)
|
accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type Account struct {
|
|||||||
// RateMultiplier 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。
|
// RateMultiplier 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。
|
||||||
// 使用指针用于兼容旧版本调度缓存(Redis)中缺字段的情况:nil 表示按 1.0 处理。
|
// 使用指针用于兼容旧版本调度缓存(Redis)中缺字段的情况:nil 表示按 1.0 处理。
|
||||||
RateMultiplier *float64
|
RateMultiplier *float64
|
||||||
|
LoadFactor *int // 调度负载因子;nil 表示使用 Concurrency
|
||||||
Status string
|
Status string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
LastUsedAt *time.Time
|
LastUsedAt *time.Time
|
||||||
@@ -88,6 +89,19 @@ func (a *Account) BillingRateMultiplier() float64 {
|
|||||||
return *a.RateMultiplier
|
return *a.RateMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Account) EffectiveLoadFactor() int {
|
||||||
|
if a == nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if a.LoadFactor != nil && *a.LoadFactor > 0 {
|
||||||
|
return *a.LoadFactor
|
||||||
|
}
|
||||||
|
if a.Concurrency > 0 {
|
||||||
|
return a.Concurrency
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Account) IsSchedulable() bool {
|
func (a *Account) IsSchedulable() bool {
|
||||||
if !a.IsActive() || !a.Schedulable {
|
if !a.IsActive() || !a.Schedulable {
|
||||||
return false
|
return false
|
||||||
@@ -1117,6 +1131,38 @@ func (a *Account) GetCacheTTLOverrideTarget() string {
|
|||||||
return "5m"
|
return "5m"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetQuotaLimit 获取 API Key 账号的配额限制(美元)
|
||||||
|
// 返回 0 表示未启用
|
||||||
|
func (a *Account) GetQuotaLimit() float64 {
|
||||||
|
if a.Extra == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v, ok := a.Extra["quota_limit"]; ok {
|
||||||
|
return parseExtraFloat64(v)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuotaUsed 获取 API Key 账号的已用配额(美元)
|
||||||
|
func (a *Account) GetQuotaUsed() float64 {
|
||||||
|
if a.Extra == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v, ok := a.Extra["quota_used"]; ok {
|
||||||
|
return parseExtraFloat64(v)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsQuotaExceeded 检查 API Key 账号配额是否已超限
|
||||||
|
func (a *Account) IsQuotaExceeded() bool {
|
||||||
|
limit := a.GetQuotaLimit()
|
||||||
|
if limit <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return a.GetQuotaUsed() >= limit
|
||||||
|
}
|
||||||
|
|
||||||
// GetWindowCostLimit 获取 5h 窗口费用阈值(美元)
|
// GetWindowCostLimit 获取 5h 窗口费用阈值(美元)
|
||||||
// 返回 0 表示未启用
|
// 返回 0 表示未启用
|
||||||
func (a *Account) GetWindowCostLimit() float64 {
|
func (a *Account) GetWindowCostLimit() float64 {
|
||||||
|
|||||||
46
backend/internal/service/account_load_factor_test.go
Normal file
46
backend/internal/service/account_load_factor_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
//go:build unit
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func intPtrHelper(v int) *int { return &v }
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_NilAccount(t *testing.T) {
|
||||||
|
var a *Account
|
||||||
|
require.Equal(t, 1, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_NilLoadFactor_PositiveConcurrency(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 5}
|
||||||
|
require.Equal(t, 5, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_NilLoadFactor_ZeroConcurrency(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 0}
|
||||||
|
require.Equal(t, 1, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_PositiveLoadFactor(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 5, LoadFactor: intPtrHelper(20)}
|
||||||
|
require.Equal(t, 20, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_ZeroLoadFactor_FallbackToConcurrency(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 5, LoadFactor: intPtrHelper(0)}
|
||||||
|
require.Equal(t, 5, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_NegativeLoadFactor_FallbackToConcurrency(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 3, LoadFactor: intPtrHelper(-1)}
|
||||||
|
require.Equal(t, 3, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveLoadFactor_ZeroLoadFactor_ZeroConcurrency(t *testing.T) {
|
||||||
|
a := &Account{Concurrency: 0, LoadFactor: intPtrHelper(0)}
|
||||||
|
require.Equal(t, 1, a.EffectiveLoadFactor())
|
||||||
|
}
|
||||||
@@ -68,6 +68,10 @@ type AccountRepository interface {
|
|||||||
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
|
UpdateSessionWindow(ctx context.Context, id int64, start, end *time.Time, status string) error
|
||||||
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
|
UpdateExtra(ctx context.Context, id int64, updates map[string]any) error
|
||||||
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
|
BulkUpdate(ctx context.Context, ids []int64, updates AccountBulkUpdate) (int64, error)
|
||||||
|
// IncrementQuotaUsed 原子递增 API Key 账号的配额用量
|
||||||
|
IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error
|
||||||
|
// ResetQuotaUsed 重置 API Key 账号的配额用量为 0
|
||||||
|
ResetQuotaUsed(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountBulkUpdate describes the fields that can be updated in a bulk operation.
|
// AccountBulkUpdate describes the fields that can be updated in a bulk operation.
|
||||||
@@ -78,6 +82,7 @@ type AccountBulkUpdate struct {
|
|||||||
Concurrency *int
|
Concurrency *int
|
||||||
Priority *int
|
Priority *int
|
||||||
RateMultiplier *float64
|
RateMultiplier *float64
|
||||||
|
LoadFactor *int
|
||||||
Status *string
|
Status *string
|
||||||
Schedulable *bool
|
Schedulable *bool
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
|
|||||||
@@ -199,6 +199,14 @@ func (s *accountRepoStub) BulkUpdate(ctx context.Context, ids []int64, updates A
|
|||||||
panic("unexpected BulkUpdate call")
|
panic("unexpected BulkUpdate call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *accountRepoStub) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *accountRepoStub) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TestAccountService_Delete_NotFound 测试删除不存在的账号时返回正确的错误。
|
// TestAccountService_Delete_NotFound 测试删除不存在的账号时返回正确的错误。
|
||||||
// 预期行为:
|
// 预期行为:
|
||||||
// - ExistsByID 返回 false(账号不存在)
|
// - ExistsByID 返回 false(账号不存在)
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ func (s *AccountTestService) TestAccountConnection(c *gin.Context, accountID int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if account.Platform == PlatformAntigravity {
|
if account.Platform == PlatformAntigravity {
|
||||||
return s.testAntigravityAccountConnection(c, account, modelID)
|
return s.routeAntigravityTest(c, account, modelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account.Platform == PlatformSora {
|
if account.Platform == PlatformSora {
|
||||||
@@ -1177,6 +1177,18 @@ func truncateSoraErrorBody(body []byte, max int) string {
|
|||||||
return soraerror.TruncateBody(body, max)
|
return soraerror.TruncateBody(body, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// routeAntigravityTest 路由 Antigravity 账号的测试请求。
|
||||||
|
// APIKey 类型走原生协议(与 gateway_handler 路由一致),OAuth/Upstream 走 CRS 中转。
|
||||||
|
func (s *AccountTestService) routeAntigravityTest(c *gin.Context, account *Account, modelID string) error {
|
||||||
|
if account.Type == AccountTypeAPIKey {
|
||||||
|
if strings.HasPrefix(modelID, "gemini-") {
|
||||||
|
return s.testGeminiAccountConnection(c, account, modelID)
|
||||||
|
}
|
||||||
|
return s.testClaudeAccountConnection(c, account, modelID)
|
||||||
|
}
|
||||||
|
return s.testAntigravityAccountConnection(c, account, modelID)
|
||||||
|
}
|
||||||
|
|
||||||
// testAntigravityAccountConnection tests an Antigravity account's connection
|
// testAntigravityAccountConnection tests an Antigravity account's connection
|
||||||
// 支持 Claude 和 Gemini 两种协议,使用非流式请求
|
// 支持 Claude 和 Gemini 两种协议,使用非流式请求
|
||||||
func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, account *Account, modelID string) error {
|
func (s *AccountTestService) testAntigravityAccountConnection(c *gin.Context, account *Account, modelID string) error {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type AdminService interface {
|
|||||||
DeleteRedeemCode(ctx context.Context, id int64) error
|
DeleteRedeemCode(ctx context.Context, id int64) error
|
||||||
BatchDeleteRedeemCodes(ctx context.Context, ids []int64) (int64, error)
|
BatchDeleteRedeemCodes(ctx context.Context, ids []int64) (int64, error)
|
||||||
ExpireRedeemCode(ctx context.Context, id int64) (*RedeemCode, error)
|
ExpireRedeemCode(ctx context.Context, id int64) (*RedeemCode, error)
|
||||||
|
ResetAccountQuota(ctx context.Context, id int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUserInput represents input for creating a new user via admin operations.
|
// CreateUserInput represents input for creating a new user via admin operations.
|
||||||
@@ -195,6 +196,7 @@ type CreateAccountInput struct {
|
|||||||
Concurrency int
|
Concurrency int
|
||||||
Priority int
|
Priority int
|
||||||
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
||||||
|
LoadFactor *int
|
||||||
GroupIDs []int64
|
GroupIDs []int64
|
||||||
ExpiresAt *int64
|
ExpiresAt *int64
|
||||||
AutoPauseOnExpired *bool
|
AutoPauseOnExpired *bool
|
||||||
@@ -215,6 +217,7 @@ type UpdateAccountInput struct {
|
|||||||
Concurrency *int // 使用指针区分"未提供"和"设置为0"
|
Concurrency *int // 使用指针区分"未提供"和"设置为0"
|
||||||
Priority *int // 使用指针区分"未提供"和"设置为0"
|
Priority *int // 使用指针区分"未提供"和"设置为0"
|
||||||
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
||||||
|
LoadFactor *int
|
||||||
Status string
|
Status string
|
||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
ExpiresAt *int64
|
ExpiresAt *int64
|
||||||
@@ -230,6 +233,7 @@ type BulkUpdateAccountsInput struct {
|
|||||||
Concurrency *int
|
Concurrency *int
|
||||||
Priority *int
|
Priority *int
|
||||||
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
|
||||||
|
LoadFactor *int
|
||||||
Status string
|
Status string
|
||||||
Schedulable *bool
|
Schedulable *bool
|
||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
@@ -1413,6 +1417,12 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
|||||||
}
|
}
|
||||||
account.RateMultiplier = input.RateMultiplier
|
account.RateMultiplier = input.RateMultiplier
|
||||||
}
|
}
|
||||||
|
if input.LoadFactor != nil && *input.LoadFactor > 0 {
|
||||||
|
if *input.LoadFactor > 10000 {
|
||||||
|
return nil, errors.New("load_factor must be <= 10000")
|
||||||
|
}
|
||||||
|
account.LoadFactor = input.LoadFactor
|
||||||
|
}
|
||||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1458,6 +1468,10 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
account.Credentials = input.Credentials
|
account.Credentials = input.Credentials
|
||||||
}
|
}
|
||||||
if len(input.Extra) > 0 {
|
if len(input.Extra) > 0 {
|
||||||
|
// 保留 quota_used,防止编辑账号时意外重置配额用量
|
||||||
|
if oldQuotaUsed, ok := account.Extra["quota_used"]; ok {
|
||||||
|
input.Extra["quota_used"] = oldQuotaUsed
|
||||||
|
}
|
||||||
account.Extra = input.Extra
|
account.Extra = input.Extra
|
||||||
}
|
}
|
||||||
if input.ProxyID != nil {
|
if input.ProxyID != nil {
|
||||||
@@ -1483,6 +1497,15 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
}
|
}
|
||||||
account.RateMultiplier = input.RateMultiplier
|
account.RateMultiplier = input.RateMultiplier
|
||||||
}
|
}
|
||||||
|
if input.LoadFactor != nil {
|
||||||
|
if *input.LoadFactor <= 0 {
|
||||||
|
account.LoadFactor = nil // 0 或负数表示清除
|
||||||
|
} else if *input.LoadFactor > 10000 {
|
||||||
|
return nil, errors.New("load_factor must be <= 10000")
|
||||||
|
} else {
|
||||||
|
account.LoadFactor = input.LoadFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
account.Status = input.Status
|
account.Status = input.Status
|
||||||
}
|
}
|
||||||
@@ -1616,6 +1639,15 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
|
|||||||
if input.RateMultiplier != nil {
|
if input.RateMultiplier != nil {
|
||||||
repoUpdates.RateMultiplier = input.RateMultiplier
|
repoUpdates.RateMultiplier = input.RateMultiplier
|
||||||
}
|
}
|
||||||
|
if input.LoadFactor != nil {
|
||||||
|
if *input.LoadFactor <= 0 {
|
||||||
|
repoUpdates.LoadFactor = nil // 0 或负数表示清除
|
||||||
|
} else if *input.LoadFactor > 10000 {
|
||||||
|
return nil, errors.New("load_factor must be <= 10000")
|
||||||
|
} else {
|
||||||
|
repoUpdates.LoadFactor = input.LoadFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
repoUpdates.Status = &input.Status
|
repoUpdates.Status = &input.Status
|
||||||
}
|
}
|
||||||
@@ -2439,3 +2471,7 @@ func (e *MixedChannelError) Error() string {
|
|||||||
return fmt.Sprintf("mixed_channel_warning: Group '%s' contains both %s and %s accounts. Using mixed channels in the same context may cause thinking block signature validation issues, which will fallback to non-thinking mode for historical messages.",
|
return fmt.Sprintf("mixed_channel_warning: Group '%s' contains both %s and %s accounts. Using mixed channels in the same context may cause thinking block signature validation issues, which will fallback to non-thinking mode for historical messages.",
|
||||||
e.GroupName, e.CurrentPlatform, e.OtherPlatform)
|
e.GroupName, e.CurrentPlatform, e.OtherPlatform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *adminServiceImpl) ResetAccountQuota(ctx context.Context, id int64) error {
|
||||||
|
return s.accountRepo.ResetQuotaUsed(ctx, id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,15 +43,24 @@ type BillingCache interface {
|
|||||||
|
|
||||||
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
// ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
|
||||||
type ModelPricing struct {
|
type ModelPricing struct {
|
||||||
InputPricePerToken float64 // 每token输入价格 (USD)
|
InputPricePerToken float64 // 每token输入价格 (USD)
|
||||||
OutputPricePerToken float64 // 每token输出价格 (USD)
|
OutputPricePerToken float64 // 每token输出价格 (USD)
|
||||||
CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD)
|
CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD)
|
||||||
CacheReadPricePerToken float64 // 缓存读取每token价格 (USD)
|
CacheReadPricePerToken float64 // 缓存读取每token价格 (USD)
|
||||||
CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD)
|
CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD)
|
||||||
CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD)
|
CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD)
|
||||||
SupportsCacheBreakdown bool // 是否支持详细的缓存分类
|
SupportsCacheBreakdown bool // 是否支持详细的缓存分类
|
||||||
|
LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格
|
||||||
|
LongContextInputMultiplier float64 // 长上下文整次会话输入倍率
|
||||||
|
LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
openAIGPT54LongContextInputThreshold = 272000
|
||||||
|
openAIGPT54LongContextInputMultiplier = 2.0
|
||||||
|
openAIGPT54LongContextOutputMultiplier = 1.5
|
||||||
|
)
|
||||||
|
|
||||||
// UsageTokens 使用的token数量
|
// UsageTokens 使用的token数量
|
||||||
type UsageTokens struct {
|
type UsageTokens struct {
|
||||||
InputTokens int
|
InputTokens int
|
||||||
@@ -161,6 +170,35 @@ func (s *BillingService) initFallbackPricing() {
|
|||||||
CacheReadPricePerToken: 0.2e-6, // $0.20 per MTok
|
CacheReadPricePerToken: 0.2e-6, // $0.20 per MTok
|
||||||
SupportsCacheBreakdown: false,
|
SupportsCacheBreakdown: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenAI GPT-5.1(本地兜底,防止动态定价不可用时拒绝计费)
|
||||||
|
s.fallbackPrices["gpt-5.1"] = &ModelPricing{
|
||||||
|
InputPricePerToken: 1.25e-6, // $1.25 per MTok
|
||||||
|
OutputPricePerToken: 10e-6, // $10 per MTok
|
||||||
|
CacheCreationPricePerToken: 1.25e-6, // $1.25 per MTok
|
||||||
|
CacheReadPricePerToken: 0.125e-6,
|
||||||
|
SupportsCacheBreakdown: false,
|
||||||
|
}
|
||||||
|
// OpenAI GPT-5.4(业务指定价格)
|
||||||
|
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
|
||||||
|
InputPricePerToken: 2.5e-6, // $2.5 per MTok
|
||||||
|
OutputPricePerToken: 15e-6, // $15 per MTok
|
||||||
|
CacheCreationPricePerToken: 2.5e-6, // $2.5 per MTok
|
||||||
|
CacheReadPricePerToken: 0.25e-6, // $0.25 per MTok
|
||||||
|
SupportsCacheBreakdown: false,
|
||||||
|
LongContextInputThreshold: openAIGPT54LongContextInputThreshold,
|
||||||
|
LongContextInputMultiplier: openAIGPT54LongContextInputMultiplier,
|
||||||
|
LongContextOutputMultiplier: openAIGPT54LongContextOutputMultiplier,
|
||||||
|
}
|
||||||
|
// Codex 族兜底统一按 GPT-5.1 Codex 价格计费
|
||||||
|
s.fallbackPrices["gpt-5.1-codex"] = &ModelPricing{
|
||||||
|
InputPricePerToken: 1.5e-6, // $1.5 per MTok
|
||||||
|
OutputPricePerToken: 12e-6, // $12 per MTok
|
||||||
|
CacheCreationPricePerToken: 1.5e-6, // $1.5 per MTok
|
||||||
|
CacheReadPricePerToken: 0.15e-6,
|
||||||
|
SupportsCacheBreakdown: false,
|
||||||
|
}
|
||||||
|
s.fallbackPrices["gpt-5.3-codex"] = s.fallbackPrices["gpt-5.1-codex"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFallbackPricing 根据模型系列获取回退价格
|
// getFallbackPricing 根据模型系列获取回退价格
|
||||||
@@ -189,12 +227,30 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
|
|||||||
}
|
}
|
||||||
return s.fallbackPrices["claude-3-haiku"]
|
return s.fallbackPrices["claude-3-haiku"]
|
||||||
}
|
}
|
||||||
|
// Claude 未知型号统一回退到 Sonnet,避免计费中断。
|
||||||
|
if strings.Contains(modelLower, "claude") {
|
||||||
|
return s.fallbackPrices["claude-sonnet-4"]
|
||||||
|
}
|
||||||
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
|
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
|
||||||
return s.fallbackPrices["gemini-3.1-pro"]
|
return s.fallbackPrices["gemini-3.1-pro"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认使用Sonnet价格
|
// OpenAI 仅匹配已知 GPT-5/Codex 族,避免未知 OpenAI 型号误计价。
|
||||||
return s.fallbackPrices["claude-sonnet-4"]
|
if strings.Contains(modelLower, "gpt-5") || strings.Contains(modelLower, "codex") {
|
||||||
|
normalized := normalizeCodexModel(modelLower)
|
||||||
|
switch normalized {
|
||||||
|
case "gpt-5.4":
|
||||||
|
return s.fallbackPrices["gpt-5.4"]
|
||||||
|
case "gpt-5.3-codex":
|
||||||
|
return s.fallbackPrices["gpt-5.3-codex"]
|
||||||
|
case "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", "codex-mini-latest":
|
||||||
|
return s.fallbackPrices["gpt-5.1-codex"]
|
||||||
|
case "gpt-5.1":
|
||||||
|
return s.fallbackPrices["gpt-5.1"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetModelPricing 获取模型价格配置
|
// GetModelPricing 获取模型价格配置
|
||||||
@@ -212,15 +268,18 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) {
|
|||||||
price5m := litellmPricing.CacheCreationInputTokenCost
|
price5m := litellmPricing.CacheCreationInputTokenCost
|
||||||
price1h := litellmPricing.CacheCreationInputTokenCostAbove1hr
|
price1h := litellmPricing.CacheCreationInputTokenCostAbove1hr
|
||||||
enableBreakdown := price1h > 0 && price1h > price5m
|
enableBreakdown := price1h > 0 && price1h > price5m
|
||||||
return &ModelPricing{
|
return s.applyModelSpecificPricingPolicy(model, &ModelPricing{
|
||||||
InputPricePerToken: litellmPricing.InputCostPerToken,
|
InputPricePerToken: litellmPricing.InputCostPerToken,
|
||||||
OutputPricePerToken: litellmPricing.OutputCostPerToken,
|
OutputPricePerToken: litellmPricing.OutputCostPerToken,
|
||||||
CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
|
CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
|
||||||
CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
|
CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
|
||||||
CacheCreation5mPrice: price5m,
|
CacheCreation5mPrice: price5m,
|
||||||
CacheCreation1hPrice: price1h,
|
CacheCreation1hPrice: price1h,
|
||||||
SupportsCacheBreakdown: enableBreakdown,
|
SupportsCacheBreakdown: enableBreakdown,
|
||||||
}, nil
|
LongContextInputThreshold: litellmPricing.LongContextInputTokenThreshold,
|
||||||
|
LongContextInputMultiplier: litellmPricing.LongContextInputCostMultiplier,
|
||||||
|
LongContextOutputMultiplier: litellmPricing.LongContextOutputCostMultiplier,
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +287,7 @@ func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) {
|
|||||||
fallback := s.getFallbackPricing(model)
|
fallback := s.getFallbackPricing(model)
|
||||||
if fallback != nil {
|
if fallback != nil {
|
||||||
log.Printf("[Billing] Using fallback pricing for model: %s", model)
|
log.Printf("[Billing] Using fallback pricing for model: %s", model)
|
||||||
return fallback, nil
|
return s.applyModelSpecificPricingPolicy(model, fallback), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("pricing not found for model: %s", model)
|
return nil, fmt.Errorf("pricing not found for model: %s", model)
|
||||||
@@ -242,12 +301,18 @@ func (s *BillingService) CalculateCost(model string, tokens UsageTokens, rateMul
|
|||||||
}
|
}
|
||||||
|
|
||||||
breakdown := &CostBreakdown{}
|
breakdown := &CostBreakdown{}
|
||||||
|
inputPricePerToken := pricing.InputPricePerToken
|
||||||
|
outputPricePerToken := pricing.OutputPricePerToken
|
||||||
|
if s.shouldApplySessionLongContextPricing(tokens, pricing) {
|
||||||
|
inputPricePerToken *= pricing.LongContextInputMultiplier
|
||||||
|
outputPricePerToken *= pricing.LongContextOutputMultiplier
|
||||||
|
}
|
||||||
|
|
||||||
// 计算输入token费用(使用per-token价格)
|
// 计算输入token费用(使用per-token价格)
|
||||||
breakdown.InputCost = float64(tokens.InputTokens) * pricing.InputPricePerToken
|
breakdown.InputCost = float64(tokens.InputTokens) * inputPricePerToken
|
||||||
|
|
||||||
// 计算输出token费用
|
// 计算输出token费用
|
||||||
breakdown.OutputCost = float64(tokens.OutputTokens) * pricing.OutputPricePerToken
|
breakdown.OutputCost = float64(tokens.OutputTokens) * outputPricePerToken
|
||||||
|
|
||||||
// 计算缓存费用
|
// 计算缓存费用
|
||||||
if pricing.SupportsCacheBreakdown && (pricing.CacheCreation5mPrice > 0 || pricing.CacheCreation1hPrice > 0) {
|
if pricing.SupportsCacheBreakdown && (pricing.CacheCreation5mPrice > 0 || pricing.CacheCreation1hPrice > 0) {
|
||||||
@@ -279,6 +344,45 @@ func (s *BillingService) CalculateCost(model string, tokens UsageTokens, rateMul
|
|||||||
return breakdown, nil
|
return breakdown, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *ModelPricing) *ModelPricing {
|
||||||
|
if pricing == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !isOpenAIGPT54Model(model) {
|
||||||
|
return pricing
|
||||||
|
}
|
||||||
|
if pricing.LongContextInputThreshold > 0 && pricing.LongContextInputMultiplier > 0 && pricing.LongContextOutputMultiplier > 0 {
|
||||||
|
return pricing
|
||||||
|
}
|
||||||
|
cloned := *pricing
|
||||||
|
if cloned.LongContextInputThreshold <= 0 {
|
||||||
|
cloned.LongContextInputThreshold = openAIGPT54LongContextInputThreshold
|
||||||
|
}
|
||||||
|
if cloned.LongContextInputMultiplier <= 0 {
|
||||||
|
cloned.LongContextInputMultiplier = openAIGPT54LongContextInputMultiplier
|
||||||
|
}
|
||||||
|
if cloned.LongContextOutputMultiplier <= 0 {
|
||||||
|
cloned.LongContextOutputMultiplier = openAIGPT54LongContextOutputMultiplier
|
||||||
|
}
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BillingService) shouldApplySessionLongContextPricing(tokens UsageTokens, pricing *ModelPricing) bool {
|
||||||
|
if pricing == nil || pricing.LongContextInputThreshold <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if pricing.LongContextInputMultiplier <= 1 && pricing.LongContextOutputMultiplier <= 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
totalInputTokens := tokens.InputTokens + tokens.CacheReadTokens
|
||||||
|
return totalInputTokens > pricing.LongContextInputThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOpenAIGPT54Model(model string) bool {
|
||||||
|
normalized := normalizeCodexModel(strings.TrimSpace(strings.ToLower(model)))
|
||||||
|
return normalized == "gpt-5.4"
|
||||||
|
}
|
||||||
|
|
||||||
// CalculateCostWithConfig 使用配置中的默认倍率计算费用
|
// CalculateCostWithConfig 使用配置中的默认倍率计算费用
|
||||||
func (s *BillingService) CalculateCostWithConfig(model string, tokens UsageTokens) (*CostBreakdown, error) {
|
func (s *BillingService) CalculateCostWithConfig(model string, tokens UsageTokens) (*CostBreakdown, error) {
|
||||||
multiplier := s.cfg.Default.RateMultiplier
|
multiplier := s.cfg.Default.RateMultiplier
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func TestGetModelPricing_CaseInsensitive(t *testing.T) {
|
|||||||
require.Equal(t, p1.InputPricePerToken, p2.InputPricePerToken)
|
require.Equal(t, p1.InputPricePerToken, p2.InputPricePerToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetModelPricing_UnknownModelFallsBackToSonnet(t *testing.T) {
|
func TestGetModelPricing_UnknownClaudeModelFallsBackToSonnet(t *testing.T) {
|
||||||
svc := newTestBillingService()
|
svc := newTestBillingService()
|
||||||
|
|
||||||
// 不包含 opus/sonnet/haiku 关键词的 Claude 模型会走默认 Sonnet 价格
|
// 不包含 opus/sonnet/haiku 关键词的 Claude 模型会走默认 Sonnet 价格
|
||||||
@@ -142,6 +142,93 @@ func TestGetModelPricing_UnknownModelFallsBackToSonnet(t *testing.T) {
|
|||||||
require.InDelta(t, 3e-6, pricing.InputPricePerToken, 1e-12)
|
require.InDelta(t, 3e-6, pricing.InputPricePerToken, 1e-12)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetModelPricing_UnknownOpenAIModelReturnsError(t *testing.T) {
|
||||||
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
pricing, err := svc.GetModelPricing("gpt-unknown-model")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, pricing)
|
||||||
|
require.Contains(t, err.Error(), "pricing not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelPricing_OpenAIGPT51Fallback(t *testing.T) {
|
||||||
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
pricing, err := svc.GetModelPricing("gpt-5.1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, pricing)
|
||||||
|
require.InDelta(t, 1.25e-6, pricing.InputPricePerToken, 1e-12)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetModelPricing_OpenAIGPT54Fallback(t *testing.T) {
|
||||||
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
pricing, err := svc.GetModelPricing("gpt-5.4")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, pricing)
|
||||||
|
require.InDelta(t, 2.5e-6, pricing.InputPricePerToken, 1e-12)
|
||||||
|
require.InDelta(t, 15e-6, pricing.OutputPricePerToken, 1e-12)
|
||||||
|
require.InDelta(t, 0.25e-6, pricing.CacheReadPricePerToken, 1e-12)
|
||||||
|
require.Equal(t, 272000, pricing.LongContextInputThreshold)
|
||||||
|
require.InDelta(t, 2.0, pricing.LongContextInputMultiplier, 1e-12)
|
||||||
|
require.InDelta(t, 1.5, pricing.LongContextOutputMultiplier, 1e-12)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalculateCost_OpenAIGPT54LongContextAppliesWholeSessionMultipliers(t *testing.T) {
|
||||||
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
tokens := UsageTokens{
|
||||||
|
InputTokens: 300000,
|
||||||
|
OutputTokens: 4000,
|
||||||
|
}
|
||||||
|
|
||||||
|
cost, err := svc.CalculateCost("gpt-5.4-2026-03-05", tokens, 1.0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedInput := float64(tokens.InputTokens) * 2.5e-6 * 2.0
|
||||||
|
expectedOutput := float64(tokens.OutputTokens) * 15e-6 * 1.5
|
||||||
|
require.InDelta(t, expectedInput, cost.InputCost, 1e-10)
|
||||||
|
require.InDelta(t, expectedOutput, cost.OutputCost, 1e-10)
|
||||||
|
require.InDelta(t, expectedInput+expectedOutput, cost.TotalCost, 1e-10)
|
||||||
|
require.InDelta(t, expectedInput+expectedOutput, cost.ActualCost, 1e-10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFallbackPricing_FamilyMatching(t *testing.T) {
|
||||||
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
model string
|
||||||
|
expectedInput float64
|
||||||
|
expectNilPricing bool
|
||||||
|
}{
|
||||||
|
{name: "empty model", model: " ", expectNilPricing: true},
|
||||||
|
{name: "claude opus 4.6", model: "claude-opus-4.6-20260201", expectedInput: 5e-6},
|
||||||
|
{name: "claude opus 4.5 alt separator", model: "claude-opus-4-5-20260101", expectedInput: 5e-6},
|
||||||
|
{name: "claude generic model fallback sonnet", model: "claude-foo-bar", expectedInput: 3e-6},
|
||||||
|
{name: "gemini explicit fallback", model: "gemini-3-1-pro", expectedInput: 2e-6},
|
||||||
|
{name: "gemini unknown no fallback", model: "gemini-2.0-pro", expectNilPricing: true},
|
||||||
|
{name: "openai gpt5.1", model: "gpt-5.1", expectedInput: 1.25e-6},
|
||||||
|
{name: "openai gpt5.4", model: "gpt-5.4", expectedInput: 2.5e-6},
|
||||||
|
{name: "openai gpt5.3 codex", model: "gpt-5.3-codex", expectedInput: 1.5e-6},
|
||||||
|
{name: "openai gpt5.1 codex max alias", model: "gpt-5.1-codex-max", expectedInput: 1.5e-6},
|
||||||
|
{name: "openai codex mini latest alias", model: "codex-mini-latest", expectedInput: 1.5e-6},
|
||||||
|
{name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true},
|
||||||
|
{name: "non supported family", model: "qwen-max", expectNilPricing: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pricing := svc.getFallbackPricing(tt.model)
|
||||||
|
if tt.expectNilPricing {
|
||||||
|
require.Nil(t, pricing)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NotNil(t, pricing)
|
||||||
|
require.InDelta(t, tt.expectedInput, pricing.InputPricePerToken, 1e-12)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
func TestCalculateCostWithLongContext_BelowThreshold(t *testing.T) {
|
func TestCalculateCostWithLongContext_BelowThreshold(t *testing.T) {
|
||||||
svc := newTestBillingService()
|
svc := newTestBillingService()
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,14 @@ func (m *mockAccountRepoForPlatform) BulkUpdate(ctx context.Context, ids []int64
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAccountRepoForPlatform) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAccountRepoForPlatform) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Verify interface implementation
|
// Verify interface implementation
|
||||||
var _ AccountRepository = (*mockAccountRepoForPlatform)(nil)
|
var _ AccountRepository = (*mockAccountRepoForPlatform)(nil)
|
||||||
|
|
||||||
|
|||||||
@@ -1228,6 +1228,10 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
modelScopeSkippedIDs = append(modelScopeSkippedIDs, account.ID)
|
modelScopeSkippedIDs = append(modelScopeSkippedIDs, account.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 配额检查
|
||||||
|
if !s.isAccountSchedulableForQuota(account) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// 窗口费用检查(非粘性会话路径)
|
// 窗口费用检查(非粘性会话路径)
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, account, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, account, false) {
|
||||||
filteredWindowCost++
|
filteredWindowCost++
|
||||||
@@ -1260,6 +1264,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
s.isAccountAllowedForPlatform(stickyAccount, platform, useMixed) &&
|
s.isAccountAllowedForPlatform(stickyAccount, platform, useMixed) &&
|
||||||
(requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, stickyAccount, requestedModel)) &&
|
(requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, stickyAccount, requestedModel)) &&
|
||||||
s.isAccountSchedulableForModelSelection(ctx, stickyAccount, requestedModel) &&
|
s.isAccountSchedulableForModelSelection(ctx, stickyAccount, requestedModel) &&
|
||||||
|
s.isAccountSchedulableForQuota(stickyAccount) &&
|
||||||
s.isAccountSchedulableForWindowCost(ctx, stickyAccount, true) &&
|
s.isAccountSchedulableForWindowCost(ctx, stickyAccount, true) &&
|
||||||
|
|
||||||
s.isAccountSchedulableForRPM(ctx, stickyAccount, true) { // 粘性会话窗口费用+RPM 检查
|
s.isAccountSchedulableForRPM(ctx, stickyAccount, true) { // 粘性会话窗口费用+RPM 检查
|
||||||
@@ -1311,7 +1316,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
for _, acc := range routingCandidates {
|
for _, acc := range routingCandidates {
|
||||||
routingLoads = append(routingLoads, AccountWithConcurrency{
|
routingLoads = append(routingLoads, AccountWithConcurrency{
|
||||||
ID: acc.ID,
|
ID: acc.ID,
|
||||||
MaxConcurrency: acc.Concurrency,
|
MaxConcurrency: acc.EffectiveLoadFactor(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
routingLoadMap, _ := s.concurrencyService.GetAccountsLoadBatch(ctx, routingLoads)
|
routingLoadMap, _ := s.concurrencyService.GetAccountsLoadBatch(ctx, routingLoads)
|
||||||
@@ -1416,6 +1421,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
s.isAccountAllowedForPlatform(account, platform, useMixed) &&
|
s.isAccountAllowedForPlatform(account, platform, useMixed) &&
|
||||||
(requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) &&
|
(requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) &&
|
||||||
s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) &&
|
s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) &&
|
||||||
|
s.isAccountSchedulableForQuota(account) &&
|
||||||
s.isAccountSchedulableForWindowCost(ctx, account, true) &&
|
s.isAccountSchedulableForWindowCost(ctx, account, true) &&
|
||||||
|
|
||||||
s.isAccountSchedulableForRPM(ctx, account, true) { // 粘性会话窗口费用+RPM 检查
|
s.isAccountSchedulableForRPM(ctx, account, true) { // 粘性会话窗口费用+RPM 检查
|
||||||
@@ -1480,6 +1486,10 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// 配额检查
|
||||||
|
if !s.isAccountSchedulableForQuota(acc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// 窗口费用检查(非粘性会话路径)
|
// 窗口费用检查(非粘性会话路径)
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
||||||
continue
|
continue
|
||||||
@@ -1499,7 +1509,7 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro
|
|||||||
for _, acc := range candidates {
|
for _, acc := range candidates {
|
||||||
accountLoads = append(accountLoads, AccountWithConcurrency{
|
accountLoads = append(accountLoads, AccountWithConcurrency{
|
||||||
ID: acc.ID,
|
ID: acc.ID,
|
||||||
MaxConcurrency: acc.Concurrency,
|
MaxConcurrency: acc.EffectiveLoadFactor(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2113,6 +2123,15 @@ func (s *GatewayService) withWindowCostPrefetch(ctx context.Context, accounts []
|
|||||||
return context.WithValue(ctx, windowCostPrefetchContextKey, costs)
|
return context.WithValue(ctx, windowCostPrefetchContextKey, costs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isAccountSchedulableForQuota 检查 API Key 账号是否在配额限制内
|
||||||
|
// 仅适用于配置了 quota_limit 的 apikey 类型账号
|
||||||
|
func (s *GatewayService) isAccountSchedulableForQuota(account *Account) bool {
|
||||||
|
if account.Type != AccountTypeAPIKey {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !account.IsQuotaExceeded()
|
||||||
|
}
|
||||||
|
|
||||||
// isAccountSchedulableForWindowCost 检查账号是否可根据窗口费用进行调度
|
// isAccountSchedulableForWindowCost 检查账号是否可根据窗口费用进行调度
|
||||||
// 仅适用于 Anthropic OAuth/SetupToken 账号
|
// 仅适用于 Anthropic OAuth/SetupToken 账号
|
||||||
// 返回 true 表示可调度,false 表示不可调度
|
// 返回 true 表示可调度,false 表示不可调度
|
||||||
@@ -2590,7 +2609,7 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
|||||||
if clearSticky {
|
if clearSticky {
|
||||||
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
||||||
}
|
}
|
||||||
if !clearSticky && s.isAccountInGroup(account, groupID) && account.Platform == platform && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
if !clearSticky && s.isAccountInGroup(account, groupID) && account.Platform == platform && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
||||||
if s.debugModelRoutingEnabled() {
|
if s.debugModelRoutingEnabled() {
|
||||||
logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID)
|
logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID)
|
||||||
}
|
}
|
||||||
@@ -2644,6 +2663,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
|||||||
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !s.isAccountSchedulableForQuota(acc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2700,7 +2722,7 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
|||||||
if clearSticky {
|
if clearSticky {
|
||||||
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
||||||
}
|
}
|
||||||
if !clearSticky && s.isAccountInGroup(account, groupID) && account.Platform == platform && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
if !clearSticky && s.isAccountInGroup(account, groupID) && account.Platform == platform && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2743,6 +2765,9 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context,
|
|||||||
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !s.isAccountSchedulableForQuota(acc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2818,7 +2843,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
|||||||
if clearSticky {
|
if clearSticky {
|
||||||
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
||||||
}
|
}
|
||||||
if !clearSticky && s.isAccountInGroup(account, groupID) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
if !clearSticky && s.isAccountInGroup(account, groupID) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
||||||
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
||||||
if s.debugModelRoutingEnabled() {
|
if s.debugModelRoutingEnabled() {
|
||||||
logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy mixed routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID)
|
logger.LegacyPrintf("service.gateway", "[ModelRoutingDebug] legacy mixed routed sticky hit: group_id=%v model=%s session=%s account=%d", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), accountID)
|
||||||
@@ -2874,6 +2899,9 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
|||||||
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !s.isAccountSchedulableForQuota(acc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2930,7 +2958,7 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
|||||||
if clearSticky {
|
if clearSticky {
|
||||||
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
_ = s.cache.DeleteSessionAccountID(ctx, derefGroupID(groupID), sessionHash)
|
||||||
}
|
}
|
||||||
if !clearSticky && s.isAccountInGroup(account, groupID) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
if !clearSticky && s.isAccountInGroup(account, groupID) && (requestedModel == "" || s.isModelSupportedByAccountWithContext(ctx, account, requestedModel)) && s.isAccountSchedulableForModelSelection(ctx, account, requestedModel) && s.isAccountSchedulableForQuota(account) && s.isAccountSchedulableForWindowCost(ctx, account, true) && s.isAccountSchedulableForRPM(ctx, account, true) {
|
||||||
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
if account.Platform == nativePlatform || (account.Platform == PlatformAntigravity && account.IsMixedSchedulingEnabled()) {
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
@@ -2975,6 +3003,9 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g
|
|||||||
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
if !s.isAccountSchedulableForModelSelection(ctx, acc, requestedModel) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !s.isAccountSchedulableForQuota(acc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
if !s.isAccountSchedulableForWindowCost(ctx, acc, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -6379,6 +6410,89 @@ type APIKeyQuotaUpdater interface {
|
|||||||
UpdateRateLimitUsage(ctx context.Context, apiKeyID int64, cost float64) error
|
UpdateRateLimitUsage(ctx context.Context, apiKeyID int64, cost float64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// postUsageBillingParams 统一扣费所需的参数
|
||||||
|
type postUsageBillingParams struct {
|
||||||
|
Cost *CostBreakdown
|
||||||
|
User *User
|
||||||
|
APIKey *APIKey
|
||||||
|
Account *Account
|
||||||
|
Subscription *UserSubscription
|
||||||
|
IsSubscriptionBill bool
|
||||||
|
AccountRateMultiplier float64
|
||||||
|
APIKeyService APIKeyQuotaUpdater
|
||||||
|
}
|
||||||
|
|
||||||
|
// postUsageBilling 统一处理使用量记录后的扣费逻辑:
|
||||||
|
// - 订阅/余额扣费
|
||||||
|
// - API Key 配额更新
|
||||||
|
// - API Key 限速用量更新
|
||||||
|
// - 账号配额用量更新(账号口径:TotalCost × 账号计费倍率)
|
||||||
|
func postUsageBilling(ctx context.Context, p *postUsageBillingParams, deps *billingDeps) {
|
||||||
|
cost := p.Cost
|
||||||
|
|
||||||
|
// 1. 订阅 / 余额扣费
|
||||||
|
if p.IsSubscriptionBill {
|
||||||
|
if cost.TotalCost > 0 {
|
||||||
|
if err := deps.userSubRepo.IncrementUsage(ctx, p.Subscription.ID, cost.TotalCost); err != nil {
|
||||||
|
slog.Error("increment subscription usage failed", "subscription_id", p.Subscription.ID, "error", err)
|
||||||
|
}
|
||||||
|
deps.billingCacheService.QueueUpdateSubscriptionUsage(p.User.ID, *p.APIKey.GroupID, cost.TotalCost)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if cost.ActualCost > 0 {
|
||||||
|
if err := deps.userRepo.DeductBalance(ctx, p.User.ID, cost.ActualCost); err != nil {
|
||||||
|
slog.Error("deduct balance failed", "user_id", p.User.ID, "error", err)
|
||||||
|
}
|
||||||
|
deps.billingCacheService.QueueDeductBalance(p.User.ID, cost.ActualCost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. API Key 配额
|
||||||
|
if cost.ActualCost > 0 && p.APIKey.Quota > 0 && p.APIKeyService != nil {
|
||||||
|
if err := p.APIKeyService.UpdateQuotaUsed(ctx, p.APIKey.ID, cost.ActualCost); err != nil {
|
||||||
|
slog.Error("update api key quota failed", "api_key_id", p.APIKey.ID, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. API Key 限速用量
|
||||||
|
if cost.ActualCost > 0 && p.APIKey.HasRateLimits() && p.APIKeyService != nil {
|
||||||
|
if err := p.APIKeyService.UpdateRateLimitUsage(ctx, p.APIKey.ID, cost.ActualCost); err != nil {
|
||||||
|
slog.Error("update api key rate limit usage failed", "api_key_id", p.APIKey.ID, "error", err)
|
||||||
|
}
|
||||||
|
deps.billingCacheService.QueueUpdateAPIKeyRateLimitUsage(p.APIKey.ID, cost.ActualCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 账号配额用量(账号口径:TotalCost × 账号计费倍率)
|
||||||
|
if cost.TotalCost > 0 && p.Account.Type == AccountTypeAPIKey && p.Account.GetQuotaLimit() > 0 {
|
||||||
|
accountCost := cost.TotalCost * p.AccountRateMultiplier
|
||||||
|
if err := deps.accountRepo.IncrementQuotaUsed(ctx, p.Account.ID, accountCost); err != nil {
|
||||||
|
slog.Error("increment account quota used failed", "account_id", p.Account.ID, "cost", accountCost, "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 更新账号最近使用时间
|
||||||
|
deps.deferredService.ScheduleLastUsedUpdate(p.Account.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// billingDeps 扣费逻辑依赖的服务(由各 gateway service 提供)
|
||||||
|
type billingDeps struct {
|
||||||
|
accountRepo AccountRepository
|
||||||
|
userRepo UserRepository
|
||||||
|
userSubRepo UserSubscriptionRepository
|
||||||
|
billingCacheService *BillingCacheService
|
||||||
|
deferredService *DeferredService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GatewayService) billingDeps() *billingDeps {
|
||||||
|
return &billingDeps{
|
||||||
|
accountRepo: s.accountRepo,
|
||||||
|
userRepo: s.userRepo,
|
||||||
|
userSubRepo: s.userSubRepo,
|
||||||
|
billingCacheService: s.billingCacheService,
|
||||||
|
deferredService: s.deferredService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RecordUsage 记录使用量并扣费(或更新订阅用量)
|
// RecordUsage 记录使用量并扣费(或更新订阅用量)
|
||||||
func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInput) error {
|
func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInput) error {
|
||||||
result := input.Result
|
result := input.Result
|
||||||
@@ -6542,45 +6656,21 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
|
|||||||
|
|
||||||
shouldBill := inserted || err != nil
|
shouldBill := inserted || err != nil
|
||||||
|
|
||||||
// 根据计费类型执行扣费
|
if shouldBill {
|
||||||
if isSubscriptionBilling {
|
postUsageBilling(ctx, &postUsageBillingParams{
|
||||||
// 订阅模式:更新订阅用量(使用 TotalCost 原始费用,不考虑倍率)
|
Cost: cost,
|
||||||
if shouldBill && cost.TotalCost > 0 {
|
User: user,
|
||||||
if err := s.userSubRepo.IncrementUsage(ctx, subscription.ID, cost.TotalCost); err != nil {
|
APIKey: apiKey,
|
||||||
logger.LegacyPrintf("service.gateway", "Increment subscription usage failed: %v", err)
|
Account: account,
|
||||||
}
|
Subscription: subscription,
|
||||||
// 异步更新订阅缓存
|
IsSubscriptionBill: isSubscriptionBilling,
|
||||||
s.billingCacheService.QueueUpdateSubscriptionUsage(user.ID, *apiKey.GroupID, cost.TotalCost)
|
AccountRateMultiplier: accountRateMultiplier,
|
||||||
}
|
APIKeyService: input.APIKeyService,
|
||||||
|
}, s.billingDeps())
|
||||||
} else {
|
} else {
|
||||||
// 余额模式:扣除用户余额(使用 ActualCost 考虑倍率后的费用)
|
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
||||||
if shouldBill && cost.ActualCost > 0 {
|
|
||||||
if err := s.userRepo.DeductBalance(ctx, user.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Deduct balance failed: %v", err)
|
|
||||||
}
|
|
||||||
// 异步更新余额缓存
|
|
||||||
s.billingCacheService.QueueDeductBalance(user.ID, cost.ActualCost)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 API Key 配额(如果设置了配额限制)
|
|
||||||
if shouldBill && cost.ActualCost > 0 && apiKey.Quota > 0 && input.APIKeyService != nil {
|
|
||||||
if err := input.APIKeyService.UpdateQuotaUsed(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Update API key quota failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update API Key rate limit usage
|
|
||||||
if shouldBill && cost.ActualCost > 0 && apiKey.HasRateLimits() && input.APIKeyService != nil {
|
|
||||||
if err := input.APIKeyService.UpdateRateLimitUsage(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Update API key rate limit usage failed: %v", err)
|
|
||||||
}
|
|
||||||
s.billingCacheService.QueueUpdateAPIKeyRateLimitUsage(apiKey.ID, cost.ActualCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule batch update for account last_used_at
|
|
||||||
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6740,44 +6830,21 @@ func (s *GatewayService) RecordUsageWithLongContext(ctx context.Context, input *
|
|||||||
|
|
||||||
shouldBill := inserted || err != nil
|
shouldBill := inserted || err != nil
|
||||||
|
|
||||||
// 根据计费类型执行扣费
|
if shouldBill {
|
||||||
if isSubscriptionBilling {
|
postUsageBilling(ctx, &postUsageBillingParams{
|
||||||
// 订阅模式:更新订阅用量(使用 TotalCost 原始费用,不考虑倍率)
|
Cost: cost,
|
||||||
if shouldBill && cost.TotalCost > 0 {
|
User: user,
|
||||||
if err := s.userSubRepo.IncrementUsage(ctx, subscription.ID, cost.TotalCost); err != nil {
|
APIKey: apiKey,
|
||||||
logger.LegacyPrintf("service.gateway", "Increment subscription usage failed: %v", err)
|
Account: account,
|
||||||
}
|
Subscription: subscription,
|
||||||
// 异步更新订阅缓存
|
IsSubscriptionBill: isSubscriptionBilling,
|
||||||
s.billingCacheService.QueueUpdateSubscriptionUsage(user.ID, *apiKey.GroupID, cost.TotalCost)
|
AccountRateMultiplier: accountRateMultiplier,
|
||||||
}
|
APIKeyService: input.APIKeyService,
|
||||||
|
}, s.billingDeps())
|
||||||
} else {
|
} else {
|
||||||
// 余额模式:扣除用户余额(使用 ActualCost 考虑倍率后的费用)
|
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
||||||
if shouldBill && cost.ActualCost > 0 {
|
|
||||||
if err := s.userRepo.DeductBalance(ctx, user.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Deduct balance failed: %v", err)
|
|
||||||
}
|
|
||||||
// 异步更新余额缓存
|
|
||||||
s.billingCacheService.QueueDeductBalance(user.ID, cost.ActualCost)
|
|
||||||
// API Key 独立配额扣费
|
|
||||||
if input.APIKeyService != nil && apiKey.Quota > 0 {
|
|
||||||
if err := input.APIKeyService.UpdateQuotaUsed(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Add API key quota used failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update API Key rate limit usage
|
|
||||||
if shouldBill && cost.ActualCost > 0 && apiKey.HasRateLimits() && input.APIKeyService != nil {
|
|
||||||
if err := input.APIKeyService.UpdateRateLimitUsage(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.gateway", "Update API key rate limit usage failed: %v", err)
|
|
||||||
}
|
|
||||||
s.billingCacheService.QueueUpdateAPIKeyRateLimitUsage(apiKey.ID, cost.ActualCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule batch update for account last_used_at
|
|
||||||
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,14 @@ func (m *mockAccountRepoForGemini) BulkUpdate(ctx context.Context, ids []int64,
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockAccountRepoForGemini) IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockAccountRepoForGemini) ResetQuotaUsed(ctx context.Context, id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Verify interface implementation
|
// Verify interface implementation
|
||||||
var _ AccountRepository = (*mockAccountRepoForGemini)(nil)
|
var _ AccountRepository = (*mockAccountRepoForGemini)(nil)
|
||||||
|
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ func (s *defaultOpenAIAccountScheduler) selectBySessionHash(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := s.service.schedulingConfig()
|
cfg := s.service.schedulingConfig()
|
||||||
|
// WaitPlan.MaxConcurrency 使用 Concurrency(非 EffectiveLoadFactor),因为 WaitPlan 控制的是 Redis 实际并发槽位等待。
|
||||||
if s.service.concurrencyService != nil {
|
if s.service.concurrencyService != nil {
|
||||||
return &AccountSelectionResult{
|
return &AccountSelectionResult{
|
||||||
Account: account,
|
Account: account,
|
||||||
@@ -590,7 +591,7 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
|
|||||||
filtered = append(filtered, account)
|
filtered = append(filtered, account)
|
||||||
loadReq = append(loadReq, AccountWithConcurrency{
|
loadReq = append(loadReq, AccountWithConcurrency{
|
||||||
ID: account.ID,
|
ID: account.ID,
|
||||||
MaxConcurrency: account.Concurrency,
|
MaxConcurrency: account.EffectiveLoadFactor(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
@@ -703,6 +704,7 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance(
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := s.service.schedulingConfig()
|
cfg := s.service.schedulingConfig()
|
||||||
|
// WaitPlan.MaxConcurrency 使用 Concurrency(非 EffectiveLoadFactor),因为 WaitPlan 控制的是 Redis 实际并发槽位等待。
|
||||||
candidate := selectionOrder[0]
|
candidate := selectionOrder[0]
|
||||||
return &AccountSelectionResult{
|
return &AccountSelectionResult{
|
||||||
Account: candidate.account,
|
Account: candidate.account,
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import (
|
|||||||
var codexCLIInstructions string
|
var codexCLIInstructions string
|
||||||
|
|
||||||
var codexModelMap = map[string]string{
|
var codexModelMap = map[string]string{
|
||||||
|
"gpt-5.4": "gpt-5.4",
|
||||||
|
"gpt-5.4-none": "gpt-5.4",
|
||||||
|
"gpt-5.4-low": "gpt-5.4",
|
||||||
|
"gpt-5.4-medium": "gpt-5.4",
|
||||||
|
"gpt-5.4-high": "gpt-5.4",
|
||||||
|
"gpt-5.4-xhigh": "gpt-5.4",
|
||||||
|
"gpt-5.4-chat-latest": "gpt-5.4",
|
||||||
"gpt-5.3": "gpt-5.3-codex",
|
"gpt-5.3": "gpt-5.3-codex",
|
||||||
"gpt-5.3-none": "gpt-5.3-codex",
|
"gpt-5.3-none": "gpt-5.3-codex",
|
||||||
"gpt-5.3-low": "gpt-5.3-codex",
|
"gpt-5.3-low": "gpt-5.3-codex",
|
||||||
@@ -154,6 +161,9 @@ func normalizeCodexModel(model string) string {
|
|||||||
|
|
||||||
normalized := strings.ToLower(modelID)
|
normalized := strings.ToLower(modelID)
|
||||||
|
|
||||||
|
if strings.Contains(normalized, "gpt-5.4") || strings.Contains(normalized, "gpt 5.4") {
|
||||||
|
return "gpt-5.4"
|
||||||
|
}
|
||||||
if strings.Contains(normalized, "gpt-5.2-codex") || strings.Contains(normalized, "gpt 5.2 codex") {
|
if strings.Contains(normalized, "gpt-5.2-codex") || strings.Contains(normalized, "gpt 5.2 codex") {
|
||||||
return "gpt-5.2-codex"
|
return "gpt-5.2-codex"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ func TestApplyCodexOAuthTransform_EmptyInput(t *testing.T) {
|
|||||||
|
|
||||||
func TestNormalizeCodexModel_Gpt53(t *testing.T) {
|
func TestNormalizeCodexModel_Gpt53(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
|
"gpt-5.4": "gpt-5.4",
|
||||||
|
"gpt-5.4-high": "gpt-5.4",
|
||||||
|
"gpt-5.4-chat-latest": "gpt-5.4",
|
||||||
|
"gpt 5.4": "gpt-5.4",
|
||||||
"gpt-5.3": "gpt-5.3-codex",
|
"gpt-5.3": "gpt-5.3-codex",
|
||||||
"gpt-5.3-codex": "gpt-5.3-codex",
|
"gpt-5.3-codex": "gpt-5.3-codex",
|
||||||
"gpt-5.3-codex-xhigh": "gpt-5.3-codex",
|
"gpt-5.3-codex-xhigh": "gpt-5.3-codex",
|
||||||
|
|||||||
@@ -319,6 +319,16 @@ func NewOpenAIGatewayService(
|
|||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenAIGatewayService) billingDeps() *billingDeps {
|
||||||
|
return &billingDeps{
|
||||||
|
accountRepo: s.accountRepo,
|
||||||
|
userRepo: s.userRepo,
|
||||||
|
userSubRepo: s.userSubRepo,
|
||||||
|
billingCacheService: s.billingCacheService,
|
||||||
|
deferredService: s.deferredService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CloseOpenAIWSPool 关闭 OpenAI WebSocket 连接池的后台 worker 和空闲连接。
|
// CloseOpenAIWSPool 关闭 OpenAI WebSocket 连接池的后台 worker 和空闲连接。
|
||||||
// 应在应用优雅关闭时调用。
|
// 应在应用优雅关闭时调用。
|
||||||
func (s *OpenAIGatewayService) CloseOpenAIWSPool() {
|
func (s *OpenAIGatewayService) CloseOpenAIWSPool() {
|
||||||
@@ -1242,7 +1252,7 @@ func (s *OpenAIGatewayService) SelectAccountWithLoadAwareness(ctx context.Contex
|
|||||||
for _, acc := range candidates {
|
for _, acc := range candidates {
|
||||||
accountLoads = append(accountLoads, AccountWithConcurrency{
|
accountLoads = append(accountLoads, AccountWithConcurrency{
|
||||||
ID: acc.ID,
|
ID: acc.ID,
|
||||||
MaxConcurrency: acc.Concurrency,
|
MaxConcurrency: acc.EffectiveLoadFactor(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3474,37 +3484,21 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
|||||||
|
|
||||||
shouldBill := inserted || err != nil
|
shouldBill := inserted || err != nil
|
||||||
|
|
||||||
// Deduct based on billing type
|
if shouldBill {
|
||||||
if isSubscriptionBilling {
|
postUsageBilling(ctx, &postUsageBillingParams{
|
||||||
if shouldBill && cost.TotalCost > 0 {
|
Cost: cost,
|
||||||
_ = s.userSubRepo.IncrementUsage(ctx, subscription.ID, cost.TotalCost)
|
User: user,
|
||||||
s.billingCacheService.QueueUpdateSubscriptionUsage(user.ID, *apiKey.GroupID, cost.TotalCost)
|
APIKey: apiKey,
|
||||||
}
|
Account: account,
|
||||||
|
Subscription: subscription,
|
||||||
|
IsSubscriptionBill: isSubscriptionBilling,
|
||||||
|
AccountRateMultiplier: accountRateMultiplier,
|
||||||
|
APIKeyService: input.APIKeyService,
|
||||||
|
}, s.billingDeps())
|
||||||
} else {
|
} else {
|
||||||
if shouldBill && cost.ActualCost > 0 {
|
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
||||||
_ = s.userRepo.DeductBalance(ctx, user.ID, cost.ActualCost)
|
|
||||||
s.billingCacheService.QueueDeductBalance(user.ID, cost.ActualCost)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update API key quota if applicable (only for balance mode with quota set)
|
|
||||||
if shouldBill && cost.ActualCost > 0 && apiKey.Quota > 0 && input.APIKeyService != nil {
|
|
||||||
if err := input.APIKeyService.UpdateQuotaUsed(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.openai_gateway", "Update API key quota failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update API Key rate limit usage
|
|
||||||
if shouldBill && cost.ActualCost > 0 && apiKey.HasRateLimits() && input.APIKeyService != nil {
|
|
||||||
if err := input.APIKeyService.UpdateRateLimitUsage(ctx, apiKey.ID, cost.ActualCost); err != nil {
|
|
||||||
logger.LegacyPrintf("service.openai_gateway", "Update API key rate limit usage failed: %v", err)
|
|
||||||
}
|
|
||||||
s.billingCacheService.QueueUpdateAPIKeyRateLimitUsage(apiKey.ID, cost.ActualCost)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schedule batch update for account last_used_at
|
|
||||||
s.deferredService.ScheduleLastUsedUpdate(account.ID)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -864,7 +864,8 @@ func isOpenAIWSClientDisconnectError(err error) bool {
|
|||||||
strings.Contains(message, "unexpected eof") ||
|
strings.Contains(message, "unexpected eof") ||
|
||||||
strings.Contains(message, "use of closed network connection") ||
|
strings.Contains(message, "use of closed network connection") ||
|
||||||
strings.Contains(message, "connection reset by peer") ||
|
strings.Contains(message, "connection reset by peer") ||
|
||||||
strings.Contains(message, "broken pipe")
|
strings.Contains(message, "broken pipe") ||
|
||||||
|
strings.Contains(message, "an established connection was aborted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func classifyOpenAIWSReadFallbackReason(err error) string {
|
func classifyOpenAIWSReadFallbackReason(err error) string {
|
||||||
|
|||||||
@@ -64,8 +64,12 @@ func (s *OpsService) getAccountsLoadMapBestEffort(ctx context.Context, accounts
|
|||||||
if acc.ID <= 0 {
|
if acc.ID <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if prev, ok := unique[acc.ID]; !ok || acc.Concurrency > prev {
|
c := acc.Concurrency
|
||||||
unique[acc.ID] = acc.Concurrency
|
if c <= 0 {
|
||||||
|
c = 1
|
||||||
|
}
|
||||||
|
if prev, ok := unique[acc.ID]; !ok || c > prev {
|
||||||
|
unique[acc.ID] = c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -389,13 +389,9 @@ func (c *OpsMetricsCollector) collectConcurrencyQueueDepth(parentCtx context.Con
|
|||||||
if acc.ID <= 0 {
|
if acc.ID <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
maxConc := acc.Concurrency
|
|
||||||
if maxConc < 0 {
|
|
||||||
maxConc = 0
|
|
||||||
}
|
|
||||||
batch = append(batch, AccountWithConcurrency{
|
batch = append(batch, AccountWithConcurrency{
|
||||||
ID: acc.ID,
|
ID: acc.ID,
|
||||||
MaxConcurrency: maxConc,
|
MaxConcurrency: acc.Concurrency,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(batch) == 0 {
|
if len(batch) == 0 {
|
||||||
|
|||||||
@@ -21,8 +21,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
openAIModelDatePattern = regexp.MustCompile(`-\d{8}$`)
|
openAIModelDatePattern = regexp.MustCompile(`-\d{8}$`)
|
||||||
openAIModelBasePattern = regexp.MustCompile(`^(gpt-\d+(?:\.\d+)?)(?:-|$)`)
|
openAIModelBasePattern = regexp.MustCompile(`^(gpt-\d+(?:\.\d+)?)(?:-|$)`)
|
||||||
|
openAIGPT54FallbackPricing = &LiteLLMModelPricing{
|
||||||
|
InputCostPerToken: 2.5e-06, // $2.5 per MTok
|
||||||
|
OutputCostPerToken: 1.5e-05, // $15 per MTok
|
||||||
|
CacheReadInputTokenCost: 2.5e-07, // $0.25 per MTok
|
||||||
|
LongContextInputTokenThreshold: 272000,
|
||||||
|
LongContextInputCostMultiplier: 2.0,
|
||||||
|
LongContextOutputCostMultiplier: 1.5,
|
||||||
|
LiteLLMProvider: "openai",
|
||||||
|
Mode: "chat",
|
||||||
|
SupportsPromptCaching: true,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// LiteLLMModelPricing LiteLLM价格数据结构
|
// LiteLLMModelPricing LiteLLM价格数据结构
|
||||||
@@ -33,6 +44,9 @@ type LiteLLMModelPricing struct {
|
|||||||
CacheCreationInputTokenCost float64 `json:"cache_creation_input_token_cost"`
|
CacheCreationInputTokenCost float64 `json:"cache_creation_input_token_cost"`
|
||||||
CacheCreationInputTokenCostAbove1hr float64 `json:"cache_creation_input_token_cost_above_1hr"`
|
CacheCreationInputTokenCostAbove1hr float64 `json:"cache_creation_input_token_cost_above_1hr"`
|
||||||
CacheReadInputTokenCost float64 `json:"cache_read_input_token_cost"`
|
CacheReadInputTokenCost float64 `json:"cache_read_input_token_cost"`
|
||||||
|
LongContextInputTokenThreshold int `json:"long_context_input_token_threshold,omitempty"`
|
||||||
|
LongContextInputCostMultiplier float64 `json:"long_context_input_cost_multiplier,omitempty"`
|
||||||
|
LongContextOutputCostMultiplier float64 `json:"long_context_output_cost_multiplier,omitempty"`
|
||||||
LiteLLMProvider string `json:"litellm_provider"`
|
LiteLLMProvider string `json:"litellm_provider"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
SupportsPromptCaching bool `json:"supports_prompt_caching"`
|
SupportsPromptCaching bool `json:"supports_prompt_caching"`
|
||||||
@@ -660,7 +674,8 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing {
|
|||||||
// 2. gpt-5.2-codex -> gpt-5.2(去掉后缀如 -codex, -mini, -max 等)
|
// 2. gpt-5.2-codex -> gpt-5.2(去掉后缀如 -codex, -mini, -max 等)
|
||||||
// 3. gpt-5.2-20251222 -> gpt-5.2(去掉日期版本号)
|
// 3. gpt-5.2-20251222 -> gpt-5.2(去掉日期版本号)
|
||||||
// 4. gpt-5.3-codex -> gpt-5.2-codex
|
// 4. gpt-5.3-codex -> gpt-5.2-codex
|
||||||
// 5. 最终回退到 DefaultTestModel (gpt-5.1-codex)
|
// 5. gpt-5.4* -> 业务静态兜底价
|
||||||
|
// 6. 最终回退到 DefaultTestModel (gpt-5.1-codex)
|
||||||
func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
||||||
if strings.HasPrefix(model, "gpt-5.3-codex-spark") {
|
if strings.HasPrefix(model, "gpt-5.3-codex-spark") {
|
||||||
if pricing, ok := s.pricingData["gpt-5.1-codex"]; ok {
|
if pricing, ok := s.pricingData["gpt-5.1-codex"]; ok {
|
||||||
@@ -690,6 +705,12 @@ func (s *PricingService) matchOpenAIModel(model string) *LiteLLMModelPricing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(model, "gpt-5.4") {
|
||||||
|
logger.With(zap.String("component", "service.pricing")).
|
||||||
|
Info(fmt.Sprintf("[Pricing] OpenAI fallback matched %s -> %s", model, "gpt-5.4(static)"))
|
||||||
|
return openAIGPT54FallbackPricing
|
||||||
|
}
|
||||||
|
|
||||||
// 最终回退到 DefaultTestModel
|
// 最终回退到 DefaultTestModel
|
||||||
defaultModel := strings.ToLower(openai.DefaultTestModel)
|
defaultModel := strings.ToLower(openai.DefaultTestModel)
|
||||||
if pricing, ok := s.pricingData[defaultModel]; ok {
|
if pricing, ok := s.pricingData[defaultModel]; ok {
|
||||||
|
|||||||
@@ -51,3 +51,20 @@ func TestGetModelPricing_OpenAIFallbackMatchedLoggedAsInfo(t *testing.T) {
|
|||||||
require.True(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "info"))
|
require.True(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "info"))
|
||||||
require.False(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "warn"))
|
require.False(t, logSink.ContainsMessageAtLevel("[Pricing] OpenAI fallback matched gpt-5.3-codex -> gpt-5.2-codex", "warn"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetModelPricing_Gpt54UsesStaticFallbackWhenRemoteMissing(t *testing.T) {
|
||||||
|
svc := &PricingService{
|
||||||
|
pricingData: map[string]*LiteLLMModelPricing{
|
||||||
|
"gpt-5.1-codex": &LiteLLMModelPricing{InputCostPerToken: 1.25e-6},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := svc.GetModelPricing("gpt-5.4")
|
||||||
|
require.NotNil(t, got)
|
||||||
|
require.InDelta(t, 2.5e-6, got.InputCostPerToken, 1e-12)
|
||||||
|
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
|
||||||
|
require.InDelta(t, 2.5e-7, got.CacheReadInputTokenCost, 1e-12)
|
||||||
|
require.Equal(t, 272000, got.LongContextInputTokenThreshold)
|
||||||
|
require.InDelta(t, 2.0, got.LongContextInputCostMultiplier, 1e-12)
|
||||||
|
require.InDelta(t, 1.5, got.LongContextOutputCostMultiplier, 1e-12)
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestCalculateProgress_BasicFields(t *testing.T) {
|
|||||||
assert.Equal(t, int64(100), progress.ID)
|
assert.Equal(t, int64(100), progress.ID)
|
||||||
assert.Equal(t, "Premium", progress.GroupName)
|
assert.Equal(t, "Premium", progress.GroupName)
|
||||||
assert.Equal(t, sub.ExpiresAt, progress.ExpiresAt)
|
assert.Equal(t, sub.ExpiresAt, progress.ExpiresAt)
|
||||||
assert.Equal(t, 29, progress.ExpiresInDays) // 约 30 天
|
assert.True(t, progress.ExpiresInDays == 29 || progress.ExpiresInDays == 30, "ExpiresInDays should be 29 or 30, got %d", progress.ExpiresInDays)
|
||||||
assert.Nil(t, progress.Daily, "无日限额时 Daily 应为 nil")
|
assert.Nil(t, progress.Daily, "无日限额时 Daily 应为 nil")
|
||||||
assert.Nil(t, progress.Weekly, "无周限额时 Weekly 应为 nil")
|
assert.Nil(t, progress.Weekly, "无周限额时 Weekly 应为 nil")
|
||||||
assert.Nil(t, progress.Monthly, "无月限额时 Monthly 应为 nil")
|
assert.Nil(t, progress.Monthly, "无月限额时 Monthly 应为 nil")
|
||||||
|
|||||||
1
backend/migrations/067_add_account_load_factor.sql
Normal file
1
backend/migrations/067_add_account_load_factor.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS load_factor INTEGER;
|
||||||
@@ -5140,6 +5140,39 @@
|
|||||||
"supports_vision": true,
|
"supports_vision": true,
|
||||||
"supports_web_search": true
|
"supports_web_search": true
|
||||||
},
|
},
|
||||||
|
"gpt-5.4": {
|
||||||
|
"cache_read_input_token_cost": 2.5e-07,
|
||||||
|
"input_cost_per_token": 2.5e-06,
|
||||||
|
"litellm_provider": "openai",
|
||||||
|
"max_input_tokens": 1050000,
|
||||||
|
"max_output_tokens": 128000,
|
||||||
|
"max_tokens": 128000,
|
||||||
|
"mode": "chat",
|
||||||
|
"output_cost_per_token": 1.5e-05,
|
||||||
|
"supported_endpoints": [
|
||||||
|
"/v1/chat/completions",
|
||||||
|
"/v1/responses"
|
||||||
|
],
|
||||||
|
"supported_modalities": [
|
||||||
|
"text",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"supported_output_modalities": [
|
||||||
|
"text",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
|
"supports_function_calling": true,
|
||||||
|
"supports_native_streaming": true,
|
||||||
|
"supports_parallel_function_calling": true,
|
||||||
|
"supports_pdf_input": true,
|
||||||
|
"supports_prompt_caching": true,
|
||||||
|
"supports_reasoning": true,
|
||||||
|
"supports_response_schema": true,
|
||||||
|
"supports_service_tier": true,
|
||||||
|
"supports_system_messages": true,
|
||||||
|
"supports_tool_choice": true,
|
||||||
|
"supports_vision": true
|
||||||
|
},
|
||||||
"gpt-5.3-codex": {
|
"gpt-5.3-codex": {
|
||||||
"cache_read_input_token_cost": 1.75e-07,
|
"cache_read_input_token_cost": 1.75e-07,
|
||||||
"cache_read_input_token_cost_priority": 3.5e-07,
|
"cache_read_input_token_cost_priority": 3.5e-07,
|
||||||
|
|||||||
@@ -240,6 +240,18 @@ export async function clearRateLimit(id: number): Promise<Account> {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset account quota usage
|
||||||
|
* @param id - Account ID
|
||||||
|
* @returns Updated account
|
||||||
|
*/
|
||||||
|
export async function resetAccountQuota(id: number): Promise<Account> {
|
||||||
|
const { data } = await apiClient.post<Account>(
|
||||||
|
`/admin/accounts/${id}/reset-quota`
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get temporary unschedulable status
|
* Get temporary unschedulable status
|
||||||
* @param id - Account ID
|
* @param id - Account ID
|
||||||
@@ -576,6 +588,7 @@ export const accountsAPI = {
|
|||||||
getTodayStats,
|
getTodayStats,
|
||||||
getBatchTodayStats,
|
getBatchTodayStats,
|
||||||
clearRateLimit,
|
clearRateLimit,
|
||||||
|
resetAccountQuota,
|
||||||
getTempUnschedulableStatus,
|
getTempUnschedulableStatus,
|
||||||
resetTempUnschedulable,
|
resetTempUnschedulable,
|
||||||
setSchedulable,
|
setSchedulable,
|
||||||
|
|||||||
@@ -71,6 +71,24 @@
|
|||||||
<span class="text-[9px] opacity-60">{{ rpmStrategyTag }}</span>
|
<span class="text-[9px] opacity-60">{{ rpmStrategyTag }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key 账号配额限制 -->
|
||||||
|
<div v-if="showQuotaLimit" class="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-medium',
|
||||||
|
quotaClass
|
||||||
|
]"
|
||||||
|
:title="quotaTooltip"
|
||||||
|
>
|
||||||
|
<svg class="h-2.5 w-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
|
||||||
|
</svg>
|
||||||
|
<span class="font-mono">${{ formatCost(currentQuotaUsed) }}</span>
|
||||||
|
<span class="text-gray-400 dark:text-gray-500">/</span>
|
||||||
|
<span class="font-mono">${{ formatCost(account.quota_limit) }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -286,6 +304,48 @@ const rpmTooltip = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 是否显示配额限制(仅 apikey 类型且设置了 quota_limit)
|
||||||
|
const showQuotaLimit = computed(() => {
|
||||||
|
return (
|
||||||
|
props.account.type === 'apikey' &&
|
||||||
|
props.account.quota_limit !== undefined &&
|
||||||
|
props.account.quota_limit !== null &&
|
||||||
|
props.account.quota_limit > 0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前已用配额
|
||||||
|
const currentQuotaUsed = computed(() => props.account.quota_used ?? 0)
|
||||||
|
|
||||||
|
// 配额状态样式
|
||||||
|
const quotaClass = computed(() => {
|
||||||
|
if (!showQuotaLimit.value) return ''
|
||||||
|
|
||||||
|
const used = currentQuotaUsed.value
|
||||||
|
const limit = props.account.quota_limit || 0
|
||||||
|
|
||||||
|
if (used >= limit) {
|
||||||
|
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||||
|
}
|
||||||
|
if (used >= limit * 0.8) {
|
||||||
|
return 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||||||
|
}
|
||||||
|
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 配额提示文字
|
||||||
|
const quotaTooltip = computed(() => {
|
||||||
|
if (!showQuotaLimit.value) return ''
|
||||||
|
|
||||||
|
const used = currentQuotaUsed.value
|
||||||
|
const limit = props.account.quota_limit || 0
|
||||||
|
|
||||||
|
if (used >= limit) {
|
||||||
|
return t('admin.accounts.capacity.quota.exceeded')
|
||||||
|
}
|
||||||
|
return t('admin.accounts.capacity.quota.normal')
|
||||||
|
})
|
||||||
|
|
||||||
// 格式化费用显示
|
// 格式化费用显示
|
||||||
const formatCost = (value: number | null | undefined) => {
|
const formatCost = (value: number | null | undefined) => {
|
||||||
if (value === null || value === undefined) return '0'
|
if (value === null || value === undefined) return '0'
|
||||||
|
|||||||
@@ -469,7 +469,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Concurrency & Priority -->
|
<!-- Concurrency & Priority -->
|
||||||
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600 lg:grid-cols-3">
|
<div class="grid grid-cols-2 gap-4 border-t border-gray-200 pt-4 dark:border-dark-600 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<label
|
<label
|
||||||
@@ -496,8 +496,39 @@
|
|||||||
class="input"
|
class="input"
|
||||||
:class="!enableConcurrency && 'cursor-not-allowed opacity-50'"
|
:class="!enableConcurrency && 'cursor-not-allowed opacity-50'"
|
||||||
aria-labelledby="bulk-edit-concurrency-label"
|
aria-labelledby="bulk-edit-concurrency-label"
|
||||||
|
@input="concurrency = Math.max(1, concurrency || 1)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<label
|
||||||
|
id="bulk-edit-load-factor-label"
|
||||||
|
class="input-label mb-0"
|
||||||
|
for="bulk-edit-load-factor-enabled"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.loadFactor') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="enableLoadFactor"
|
||||||
|
id="bulk-edit-load-factor-enabled"
|
||||||
|
type="checkbox"
|
||||||
|
aria-controls="bulk-edit-load-factor"
|
||||||
|
class="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model.number="loadFactor"
|
||||||
|
id="bulk-edit-load-factor"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:disabled="!enableLoadFactor"
|
||||||
|
class="input"
|
||||||
|
:class="!enableLoadFactor && 'cursor-not-allowed opacity-50'"
|
||||||
|
aria-labelledby="bulk-edit-load-factor-label"
|
||||||
|
@input="loadFactor = (loadFactor && loadFactor >= 1) ? loadFactor : null"
|
||||||
|
/>
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.loadFactorHint') }}</p>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<label
|
<label
|
||||||
@@ -869,6 +900,7 @@ const enableCustomErrorCodes = ref(false)
|
|||||||
const enableInterceptWarmup = ref(false)
|
const enableInterceptWarmup = ref(false)
|
||||||
const enableProxy = ref(false)
|
const enableProxy = ref(false)
|
||||||
const enableConcurrency = ref(false)
|
const enableConcurrency = ref(false)
|
||||||
|
const enableLoadFactor = ref(false)
|
||||||
const enablePriority = ref(false)
|
const enablePriority = ref(false)
|
||||||
const enableRateMultiplier = ref(false)
|
const enableRateMultiplier = ref(false)
|
||||||
const enableStatus = ref(false)
|
const enableStatus = ref(false)
|
||||||
@@ -889,6 +921,7 @@ const customErrorCodeInput = ref<number | null>(null)
|
|||||||
const interceptWarmupRequests = ref(false)
|
const interceptWarmupRequests = ref(false)
|
||||||
const proxyId = ref<number | null>(null)
|
const proxyId = ref<number | null>(null)
|
||||||
const concurrency = ref(1)
|
const concurrency = ref(1)
|
||||||
|
const loadFactor = ref<number | null>(null)
|
||||||
const priority = ref(1)
|
const priority = ref(1)
|
||||||
const rateMultiplier = ref(1)
|
const rateMultiplier = ref(1)
|
||||||
const status = ref<'active' | 'inactive'>('active')
|
const status = ref<'active' | 'inactive'>('active')
|
||||||
@@ -918,6 +951,7 @@ const allModels = [
|
|||||||
{ value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' },
|
{ value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' },
|
||||||
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
{ value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
|
||||||
{ value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' },
|
{ value: 'gpt-5.3-codex-spark', label: 'GPT-5.3 Codex Spark' },
|
||||||
|
{ value: 'gpt-5.4', label: 'GPT-5.4' },
|
||||||
{ value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' },
|
{ value: 'gpt-5.2-2025-12-11', label: 'GPT-5.2' },
|
||||||
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
{ value: 'gpt-5.2-codex', label: 'GPT-5.2 Codex' },
|
||||||
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
{ value: 'gpt-5.1-codex-max', label: 'GPT-5.1 Codex Max' },
|
||||||
@@ -1032,6 +1066,12 @@ const presetMappings = [
|
|||||||
to: 'gpt-5.3-codex-spark',
|
to: 'gpt-5.3-codex-spark',
|
||||||
color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
|
color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'GPT-5.4',
|
||||||
|
from: 'gpt-5.4',
|
||||||
|
to: 'gpt-5.4',
|
||||||
|
color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '5.2→5.3',
|
label: '5.2→5.3',
|
||||||
from: 'gpt-5.2-codex',
|
from: 'gpt-5.2-codex',
|
||||||
@@ -1195,6 +1235,12 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
updates.concurrency = concurrency.value
|
updates.concurrency = concurrency.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (enableLoadFactor.value) {
|
||||||
|
// 空值/NaN/0 时发送 0(后端约定 <= 0 表示清除)
|
||||||
|
const lf = loadFactor.value
|
||||||
|
updates.load_factor = (lf != null && !Number.isNaN(lf) && lf > 0) ? lf : 0
|
||||||
|
}
|
||||||
|
|
||||||
if (enablePriority.value) {
|
if (enablePriority.value) {
|
||||||
updates.priority = priority.value
|
updates.priority = priority.value
|
||||||
}
|
}
|
||||||
@@ -1340,6 +1386,7 @@ const handleSubmit = async () => {
|
|||||||
enableInterceptWarmup.value ||
|
enableInterceptWarmup.value ||
|
||||||
enableProxy.value ||
|
enableProxy.value ||
|
||||||
enableConcurrency.value ||
|
enableConcurrency.value ||
|
||||||
|
enableLoadFactor.value ||
|
||||||
enablePriority.value ||
|
enablePriority.value ||
|
||||||
enableRateMultiplier.value ||
|
enableRateMultiplier.value ||
|
||||||
enableStatus.value ||
|
enableStatus.value ||
|
||||||
@@ -1430,6 +1477,7 @@ watch(
|
|||||||
enableInterceptWarmup.value = false
|
enableInterceptWarmup.value = false
|
||||||
enableProxy.value = false
|
enableProxy.value = false
|
||||||
enableConcurrency.value = false
|
enableConcurrency.value = false
|
||||||
|
enableLoadFactor.value = false
|
||||||
enablePriority.value = false
|
enablePriority.value = false
|
||||||
enableRateMultiplier.value = false
|
enableRateMultiplier.value = false
|
||||||
enableStatus.value = false
|
enableStatus.value = false
|
||||||
@@ -1446,6 +1494,7 @@ watch(
|
|||||||
interceptWarmupRequests.value = false
|
interceptWarmupRequests.value = false
|
||||||
proxyId.value = null
|
proxyId.value = null
|
||||||
concurrency.value = 1
|
concurrency.value = 1
|
||||||
|
loadFactor.value = null
|
||||||
priority.value = 1
|
priority.value = 1
|
||||||
rateMultiplier.value = 1
|
rateMultiplier.value = 1
|
||||||
status.value = 'active'
|
status.value = 'active'
|
||||||
|
|||||||
@@ -1227,6 +1227,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key 账号配额限制 -->
|
||||||
|
<QuotaLimitCard v-if="form.type === 'apikey'" v-model="editQuotaLimit" />
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
@@ -1749,10 +1752,18 @@
|
|||||||
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
||||||
<input v-model.number="form.concurrency" type="number" min="1" class="input" />
|
<input v-model.number="form.concurrency" type="number" min="1" class="input"
|
||||||
|
@input="form.concurrency = Math.max(1, form.concurrency || 1)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.loadFactor') }}</label>
|
||||||
|
<input v-model.number="form.load_factor" type="number" min="1"
|
||||||
|
class="input" :placeholder="String(form.concurrency || 1)"
|
||||||
|
@input="form.load_factor = (form.load_factor && form.load_factor >= 1) ? form.load_factor : null" />
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.loadFactorHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.priority') }}</label>
|
<label class="input-label">{{ t('admin.accounts.priority') }}</label>
|
||||||
@@ -2337,6 +2348,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||||
@@ -2460,6 +2472,7 @@ const accountCategory = ref<'oauth-based' | 'apikey'>('oauth-based') // UI selec
|
|||||||
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
|
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
|
||||||
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
||||||
const apiKeyValue = ref('')
|
const apiKeyValue = ref('')
|
||||||
|
const editQuotaLimit = ref<number | null>(null)
|
||||||
const modelMappings = ref<ModelMapping[]>([])
|
const modelMappings = ref<ModelMapping[]>([])
|
||||||
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||||
const allowedModels = ref<string[]>([])
|
const allowedModels = ref<string[]>([])
|
||||||
@@ -2633,6 +2646,7 @@ const form = reactive({
|
|||||||
credentials: {} as Record<string, unknown>,
|
credentials: {} as Record<string, unknown>,
|
||||||
proxy_id: null as number | null,
|
proxy_id: null as number | null,
|
||||||
concurrency: 10,
|
concurrency: 10,
|
||||||
|
load_factor: null as number | null,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
rate_multiplier: 1,
|
rate_multiplier: 1,
|
||||||
group_ids: [] as number[],
|
group_ids: [] as number[],
|
||||||
@@ -3112,6 +3126,7 @@ const resetForm = () => {
|
|||||||
form.credentials = {}
|
form.credentials = {}
|
||||||
form.proxy_id = null
|
form.proxy_id = null
|
||||||
form.concurrency = 10
|
form.concurrency = 10
|
||||||
|
form.load_factor = null
|
||||||
form.priority = 1
|
form.priority = 1
|
||||||
form.rate_multiplier = 1
|
form.rate_multiplier = 1
|
||||||
form.group_ids = []
|
form.group_ids = []
|
||||||
@@ -3120,6 +3135,7 @@ const resetForm = () => {
|
|||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||||
apiKeyValue.value = ''
|
apiKeyValue.value = ''
|
||||||
|
editQuotaLimit.value = null
|
||||||
modelMappings.value = []
|
modelMappings.value = []
|
||||||
modelRestrictionMode.value = 'whitelist'
|
modelRestrictionMode.value = 'whitelist'
|
||||||
allowedModels.value = [...claudeModels] // Default fill related models
|
allowedModels.value = [...claudeModels] // Default fill related models
|
||||||
@@ -3483,6 +3499,7 @@ const handleImportAccessToken = async (accessTokenInput: string) => {
|
|||||||
extra: soraExtra,
|
extra: soraExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3533,15 +3550,21 @@ const createAccountAndFinish = async (
|
|||||||
if (!applyTempUnschedConfig(credentials)) {
|
if (!applyTempUnschedConfig(credentials)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Inject quota_limit for apikey accounts
|
||||||
|
let finalExtra = extra
|
||||||
|
if (type === 'apikey' && editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||||
|
finalExtra = { ...(extra || {}), quota_limit: editQuotaLimit.value }
|
||||||
|
}
|
||||||
await doCreateAccount({
|
await doCreateAccount({
|
||||||
name: form.name,
|
name: form.name,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
platform,
|
platform,
|
||||||
type,
|
type,
|
||||||
credentials,
|
credentials,
|
||||||
extra,
|
extra: finalExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3597,6 +3620,7 @@ const handleOpenAIExchange = async (authCode: string) => {
|
|||||||
extra,
|
extra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3626,6 +3650,7 @@ const handleOpenAIExchange = async (authCode: string) => {
|
|||||||
extra: soraExtra,
|
extra: soraExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3703,6 +3728,7 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
|
|||||||
extra,
|
extra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3730,6 +3756,7 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
|
|||||||
extra: soraExtra,
|
extra: soraExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3818,6 +3845,7 @@ const handleSoraValidateST = async (sessionTokenInput: string) => {
|
|||||||
extra: soraExtra,
|
extra: soraExtra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -3906,6 +3934,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
|||||||
extra: {},
|
extra: {},
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
@@ -4064,8 +4093,11 @@ const handleAnthropicExchange = async (authCode: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add RPM limit settings
|
// Add RPM limit settings
|
||||||
if (rpmLimitEnabled.value && baseRpm.value != null && baseRpm.value > 0) {
|
if (rpmLimitEnabled.value) {
|
||||||
extra.base_rpm = baseRpm.value
|
const DEFAULT_BASE_RPM = 15
|
||||||
|
extra.base_rpm = (baseRpm.value != null && baseRpm.value > 0)
|
||||||
|
? baseRpm.value
|
||||||
|
: DEFAULT_BASE_RPM
|
||||||
extra.rpm_strategy = rpmStrategy.value
|
extra.rpm_strategy = rpmStrategy.value
|
||||||
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
||||||
extra.rpm_sticky_buffer = rpmStickyBuffer.value
|
extra.rpm_sticky_buffer = rpmStickyBuffer.value
|
||||||
@@ -4176,8 +4208,11 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add RPM limit settings
|
// Add RPM limit settings
|
||||||
if (rpmLimitEnabled.value && baseRpm.value != null && baseRpm.value > 0) {
|
if (rpmLimitEnabled.value) {
|
||||||
extra.base_rpm = baseRpm.value
|
const DEFAULT_BASE_RPM = 15
|
||||||
|
extra.base_rpm = (baseRpm.value != null && baseRpm.value > 0)
|
||||||
|
? baseRpm.value
|
||||||
|
: DEFAULT_BASE_RPM
|
||||||
extra.rpm_strategy = rpmStrategy.value
|
extra.rpm_strategy = rpmStrategy.value
|
||||||
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
||||||
extra.rpm_sticky_buffer = rpmStickyBuffer.value
|
extra.rpm_sticky_buffer = rpmStickyBuffer.value
|
||||||
@@ -4223,6 +4258,7 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
extra,
|
extra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
|
load_factor: form.load_factor ?? undefined,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
rate_multiplier: form.rate_multiplier,
|
rate_multiplier: form.rate_multiplier,
|
||||||
group_ids: form.group_ids,
|
group_ids: form.group_ids,
|
||||||
|
|||||||
@@ -650,10 +650,18 @@
|
|||||||
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
<ProxySelector v-model="form.proxy_id" :proxies="proxies" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-3">
|
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
<label class="input-label">{{ t('admin.accounts.concurrency') }}</label>
|
||||||
<input v-model.number="form.concurrency" type="number" min="1" class="input" />
|
<input v-model.number="form.concurrency" type="number" min="1" class="input"
|
||||||
|
@input="form.concurrency = Math.max(1, form.concurrency || 1)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.loadFactor') }}</label>
|
||||||
|
<input v-model.number="form.load_factor" type="number" min="1"
|
||||||
|
class="input" :placeholder="String(form.concurrency || 1)"
|
||||||
|
@input="form.load_factor = (form.load_factor && form.load_factor >= 1) ? form.load_factor : null" />
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.loadFactorHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.priority') }}</label>
|
<label class="input-label">{{ t('admin.accounts.priority') }}</label>
|
||||||
@@ -759,6 +767,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key 账号配额限制 -->
|
||||||
|
<QuotaLimitCard v-if="account?.type === 'apikey'" v-model="editQuotaLimit" />
|
||||||
|
|
||||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||||
<div
|
<div
|
||||||
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
|
v-if="account?.platform === 'openai' && account?.type === 'oauth'"
|
||||||
@@ -1269,6 +1280,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||||
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
import { applyInterceptWarmup } from '@/components/account/credentialsBuilder'
|
||||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||||
@@ -1386,6 +1398,7 @@ const openaiOAuthResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF
|
|||||||
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
const openaiAPIKeyResponsesWebSocketV2Mode = ref<OpenAIWSMode>(OPENAI_WS_MODE_OFF)
|
||||||
const codexCLIOnlyEnabled = ref(false)
|
const codexCLIOnlyEnabled = ref(false)
|
||||||
const anthropicPassthroughEnabled = ref(false)
|
const anthropicPassthroughEnabled = ref(false)
|
||||||
|
const editQuotaLimit = ref<number | null>(null)
|
||||||
const openAIWSModeOptions = computed(() => [
|
const openAIWSModeOptions = computed(() => [
|
||||||
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
|
||||||
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
|
||||||
@@ -1465,6 +1478,7 @@ const form = reactive({
|
|||||||
notes: '',
|
notes: '',
|
||||||
proxy_id: null as number | null,
|
proxy_id: null as number | null,
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
|
load_factor: null as number | null,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
rate_multiplier: 1,
|
rate_multiplier: 1,
|
||||||
status: 'active' as 'active' | 'inactive',
|
status: 'active' as 'active' | 'inactive',
|
||||||
@@ -1498,9 +1512,12 @@ watch(
|
|||||||
form.notes = newAccount.notes || ''
|
form.notes = newAccount.notes || ''
|
||||||
form.proxy_id = newAccount.proxy_id
|
form.proxy_id = newAccount.proxy_id
|
||||||
form.concurrency = newAccount.concurrency
|
form.concurrency = newAccount.concurrency
|
||||||
|
form.load_factor = newAccount.load_factor ?? null
|
||||||
form.priority = newAccount.priority
|
form.priority = newAccount.priority
|
||||||
form.rate_multiplier = newAccount.rate_multiplier ?? 1
|
form.rate_multiplier = newAccount.rate_multiplier ?? 1
|
||||||
form.status = newAccount.status as 'active' | 'inactive'
|
form.status = (newAccount.status === 'active' || newAccount.status === 'inactive')
|
||||||
|
? newAccount.status
|
||||||
|
: 'active'
|
||||||
form.group_ids = newAccount.group_ids || []
|
form.group_ids = newAccount.group_ids || []
|
||||||
form.expires_at = newAccount.expires_at ?? null
|
form.expires_at = newAccount.expires_at ?? null
|
||||||
|
|
||||||
@@ -1541,6 +1558,14 @@ watch(
|
|||||||
anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true
|
anthropicPassthroughEnabled.value = extra?.anthropic_passthrough === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load quota limit for apikey accounts
|
||||||
|
if (newAccount.type === 'apikey') {
|
||||||
|
const quotaVal = extra?.quota_limit as number | undefined
|
||||||
|
editQuotaLimit.value = (quotaVal && quotaVal > 0) ? quotaVal : null
|
||||||
|
} else {
|
||||||
|
editQuotaLimit.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// Load antigravity model mapping (Antigravity 只支持映射模式)
|
// Load antigravity model mapping (Antigravity 只支持映射模式)
|
||||||
if (newAccount.platform === 'antigravity') {
|
if (newAccount.platform === 'antigravity') {
|
||||||
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
||||||
@@ -2040,6 +2065,11 @@ const handleSubmit = async () => {
|
|||||||
if (!props.account) return
|
if (!props.account) return
|
||||||
const accountID = props.account.id
|
const accountID = props.account.id
|
||||||
|
|
||||||
|
if (form.status !== 'active' && form.status !== 'inactive') {
|
||||||
|
appStore.showError(t('admin.accounts.pleaseSelectStatus'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const updatePayload: Record<string, unknown> = { ...form }
|
const updatePayload: Record<string, unknown> = { ...form }
|
||||||
try {
|
try {
|
||||||
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
||||||
@@ -2049,6 +2079,11 @@ const handleSubmit = async () => {
|
|||||||
if (form.expires_at === null) {
|
if (form.expires_at === null) {
|
||||||
updatePayload.expires_at = 0
|
updatePayload.expires_at = 0
|
||||||
}
|
}
|
||||||
|
// load_factor: 空值/NaN/0/负数 时发送 0(后端约定 <= 0 = 清除)
|
||||||
|
const lf = form.load_factor
|
||||||
|
if (lf == null || Number.isNaN(lf) || lf <= 0) {
|
||||||
|
updatePayload.load_factor = 0
|
||||||
|
}
|
||||||
updatePayload.auto_pause_on_expired = autoPauseOnExpired.value
|
updatePayload.auto_pause_on_expired = autoPauseOnExpired.value
|
||||||
|
|
||||||
// For apikey type, handle credentials update
|
// For apikey type, handle credentials update
|
||||||
@@ -2188,8 +2223,11 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RPM limit settings
|
// RPM limit settings
|
||||||
if (rpmLimitEnabled.value && baseRpm.value != null && baseRpm.value > 0) {
|
if (rpmLimitEnabled.value) {
|
||||||
newExtra.base_rpm = baseRpm.value
|
const DEFAULT_BASE_RPM = 15
|
||||||
|
newExtra.base_rpm = (baseRpm.value != null && baseRpm.value > 0)
|
||||||
|
? baseRpm.value
|
||||||
|
: DEFAULT_BASE_RPM
|
||||||
newExtra.rpm_strategy = rpmStrategy.value
|
newExtra.rpm_strategy = rpmStrategy.value
|
||||||
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
if (rpmStickyBuffer.value != null && rpmStickyBuffer.value > 0) {
|
||||||
newExtra.rpm_sticky_buffer = rpmStickyBuffer.value
|
newExtra.rpm_sticky_buffer = rpmStickyBuffer.value
|
||||||
@@ -2283,6 +2321,19 @@ const handleSubmit = async () => {
|
|||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For apikey accounts, handle quota_limit in extra
|
||||||
|
if (props.account.type === 'apikey') {
|
||||||
|
const currentExtra = (updatePayload.extra as Record<string, unknown>) ||
|
||||||
|
(props.account.extra as Record<string, unknown>) || {}
|
||||||
|
const newExtra: Record<string, unknown> = { ...currentExtra }
|
||||||
|
if (editQuotaLimit.value != null && editQuotaLimit.value > 0) {
|
||||||
|
newExtra.quota_limit = editQuotaLimit.value
|
||||||
|
} else {
|
||||||
|
delete newExtra.quota_limit
|
||||||
|
}
|
||||||
|
updatePayload.extra = newExtra
|
||||||
|
}
|
||||||
|
|
||||||
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||||
await submitUpdateAccount(accountID, updatePayload)
|
await submitUpdateAccount(accountID, updatePayload)
|
||||||
})
|
})
|
||||||
|
|||||||
92
frontend/src/components/account/QuotaLimitCard.vue
Normal file
92
frontend/src/components/account/QuotaLimitCard.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: number | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: number | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const enabled = ref(props.modelValue != null && props.modelValue > 0)
|
||||||
|
|
||||||
|
// Sync enabled state when modelValue changes externally (e.g. account load)
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
enabled.value = val != null && val > 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// When toggle is turned off, clear the value
|
||||||
|
watch(enabled, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onInput = (e: Event) => {
|
||||||
|
const raw = (e.target as HTMLInputElement).valueAsNumber
|
||||||
|
emit('update:modelValue', Number.isNaN(raw) ? null : raw)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h3 class="input-label mb-0 text-base font-semibold">{{ t('admin.accounts.quotaLimit') }}</h3>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.quotaLimitHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
|
||||||
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<label class="input-label mb-0">{{ t('admin.accounts.quotaLimitToggle') }}</label>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.accounts.quotaLimitToggleHint') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="enabled = !enabled"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
enabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
enabled ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="enabled" class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.quotaLimitAmount') }}</label>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400">$</span>
|
||||||
|
<input
|
||||||
|
:value="modelValue"
|
||||||
|
@input="onInput"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
class="input pl-7"
|
||||||
|
:placeholder="t('admin.accounts.quotaLimitPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.quotaLimitAmountHint') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -41,6 +41,10 @@
|
|||||||
<Icon name="clock" size="sm" />
|
<Icon name="clock" size="sm" />
|
||||||
{{ t('admin.accounts.clearRateLimit') }}
|
{{ t('admin.accounts.clearRateLimit') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="hasQuotaLimit" @click="$emit('reset-quota', account); $emit('close')" class="flex w-full items-center gap-2 px-4 py-2 text-sm text-teal-600 hover:bg-gray-100 dark:hover:bg-dark-700">
|
||||||
|
<Icon name="refresh" size="sm" />
|
||||||
|
{{ t('admin.accounts.resetQuota') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +59,7 @@ import { Icon } from '@/components/icons'
|
|||||||
import type { Account } from '@/types'
|
import type { Account } from '@/types'
|
||||||
|
|
||||||
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
||||||
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'reset-status', 'clear-rate-limit'])
|
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'reauth', 'refresh-token', 'reset-status', 'clear-rate-limit', 'reset-quota'])
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const isRateLimited = computed(() => {
|
const isRateLimited = computed(() => {
|
||||||
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
|
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
|
||||||
@@ -71,6 +75,12 @@ const isRateLimited = computed(() => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const isOverloaded = computed(() => props.account?.overload_until && new Date(props.account.overload_until) > new Date())
|
const isOverloaded = computed(() => props.account?.overload_until && new Date(props.account.overload_until) > new Date())
|
||||||
|
const hasQuotaLimit = computed(() => {
|
||||||
|
return props.account?.type === 'apikey' &&
|
||||||
|
props.account?.quota_limit !== undefined &&
|
||||||
|
props.account?.quota_limit !== null &&
|
||||||
|
props.account?.quota_limit > 0
|
||||||
|
})
|
||||||
|
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') emit('close')
|
if (event.key === 'Escape') emit('close')
|
||||||
|
|||||||
@@ -512,12 +512,14 @@ function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
|||||||
|
|
||||||
// config.toml content
|
// config.toml content
|
||||||
const configContent = `model_provider = "OpenAI"
|
const configContent = `model_provider = "OpenAI"
|
||||||
model = "gpt-5.3-codex"
|
model = "gpt-5.4"
|
||||||
review_model = "gpt-5.3-codex"
|
review_model = "gpt-5.4"
|
||||||
model_reasoning_effort = "xhigh"
|
model_reasoning_effort = "xhigh"
|
||||||
disable_response_storage = true
|
disable_response_storage = true
|
||||||
network_access = "enabled"
|
network_access = "enabled"
|
||||||
windows_wsl_setup_acknowledged = true
|
windows_wsl_setup_acknowledged = true
|
||||||
|
model_context_window = 1000000
|
||||||
|
model_auto_compact_token_limit = 900000
|
||||||
|
|
||||||
[model_providers.OpenAI]
|
[model_providers.OpenAI]
|
||||||
name = "OpenAI"
|
name = "OpenAI"
|
||||||
@@ -549,12 +551,14 @@ function generateOpenAIWsFiles(baseUrl: string, apiKey: string): FileConfig[] {
|
|||||||
|
|
||||||
// config.toml content with WebSocket v2
|
// config.toml content with WebSocket v2
|
||||||
const configContent = `model_provider = "OpenAI"
|
const configContent = `model_provider = "OpenAI"
|
||||||
model = "gpt-5.3-codex"
|
model = "gpt-5.4"
|
||||||
review_model = "gpt-5.3-codex"
|
review_model = "gpt-5.4"
|
||||||
model_reasoning_effort = "xhigh"
|
model_reasoning_effort = "xhigh"
|
||||||
disable_response_storage = true
|
disable_response_storage = true
|
||||||
network_access = "enabled"
|
network_access = "enabled"
|
||||||
windows_wsl_setup_acknowledged = true
|
windows_wsl_setup_acknowledged = true
|
||||||
|
model_context_window = 1000000
|
||||||
|
model_auto_compact_token_limit = 900000
|
||||||
|
|
||||||
[model_providers.OpenAI]
|
[model_providers.OpenAI]
|
||||||
name = "OpenAI"
|
name = "OpenAI"
|
||||||
@@ -670,6 +674,22 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
|||||||
xhigh: {}
|
xhigh: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'gpt-5.4': {
|
||||||
|
name: 'GPT-5.4',
|
||||||
|
limit: {
|
||||||
|
context: 1050000,
|
||||||
|
output: 128000
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
store: false
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
low: {},
|
||||||
|
medium: {},
|
||||||
|
high: {},
|
||||||
|
xhigh: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
'gpt-5.3-codex-spark': {
|
'gpt-5.3-codex-spark': {
|
||||||
name: 'GPT-5.3 Codex Spark',
|
name: 'GPT-5.3 Codex Spark',
|
||||||
limit: {
|
limit: {
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { buildModelMappingObject, getModelsByPlatform } from '../useModelWhitelist'
|
import { buildModelMappingObject, getModelsByPlatform } from '../useModelWhitelist'
|
||||||
|
|
||||||
describe('useModelWhitelist', () => {
|
describe('useModelWhitelist', () => {
|
||||||
|
it('openai 模型列表包含 GPT-5.4 官方快照', () => {
|
||||||
|
const models = getModelsByPlatform('openai')
|
||||||
|
|
||||||
|
expect(models).toContain('gpt-5.4')
|
||||||
|
expect(models).toContain('gpt-5.4-2026-03-05')
|
||||||
|
})
|
||||||
|
|
||||||
it('antigravity 模型列表包含图片模型兼容项', () => {
|
it('antigravity 模型列表包含图片模型兼容项', () => {
|
||||||
const models = getModelsByPlatform('antigravity')
|
const models = getModelsByPlatform('antigravity')
|
||||||
|
|
||||||
@@ -15,4 +22,12 @@ describe('useModelWhitelist', () => {
|
|||||||
'gemini-3.1-flash-image': 'gemini-3.1-flash-image'
|
'gemini-3.1-flash-image': 'gemini-3.1-flash-image'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('whitelist 模式会保留 GPT-5.4 官方快照的精确映射', () => {
|
||||||
|
const mapping = buildModelMappingObject('whitelist', ['gpt-5.4-2026-03-05'], [])
|
||||||
|
|
||||||
|
expect(mapping).toEqual({
|
||||||
|
'gpt-5.4-2026-03-05': 'gpt-5.4-2026-03-05'
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const openaiModels = [
|
|||||||
// GPT-5.2 系列
|
// GPT-5.2 系列
|
||||||
'gpt-5.2', 'gpt-5.2-2025-12-11', 'gpt-5.2-chat-latest',
|
'gpt-5.2', 'gpt-5.2-2025-12-11', 'gpt-5.2-chat-latest',
|
||||||
'gpt-5.2-codex', 'gpt-5.2-pro', 'gpt-5.2-pro-2025-12-11',
|
'gpt-5.2-codex', 'gpt-5.2-pro', 'gpt-5.2-pro-2025-12-11',
|
||||||
|
// GPT-5.4 系列
|
||||||
|
'gpt-5.4', 'gpt-5.4-2026-03-05',
|
||||||
// GPT-5.3 系列
|
// GPT-5.3 系列
|
||||||
'gpt-5.3-codex', 'gpt-5.3-codex-spark',
|
'gpt-5.3-codex', 'gpt-5.3-codex-spark',
|
||||||
'chatgpt-4o-latest',
|
'chatgpt-4o-latest',
|
||||||
@@ -277,6 +279,7 @@ const openaiPresetMappings = [
|
|||||||
{ label: 'GPT-5.3 Codex Spark', from: 'gpt-5.3-codex-spark', to: 'gpt-5.3-codex-spark', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' },
|
{ label: 'GPT-5.3 Codex Spark', from: 'gpt-5.3-codex-spark', to: 'gpt-5.3-codex-spark', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' },
|
||||||
{ label: 'GPT-5.1', from: 'gpt-5.1', to: 'gpt-5.1', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
|
{ label: 'GPT-5.1', from: 'gpt-5.1', to: 'gpt-5.1', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },
|
||||||
{ label: 'GPT-5.2', from: 'gpt-5.2', to: 'gpt-5.2', color: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' },
|
{ label: 'GPT-5.2', from: 'gpt-5.2', to: 'gpt-5.2', color: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' },
|
||||||
|
{ label: 'GPT-5.4', from: 'gpt-5.4', to: 'gpt-5.4', color: 'bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400' },
|
||||||
{ label: 'GPT-5.1 Codex', from: 'gpt-5.1-codex', to: 'gpt-5.1-codex', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }
|
{ label: 'GPT-5.1 Codex', from: 'gpt-5.1-codex', to: 'gpt-5.1-codex', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1734,6 +1734,10 @@ export default {
|
|||||||
stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit',
|
stickyExemptWarning: 'RPM limit (Sticky Exempt) - Approaching limit',
|
||||||
stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only'
|
stickyExemptOver: 'RPM limit (Sticky Exempt) - Over limit, sticky only'
|
||||||
},
|
},
|
||||||
|
quota: {
|
||||||
|
exceeded: 'Quota exceeded, account paused',
|
||||||
|
normal: 'Quota normal'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tempUnschedulable: {
|
tempUnschedulable: {
|
||||||
title: 'Temp Unschedulable',
|
title: 'Temp Unschedulable',
|
||||||
@@ -1779,6 +1783,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearRateLimit: 'Clear Rate Limit',
|
clearRateLimit: 'Clear Rate Limit',
|
||||||
|
resetQuota: 'Reset Quota',
|
||||||
|
quotaLimit: 'Quota Limit',
|
||||||
|
quotaLimitPlaceholder: '0 means unlimited',
|
||||||
|
quotaLimitHint: 'Set max spending limit (USD). Account will be paused when reached. Changing limit won\'t reset usage.',
|
||||||
|
quotaLimitToggle: 'Enable Quota Limit',
|
||||||
|
quotaLimitToggleHint: 'When enabled, account will be paused when usage reaches the set limit',
|
||||||
|
quotaLimitAmount: 'Limit Amount',
|
||||||
|
quotaLimitAmountHint: 'Maximum spending limit (USD). Account will be auto-paused when reached. Changing limit won\'t reset usage.',
|
||||||
testConnection: 'Test Connection',
|
testConnection: 'Test Connection',
|
||||||
reAuthorize: 'Re-Authorize',
|
reAuthorize: 'Re-Authorize',
|
||||||
refreshToken: 'Refresh Token',
|
refreshToken: 'Refresh Token',
|
||||||
@@ -1991,10 +2003,12 @@ export default {
|
|||||||
proxy: 'Proxy',
|
proxy: 'Proxy',
|
||||||
noProxy: 'No Proxy',
|
noProxy: 'No Proxy',
|
||||||
concurrency: 'Concurrency',
|
concurrency: 'Concurrency',
|
||||||
|
loadFactor: 'Load Factor',
|
||||||
|
loadFactorHint: 'Higher load factor increases scheduling frequency',
|
||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
priorityHint: 'Lower value accounts are used first',
|
priorityHint: 'Lower value accounts are used first',
|
||||||
billingRateMultiplier: 'Billing Rate Multiplier',
|
billingRateMultiplier: 'Billing Rate Multiplier',
|
||||||
billingRateMultiplierHint: '>=0, 0 means free. Affects account billing only',
|
billingRateMultiplierHint: '0 = free, affects account billing only',
|
||||||
expiresAt: 'Expires At',
|
expiresAt: 'Expires At',
|
||||||
expiresAtHint: 'Leave empty for no expiration',
|
expiresAtHint: 'Leave empty for no expiration',
|
||||||
higherPriorityFirst: 'Lower value means higher priority',
|
higherPriorityFirst: 'Lower value means higher priority',
|
||||||
@@ -2010,6 +2024,7 @@ export default {
|
|||||||
accountUpdated: 'Account updated successfully',
|
accountUpdated: 'Account updated successfully',
|
||||||
failedToCreate: 'Failed to create account',
|
failedToCreate: 'Failed to create account',
|
||||||
failedToUpdate: 'Failed to update account',
|
failedToUpdate: 'Failed to update account',
|
||||||
|
pleaseSelectStatus: 'Please select a valid account status',
|
||||||
mixedChannelWarningTitle: 'Mixed Channel Warning',
|
mixedChannelWarningTitle: 'Mixed Channel Warning',
|
||||||
mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?',
|
mixedChannelWarning: 'Warning: Group "{groupName}" contains both {currentPlatform} and {otherPlatform} accounts. Mixing different channels may cause thinking block signature validation issues, which will fallback to non-thinking mode. Are you sure you want to continue?',
|
||||||
pleaseEnterAccountName: 'Please enter account name',
|
pleaseEnterAccountName: 'Please enter account name',
|
||||||
|
|||||||
@@ -1784,8 +1784,20 @@ export default {
|
|||||||
stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值',
|
stickyExemptWarning: 'RPM 限制 (粘性豁免) - 接近阈值',
|
||||||
stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话'
|
stickyExemptOver: 'RPM 限制 (粘性豁免) - 超限,仅粘性会话'
|
||||||
},
|
},
|
||||||
|
quota: {
|
||||||
|
exceeded: '配额已用完,账号暂停调度',
|
||||||
|
normal: '配额正常'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
clearRateLimit: '清除速率限制',
|
clearRateLimit: '清除速率限制',
|
||||||
|
resetQuota: '重置配额',
|
||||||
|
quotaLimit: '配额限制',
|
||||||
|
quotaLimitPlaceholder: '0 表示不限制',
|
||||||
|
quotaLimitHint: '设置最大使用额度(美元),达到后账号暂停调度。修改限额不会重置已用额度。',
|
||||||
|
quotaLimitToggle: '启用配额限制',
|
||||||
|
quotaLimitToggleHint: '开启后,当账号用量达到设定额度时自动暂停调度',
|
||||||
|
quotaLimitAmount: '限额金额',
|
||||||
|
quotaLimitAmountHint: '账号最大可用额度(美元),达到后自动暂停。修改限额不会重置已用额度。',
|
||||||
testConnection: '测试连接',
|
testConnection: '测试连接',
|
||||||
reAuthorize: '重新授权',
|
reAuthorize: '重新授权',
|
||||||
refreshToken: '刷新令牌',
|
refreshToken: '刷新令牌',
|
||||||
@@ -2133,10 +2145,12 @@ export default {
|
|||||||
proxy: '代理',
|
proxy: '代理',
|
||||||
noProxy: '无代理',
|
noProxy: '无代理',
|
||||||
concurrency: '并发数',
|
concurrency: '并发数',
|
||||||
|
loadFactor: '负载因子',
|
||||||
|
loadFactorHint: '提高负载因子可以提高对账号的调度频率',
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
priorityHint: '优先级越小的账号优先使用',
|
priorityHint: '优先级越小的账号优先使用',
|
||||||
billingRateMultiplier: '账号计费倍率',
|
billingRateMultiplier: '账号计费倍率',
|
||||||
billingRateMultiplierHint: '>=0,0 表示该账号计费为 0;仅影响账号计费口径',
|
billingRateMultiplierHint: '0 表示不计费,仅影响账号计费',
|
||||||
expiresAt: '过期时间',
|
expiresAt: '过期时间',
|
||||||
expiresAtHint: '留空表示不过期',
|
expiresAtHint: '留空表示不过期',
|
||||||
higherPriorityFirst: '数值越小优先级越高',
|
higherPriorityFirst: '数值越小优先级越高',
|
||||||
@@ -2152,6 +2166,7 @@ export default {
|
|||||||
accountUpdated: '账号更新成功',
|
accountUpdated: '账号更新成功',
|
||||||
failedToCreate: '创建账号失败',
|
failedToCreate: '创建账号失败',
|
||||||
failedToUpdate: '更新账号失败',
|
failedToUpdate: '更新账号失败',
|
||||||
|
pleaseSelectStatus: '请选择有效的账号状态',
|
||||||
mixedChannelWarningTitle: '混合渠道警告',
|
mixedChannelWarningTitle: '混合渠道警告',
|
||||||
mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?',
|
mixedChannelWarning: '警告:分组 "{groupName}" 中同时包含 {currentPlatform} 和 {otherPlatform} 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会自动回退到非 thinking 模式。确定要继续吗?',
|
||||||
pleaseEnterAccountName: '请输入账号名称',
|
pleaseEnterAccountName: '请输入账号名称',
|
||||||
|
|||||||
@@ -653,6 +653,7 @@ export interface Account {
|
|||||||
} & Record<string, unknown>)
|
} & Record<string, unknown>)
|
||||||
proxy_id: number | null
|
proxy_id: number | null
|
||||||
concurrency: number
|
concurrency: number
|
||||||
|
load_factor?: number | null
|
||||||
current_concurrency?: number // Real-time concurrency count from Redis
|
current_concurrency?: number // Real-time concurrency count from Redis
|
||||||
priority: number
|
priority: number
|
||||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||||
@@ -705,6 +706,10 @@ export interface Account {
|
|||||||
cache_ttl_override_enabled?: boolean | null
|
cache_ttl_override_enabled?: boolean | null
|
||||||
cache_ttl_override_target?: string | null
|
cache_ttl_override_target?: string | null
|
||||||
|
|
||||||
|
// API Key 账号配额限制
|
||||||
|
quota_limit?: number | null
|
||||||
|
quota_used?: number | null
|
||||||
|
|
||||||
// 运行时状态(仅当启用对应限制时返回)
|
// 运行时状态(仅当启用对应限制时返回)
|
||||||
current_window_cost?: number | null // 当前窗口费用
|
current_window_cost?: number | null // 当前窗口费用
|
||||||
active_sessions?: number | null // 当前活跃会话数
|
active_sessions?: number | null // 当前活跃会话数
|
||||||
@@ -783,6 +788,7 @@ export interface CreateAccountRequest {
|
|||||||
extra?: Record<string, unknown>
|
extra?: Record<string, unknown>
|
||||||
proxy_id?: number | null
|
proxy_id?: number | null
|
||||||
concurrency?: number
|
concurrency?: number
|
||||||
|
load_factor?: number | null
|
||||||
priority?: number
|
priority?: number
|
||||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
@@ -799,6 +805,7 @@ export interface UpdateAccountRequest {
|
|||||||
extra?: Record<string, unknown>
|
extra?: Record<string, unknown>
|
||||||
proxy_id?: number | null
|
proxy_id?: number | null
|
||||||
concurrency?: number
|
concurrency?: number
|
||||||
|
load_factor?: number | null
|
||||||
priority?: number
|
priority?: number
|
||||||
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
|
||||||
schedulable?: boolean
|
schedulable?: boolean
|
||||||
|
|||||||
@@ -261,7 +261,7 @@
|
|||||||
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
||||||
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
||||||
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
||||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" />
|
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @reauth="handleReAuth" @refresh-token="handleRefresh" @reset-status="handleResetStatus" @clear-rate-limit="handleClearRateLimit" @reset-quota="handleResetQuota" />
|
||||||
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
||||||
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
||||||
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
|
<BulkEditAccountModal :show="showBulkEdit" :account-ids="selIds" :selected-platforms="selPlatforms" :selected-types="selTypes" :proxies="proxies" :groups="groups" @close="showBulkEdit = false" @updated="handleBulkUpdated" />
|
||||||
@@ -1125,6 +1125,16 @@ const handleClearRateLimit = async (a: Account) => {
|
|||||||
console.error('Failed to clear rate limit:', error)
|
console.error('Failed to clear rate limit:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const handleResetQuota = async (a: Account) => {
|
||||||
|
try {
|
||||||
|
const updated = await adminAPI.accounts.resetAccountQuota(a.id)
|
||||||
|
patchAccountInList(updated)
|
||||||
|
enterAutoRefreshSilentWindow()
|
||||||
|
appStore.showSuccess(t('common.success'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reset quota:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
|
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
|
||||||
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
|
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
|
||||||
const handleToggleSchedulable = async (a: Account) => {
|
const handleToggleSchedulable = async (a: Account) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user