feat(groups): add deny-or-downgrade control for reasoning effort ceiling

OpenAI/Composite groups can now reject requests that exceed the max
reasoning effort instead of always rewriting them down to the ceiling.
Existing groups keep the previous auto-downgrade behavior.
This commit is contained in:
刘佳林
2026-09-01 10:15:45 +08:00
parent 2ac784c51a
commit aa7a811e66
47 changed files with 751 additions and 105 deletions
+12 -1
View File
@@ -136,6 +136,8 @@ type Group struct {
RpmLimit int `json:"rpm_limit,omitempty"`
// OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
// 超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit,omitempty"`
// OpenAI reasoning effort 自定义精确映射;先映射再应用上限
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings,omitempty"`
// 是否启用利润控制:调度时仅允许账号计费倍率满足毛利率要求的账号进入候选池
@@ -258,7 +260,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
values[i] = new(sql.NullFloat64)
case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit:
values[i] = new(sql.NullInt64)
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort:
case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldDuplicateOperationID, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel, group.FieldMaxReasoningEffort, group.FieldMaxReasoningEffortOverLimit:
values[i] = new(sql.NullString)
case group.FieldCreatedAt, group.FieldUpdatedAt, group.FieldDeletedAt:
values[i] = new(sql.NullTime)
@@ -662,6 +664,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.MaxReasoningEffort = value.String
}
case group.FieldMaxReasoningEffortOverLimit:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field max_reasoning_effort_over_limit", values[i])
} else if value.Valid {
_m.MaxReasoningEffortOverLimit = value.String
}
case group.FieldReasoningEffortMappings:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field reasoning_effort_mappings", values[i])
@@ -976,6 +984,9 @@ func (_m *Group) String() string {
builder.WriteString("max_reasoning_effort=")
builder.WriteString(_m.MaxReasoningEffort)
builder.WriteString(", ")
builder.WriteString("max_reasoning_effort_over_limit=")
builder.WriteString(_m.MaxReasoningEffortOverLimit)
builder.WriteString(", ")
builder.WriteString("reasoning_effort_mappings=")
builder.WriteString(fmt.Sprintf("%v", _m.ReasoningEffortMappings))
builder.WriteString(", ")
+12
View File
@@ -132,6 +132,8 @@ const (
FieldRpmLimit = "rpm_limit"
// FieldMaxReasoningEffort holds the string denoting the max_reasoning_effort field in the database.
FieldMaxReasoningEffort = "max_reasoning_effort"
// FieldMaxReasoningEffortOverLimit holds the string denoting the max_reasoning_effort_over_limit field in the database.
FieldMaxReasoningEffortOverLimit = "max_reasoning_effort_over_limit"
// FieldReasoningEffortMappings holds the string denoting the reasoning_effort_mappings field in the database.
FieldReasoningEffortMappings = "reasoning_effort_mappings"
// FieldProfitControlEnabled holds the string denoting the profit_control_enabled field in the database.
@@ -273,6 +275,7 @@ var Columns = []string{
FieldModelsListConfig,
FieldRpmLimit,
FieldMaxReasoningEffort,
FieldMaxReasoningEffortOverLimit,
FieldReasoningEffortMappings,
FieldProfitControlEnabled,
FieldProfitMinMargin,
@@ -404,6 +407,10 @@ var (
DefaultMaxReasoningEffort string
// MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
MaxReasoningEffortValidator func(string) error
// DefaultMaxReasoningEffortOverLimit holds the default value on creation for the "max_reasoning_effort_over_limit" field.
DefaultMaxReasoningEffortOverLimit string
// MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
MaxReasoningEffortOverLimitValidator func(string) error
// DefaultReasoningEffortMappings holds the default value on creation for the "reasoning_effort_mappings" field.
DefaultReasoningEffortMappings []domain.ReasoningEffortMapping
// DefaultProfitControlEnabled holds the default value on creation for the "profit_control_enabled" field.
@@ -682,6 +689,11 @@ func ByMaxReasoningEffort(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMaxReasoningEffort, opts...).ToFunc()
}
// ByMaxReasoningEffortOverLimit orders the results by the max_reasoning_effort_over_limit field.
func ByMaxReasoningEffortOverLimit(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldMaxReasoningEffortOverLimit, opts...).ToFunc()
}
// ByProfitControlEnabled orders the results by the profit_control_enabled field.
func ByProfitControlEnabled(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldProfitControlEnabled, opts...).ToFunc()
+70
View File
@@ -315,6 +315,11 @@ func MaxReasoningEffort(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffort, v))
}
// MaxReasoningEffortOverLimit applies equality check predicate on the "max_reasoning_effort_over_limit" field. It's identical to MaxReasoningEffortOverLimitEQ.
func MaxReasoningEffortOverLimit(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
}
// ProfitControlEnabled applies equality check predicate on the "profit_control_enabled" field. It's identical to ProfitControlEnabledEQ.
func ProfitControlEnabled(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
@@ -2460,6 +2465,71 @@ func MaxReasoningEffortContainsFold(v string) predicate.Group {
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffort, v))
}
// MaxReasoningEffortOverLimitEQ applies the EQ predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitEQ(v string) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitNEQ applies the NEQ predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitNEQ(v string) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitIn applies the In predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldIn(FieldMaxReasoningEffortOverLimit, vs...))
}
// MaxReasoningEffortOverLimitNotIn applies the NotIn predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitNotIn(vs ...string) predicate.Group {
return predicate.Group(sql.FieldNotIn(FieldMaxReasoningEffortOverLimit, vs...))
}
// MaxReasoningEffortOverLimitGT applies the GT predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitGT(v string) predicate.Group {
return predicate.Group(sql.FieldGT(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitGTE applies the GTE predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitGTE(v string) predicate.Group {
return predicate.Group(sql.FieldGTE(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitLT applies the LT predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitLT(v string) predicate.Group {
return predicate.Group(sql.FieldLT(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitLTE applies the LTE predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitLTE(v string) predicate.Group {
return predicate.Group(sql.FieldLTE(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitContains applies the Contains predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitContains(v string) predicate.Group {
return predicate.Group(sql.FieldContains(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitHasPrefix applies the HasPrefix predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitHasPrefix(v string) predicate.Group {
return predicate.Group(sql.FieldHasPrefix(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitHasSuffix applies the HasSuffix predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitHasSuffix(v string) predicate.Group {
return predicate.Group(sql.FieldHasSuffix(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitEqualFold applies the EqualFold predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitEqualFold(v string) predicate.Group {
return predicate.Group(sql.FieldEqualFold(FieldMaxReasoningEffortOverLimit, v))
}
// MaxReasoningEffortOverLimitContainsFold applies the ContainsFold predicate on the "max_reasoning_effort_over_limit" field.
func MaxReasoningEffortOverLimitContainsFold(v string) predicate.Group {
return predicate.Group(sql.FieldContainsFold(FieldMaxReasoningEffortOverLimit, v))
}
// ProfitControlEnabledEQ applies the EQ predicate on the "profit_control_enabled" field.
func ProfitControlEnabledEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldProfitControlEnabled, v))
+70
View File
@@ -802,6 +802,20 @@ func (_c *GroupCreate) SetNillableMaxReasoningEffort(v *string) *GroupCreate {
return _c
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_c *GroupCreate) SetMaxReasoningEffortOverLimit(v string) *GroupCreate {
_c.mutation.SetMaxReasoningEffortOverLimit(v)
return _c
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_c *GroupCreate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupCreate {
if v != nil {
_c.SetMaxReasoningEffortOverLimit(*v)
}
return _c
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_c *GroupCreate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupCreate {
_c.mutation.SetReasoningEffortMappings(v)
@@ -1123,6 +1137,10 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultMaxReasoningEffort
_c.mutation.SetMaxReasoningEffort(v)
}
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
v := group.DefaultMaxReasoningEffortOverLimit
_c.mutation.SetMaxReasoningEffortOverLimit(v)
}
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
v := group.DefaultReasoningEffortMappings
_c.mutation.SetReasoningEffortMappings(v)
@@ -1317,6 +1335,14 @@ func (_c *GroupCreate) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if _, ok := _c.mutation.MaxReasoningEffortOverLimit(); !ok {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: errors.New(`ent: missing required field "Group.max_reasoning_effort_over_limit"`)}
}
if v, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
if _, ok := _c.mutation.ReasoningEffortMappings(); !ok {
return &ValidationError{Name: "reasoning_effort_mappings", err: errors.New(`ent: missing required field "Group.reasoning_effort_mappings"`)}
}
@@ -1588,6 +1614,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
_node.MaxReasoningEffort = value
}
if value, ok := _c.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
_node.MaxReasoningEffortOverLimit = value
}
if value, ok := _c.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
_node.ReasoningEffortMappings = value
@@ -2708,6 +2738,18 @@ func (u *GroupUpsert) UpdateMaxReasoningEffort() *GroupUpsert {
return u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsert) SetMaxReasoningEffortOverLimit(v string) *GroupUpsert {
u.Set(group.FieldMaxReasoningEffortOverLimit, v)
return u
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsert) UpdateMaxReasoningEffortOverLimit() *GroupUpsert {
u.SetExcluded(group.FieldMaxReasoningEffortOverLimit)
return u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsert) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsert {
u.Set(group.FieldReasoningEffortMappings, v)
@@ -3922,6 +3964,20 @@ func (u *GroupUpsertOne) UpdateMaxReasoningEffort() *GroupUpsertOne {
})
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsertOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetMaxReasoningEffortOverLimit(v)
})
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateMaxReasoningEffortOverLimit() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateMaxReasoningEffortOverLimit()
})
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsertOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
@@ -5312,6 +5368,20 @@ func (u *GroupUpsertBulk) UpdateMaxReasoningEffort() *GroupUpsertBulk {
})
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (u *GroupUpsertBulk) SetMaxReasoningEffortOverLimit(v string) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetMaxReasoningEffortOverLimit(v)
})
}
// UpdateMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateMaxReasoningEffortOverLimit() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateMaxReasoningEffortOverLimit()
})
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (u *GroupUpsertBulk) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
+44
View File
@@ -1094,6 +1094,20 @@ func (_u *GroupUpdate) SetNillableMaxReasoningEffort(v *string) *GroupUpdate {
return _u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_u *GroupUpdate) SetMaxReasoningEffortOverLimit(v string) *GroupUpdate {
_u.mutation.SetMaxReasoningEffortOverLimit(v)
return _u
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdate {
if v != nil {
_u.SetMaxReasoningEffortOverLimit(*v)
}
return _u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_u *GroupUpdate) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdate {
_u.mutation.SetReasoningEffortMappings(v)
@@ -1487,6 +1501,11 @@ func (_u *GroupUpdate) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
return nil
}
@@ -1821,6 +1840,9 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
}
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
}
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
}
@@ -3215,6 +3237,20 @@ func (_u *GroupUpdateOne) SetNillableMaxReasoningEffort(v *string) *GroupUpdateO
return _u
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (_u *GroupUpdateOne) SetMaxReasoningEffortOverLimit(v string) *GroupUpdateOne {
_u.mutation.SetMaxReasoningEffortOverLimit(v)
return _u
}
// SetNillableMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableMaxReasoningEffortOverLimit(v *string) *GroupUpdateOne {
if v != nil {
_u.SetMaxReasoningEffortOverLimit(*v)
}
return _u
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (_u *GroupUpdateOne) SetReasoningEffortMappings(v []domain.ReasoningEffortMapping) *GroupUpdateOne {
_u.mutation.SetReasoningEffortMappings(v)
@@ -3621,6 +3657,11 @@ func (_u *GroupUpdateOne) check() error {
return &ValidationError{Name: "max_reasoning_effort", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort": %w`, err)}
}
}
if v, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
if err := group.MaxReasoningEffortOverLimitValidator(v); err != nil {
return &ValidationError{Name: "max_reasoning_effort_over_limit", err: fmt.Errorf(`ent: validator failed for field "Group.max_reasoning_effort_over_limit": %w`, err)}
}
}
return nil
}
@@ -3972,6 +4013,9 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.MaxReasoningEffort(); ok {
_spec.SetField(group.FieldMaxReasoningEffort, field.TypeString, value)
}
if value, ok := _u.mutation.MaxReasoningEffortOverLimit(); ok {
_spec.SetField(group.FieldMaxReasoningEffortOverLimit, field.TypeString, value)
}
if value, ok := _u.mutation.ReasoningEffortMappings(); ok {
_spec.SetField(group.FieldReasoningEffortMappings, field.TypeJSON, value)
}
+1
View File
@@ -961,6 +961,7 @@ var (
{Name: "models_list_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "rpm_limit", Type: field.TypeInt, Default: 0},
{Name: "max_reasoning_effort", Type: field.TypeString, Size: 20, Default: ""},
{Name: "max_reasoning_effort_over_limit", Type: field.TypeString, Size: 20, Default: "downgrade"},
{Name: "reasoning_effort_mappings", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "profit_control_enabled", Type: field.TypeBool, Default: false},
{Name: "profit_min_margin", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
+55 -1
View File
@@ -22166,6 +22166,7 @@ type GroupMutation struct {
rpm_limit *int
addrpm_limit *int
max_reasoning_effort *string
max_reasoning_effort_over_limit *string
reasoning_effort_mappings *[]domain.ReasoningEffortMapping
appendreasoning_effort_mappings []domain.ReasoningEffortMapping
profit_control_enabled *bool
@@ -25216,6 +25217,42 @@ func (m *GroupMutation) ResetMaxReasoningEffort() {
m.max_reasoning_effort = nil
}
// SetMaxReasoningEffortOverLimit sets the "max_reasoning_effort_over_limit" field.
func (m *GroupMutation) SetMaxReasoningEffortOverLimit(s string) {
m.max_reasoning_effort_over_limit = &s
}
// MaxReasoningEffortOverLimit returns the value of the "max_reasoning_effort_over_limit" field in the mutation.
func (m *GroupMutation) MaxReasoningEffortOverLimit() (r string, exists bool) {
v := m.max_reasoning_effort_over_limit
if v == nil {
return
}
return *v, true
}
// OldMaxReasoningEffortOverLimit returns the old "max_reasoning_effort_over_limit" field's value of the Group entity.
// If the Group 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 *GroupMutation) OldMaxReasoningEffortOverLimit(ctx context.Context) (v string, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldMaxReasoningEffortOverLimit is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldMaxReasoningEffortOverLimit requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldMaxReasoningEffortOverLimit: %w", err)
}
return oldValue.MaxReasoningEffortOverLimit, nil
}
// ResetMaxReasoningEffortOverLimit resets all changes to the "max_reasoning_effort_over_limit" field.
func (m *GroupMutation) ResetMaxReasoningEffortOverLimit() {
m.max_reasoning_effort_over_limit = nil
}
// SetReasoningEffortMappings sets the "reasoning_effort_mappings" field.
func (m *GroupMutation) SetReasoningEffortMappings(dem []domain.ReasoningEffortMapping) {
m.reasoning_effort_mappings = &dem
@@ -25773,7 +25810,7 @@ func (m *GroupMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *GroupMutation) Fields() []string {
fields := make([]string, 0, 62)
fields := make([]string, 0, 63)
if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt)
}
@@ -25948,6 +25985,9 @@ func (m *GroupMutation) Fields() []string {
if m.max_reasoning_effort != nil {
fields = append(fields, group.FieldMaxReasoningEffort)
}
if m.max_reasoning_effort_over_limit != nil {
fields = append(fields, group.FieldMaxReasoningEffortOverLimit)
}
if m.reasoning_effort_mappings != nil {
fields = append(fields, group.FieldReasoningEffortMappings)
}
@@ -26084,6 +26124,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
return m.RpmLimit()
case group.FieldMaxReasoningEffort:
return m.MaxReasoningEffort()
case group.FieldMaxReasoningEffortOverLimit:
return m.MaxReasoningEffortOverLimit()
case group.FieldReasoningEffortMappings:
return m.ReasoningEffortMappings()
case group.FieldProfitControlEnabled:
@@ -26217,6 +26259,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldRpmLimit(ctx)
case group.FieldMaxReasoningEffort:
return m.OldMaxReasoningEffort(ctx)
case group.FieldMaxReasoningEffortOverLimit:
return m.OldMaxReasoningEffortOverLimit(ctx)
case group.FieldReasoningEffortMappings:
return m.OldReasoningEffortMappings(ctx)
case group.FieldProfitControlEnabled:
@@ -26640,6 +26684,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
}
m.SetMaxReasoningEffort(v)
return nil
case group.FieldMaxReasoningEffortOverLimit:
v, ok := value.(string)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetMaxReasoningEffortOverLimit(v)
return nil
case group.FieldReasoningEffortMappings:
v, ok := value.([]domain.ReasoningEffortMapping)
if !ok {
@@ -27353,6 +27404,9 @@ func (m *GroupMutation) ResetField(name string) error {
case group.FieldMaxReasoningEffort:
m.ResetMaxReasoningEffort()
return nil
case group.FieldMaxReasoningEffortOverLimit:
m.ResetMaxReasoningEffortOverLimit()
return nil
case group.FieldReasoningEffortMappings:
m.ResetReasoningEffortMappings()
return nil
+10 -4
View File
@@ -1189,20 +1189,26 @@ func init() {
group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string)
// group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
group.MaxReasoningEffortValidator = groupDescMaxReasoningEffort.Validators[0].(func(string) error)
// groupDescMaxReasoningEffortOverLimit is the schema descriptor for max_reasoning_effort_over_limit field.
groupDescMaxReasoningEffortOverLimit := groupFields[55].Descriptor()
// group.DefaultMaxReasoningEffortOverLimit holds the default value on creation for the max_reasoning_effort_over_limit field.
group.DefaultMaxReasoningEffortOverLimit = groupDescMaxReasoningEffortOverLimit.Default.(string)
// group.MaxReasoningEffortOverLimitValidator is a validator for the "max_reasoning_effort_over_limit" field. It is called by the builders before save.
group.MaxReasoningEffortOverLimitValidator = groupDescMaxReasoningEffortOverLimit.Validators[0].(func(string) error)
// groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field.
groupDescReasoningEffortMappings := groupFields[55].Descriptor()
groupDescReasoningEffortMappings := groupFields[56].Descriptor()
// group.DefaultReasoningEffortMappings holds the default value on creation for the reasoning_effort_mappings field.
group.DefaultReasoningEffortMappings = groupDescReasoningEffortMappings.Default.([]domain.ReasoningEffortMapping)
// groupDescProfitControlEnabled is the schema descriptor for profit_control_enabled field.
groupDescProfitControlEnabled := groupFields[56].Descriptor()
groupDescProfitControlEnabled := groupFields[57].Descriptor()
// group.DefaultProfitControlEnabled holds the default value on creation for the profit_control_enabled field.
group.DefaultProfitControlEnabled = groupDescProfitControlEnabled.Default.(bool)
// groupDescProfitMinMargin is the schema descriptor for profit_min_margin field.
groupDescProfitMinMargin := groupFields[57].Descriptor()
groupDescProfitMinMargin := groupFields[58].Descriptor()
// group.DefaultProfitMinMargin holds the default value on creation for the profit_min_margin field.
group.DefaultProfitMinMargin = groupDescProfitMinMargin.Default.(float64)
// groupDescProfitSafetyBuffer is the schema descriptor for profit_safety_buffer field.
groupDescProfitSafetyBuffer := groupFields[58].Descriptor()
groupDescProfitSafetyBuffer := groupFields[59].Descriptor()
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
+4
View File
@@ -271,6 +271,10 @@ func (Group) Fields() []ent.Field {
MaxLen(20).
Default("").
Comment("OpenAI reasoning effort 上限;可选 minimal/low/medium/high/xhigh/max"),
field.String("max_reasoning_effort_over_limit").
MaxLen(20).
Default("downgrade").
Comment("超过推理强度上限时的访问控制:downgrade 自动降档,deny 拒绝访问"),
field.JSON("reasoning_effort_mappings", []domain.ReasoningEffortMapping{}).
Default([]domain.ReasoningEffortMapping{}).
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
@@ -156,6 +156,8 @@ type CreateGroupRequest struct {
RPMLimit int `json:"rpm_limit"`
// OpenAI/Codex 请求推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort"`
// 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit"`
// OpenAI/Codex 推理强度精确映射。
ReasoningEffortMappings []service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
// 从指定分组复制账号(创建后自动绑定)
@@ -225,6 +227,8 @@ type UpdateGroupRequest struct {
RPMLimit *int `json:"rpm_limit"`
// OpenAI/Codex 请求推理强度上限;空字符串清除,nil 不修改。
MaxReasoningEffort *string `json:"max_reasoning_effort"`
// 超过上限时的访问控制;空字符串视为 downgrade,nil 不修改。
MaxReasoningEffortOverLimit *string `json:"max_reasoning_effort_over_limit"`
// nil 不修改,空数组清空,非空数组替换。
ReasoningEffortMappings *[]service.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
// 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
@@ -557,6 +561,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
ModelsListConfig: req.ModelsListConfig,
RPMLimit: req.RPMLimit,
MaxReasoningEffort: req.MaxReasoningEffort,
MaxReasoningEffortOverLimit: req.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: req.ReasoningEffortMappings,
CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
})
@@ -686,6 +691,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
ModelsListConfig: req.ModelsListConfig,
RPMLimit: req.RPMLimit,
MaxReasoningEffort: req.MaxReasoningEffort,
MaxReasoningEffortOverLimit: req.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: req.ReasoningEffortMappings,
CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs,
})
+20 -11
View File
@@ -1,6 +1,7 @@
package handler
import (
"net/http"
"strings"
"github.com/Wei-Shaw/sub2api/internal/service"
@@ -59,17 +60,17 @@ func effectiveAPIKeyPlatform(c *gin.Context, apiKey *service.APIKey) string {
return apiKey.Group.Platform
}
func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, bool) {
func openAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey) (string, []service.ReasoningEffortMapping, string, bool) {
if apiKey == nil || apiKey.Group == nil {
return "", nil, false
return "", nil, "", false
}
if apiKey.Group.Platform != service.PlatformOpenAI && apiKey.Group.Platform != service.PlatformComposite {
return "", nil, false
return "", nil, "", false
}
if effectiveAPIKeyPlatform(c, apiKey) != service.PlatformOpenAI {
return "", nil, false
return "", nil, "", false
}
return apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings, true
return apiKey.Group.MaxReasoningEffort, apiKey.Group.ReasoningEffortMappings, apiKey.Group.MaxReasoningEffortOverLimit, true
}
func bindRequestedReasoningEffort(c *gin.Context, body []byte, model string) {
@@ -100,13 +101,21 @@ func stampForwardRequestedReasoningEffort(result *service.ForwardResult, request
result.RequestedReasoningEffort = requested
}
func applyOpenAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool) {
func applyOpenAIReasoningEffortPolicyForRequest(c *gin.Context, apiKey *service.APIKey, body []byte) ([]byte, bool, error) {
bindRequestedReasoningEffort(c, body, strings.TrimSpace(gjson.GetBytes(body, "model").String()))
maxEffort, mappings, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
if !ok {
return body, false
return body, false, nil
}
return service.ApplyOpenAIReasoningEffortPolicy(body, maxEffort, mappings)
return service.ApplyOpenAIReasoningEffortPolicy(body, maxEffort, mappings, overLimit)
}
func respondOpenAIReasoningEffortPolicyError(c *gin.Context, err error, write func(*gin.Context, int, string, string)) {
if c == nil || err == nil || write == nil {
return
}
service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalPolicyDenied)
write(c, http.StatusForbidden, "permission_error", err.Error())
}
func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *service.APIKey, body []byte) {
@@ -121,9 +130,9 @@ func bindOpenAIReasoningEffortPolicyForMessagesRequest(c *gin.Context, apiKey *s
if !effort.Exists() || effort.Type != gjson.String || strings.TrimSpace(effort.String()) == "" {
return
}
maxEffort, mappings, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
maxEffort, mappings, overLimit, ok := openAIReasoningEffortPolicyForRequest(c, apiKey)
if !ok {
return
}
c.Request = c.Request.WithContext(service.WithOpenAIReasoningEffortPolicy(c.Request.Context(), maxEffort, mappings))
c.Request = c.Request.WithContext(service.WithOpenAIReasoningEffortPolicy(c.Request.Context(), maxEffort, mappings, overLimit))
}
@@ -117,7 +117,8 @@ func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) {
openAICtx, _ := gin.CreateTestContext(httptest.NewRecorder())
openAICtx.Request = httptest.NewRequest("POST", "/v1/responses", nil)
openAICtx.Request = openAICtx.Request.WithContext(service.WithResolvedTargetPlatform(openAICtx.Request.Context(), service.PlatformOpenAI))
got, changed := applyOpenAIReasoningEffortPolicyForRequest(openAICtx, apiKey, body)
got, changed, err := applyOpenAIReasoningEffortPolicyForRequest(openAICtx, apiKey, body)
require.NoError(t, err)
require.True(t, changed)
require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(got))
requested := service.RequestedReasoningEffortFromContext(openAICtx.Request.Context())
@@ -125,7 +126,8 @@ func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) {
require.Equal(t, "max", *requested)
bindOpenAIReasoningEffortPolicyForMessagesRequest(openAICtx, apiKey, []byte(`{"output_config":{"effort":"max"}}`))
bound, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(openAICtx.Request.Context(), body)
bound, changed, err := service.ApplyOpenAIReasoningEffortPolicyFromContext(openAICtx.Request.Context(), body)
require.NoError(t, err)
require.True(t, changed)
require.JSONEq(t, `{"reasoning":{"effort":"medium"}}`, string(bound))
@@ -133,14 +135,27 @@ func TestOpenAIReasoningEffortPolicyForCompositeTarget(t *testing.T) {
omittedCtx.Request = httptest.NewRequest("POST", "/v1/messages", nil)
omittedCtx.Request = omittedCtx.Request.WithContext(service.WithResolvedTargetPlatform(omittedCtx.Request.Context(), service.PlatformOpenAI))
bindOpenAIReasoningEffortPolicyForMessagesRequest(omittedCtx, apiKey, []byte(`{"model":"gpt-5"}`))
omitted, changed := service.ApplyOpenAIReasoningEffortPolicyFromContext(omittedCtx.Request.Context(), body)
omitted, changed, err := service.ApplyOpenAIReasoningEffortPolicyFromContext(omittedCtx.Request.Context(), body)
require.NoError(t, err)
require.False(t, changed)
require.Equal(t, body, omitted)
denyGroup := *group
denyGroup.MaxReasoningEffortOverLimit = service.ReasoningEffortOverLimitDeny
denyAPIKey := &service.APIKey{Group: &denyGroup}
denyCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
denyCtx.Request = httptest.NewRequest("POST", "/v1/responses", nil)
denyCtx.Request = denyCtx.Request.WithContext(service.WithResolvedTargetPlatform(denyCtx.Request.Context(), service.PlatformOpenAI))
_, _, err = applyOpenAIReasoningEffortPolicyForRequest(denyCtx, denyAPIKey, body)
require.Error(t, err)
var overLimit *service.ReasoningEffortOverLimitError
require.ErrorAs(t, err, &overLimit)
grokCtx, _ := gin.CreateTestContext(httptest.NewRecorder())
grokCtx.Request = httptest.NewRequest("POST", "/v1/responses", nil)
grokCtx.Request = grokCtx.Request.WithContext(service.WithResolvedTargetPlatform(grokCtx.Request.Context(), service.PlatformGrok))
got, changed = applyOpenAIReasoningEffortPolicyForRequest(grokCtx, apiKey, body)
got, changed, err = applyOpenAIReasoningEffortPolicyForRequest(grokCtx, apiKey, body)
require.NoError(t, err)
require.False(t, changed)
require.Equal(t, body, got)
}
+1
View File
@@ -221,6 +221,7 @@ func groupFromServiceBase(g *service.Group) Group {
RequirePrivacySet: g.RequirePrivacySet,
RPMLimit: g.RPMLimit,
MaxReasoningEffort: g.MaxReasoningEffort,
MaxReasoningEffortOverLimit: g.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: g.ReasoningEffortMappings,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
+2
View File
@@ -153,6 +153,8 @@ type Group struct {
RPMLimit int `json:"rpm_limit"`
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort"`
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit"`
// ReasoningEffortMappings OpenAI/Codex 推理强度精确映射。
ReasoningEffortMappings []domain.ReasoningEffortMapping `json:"reasoning_effort_mappings"`
@@ -81,7 +81,10 @@ func (h *OpenAIGatewayHandler) ChatCompletions(c *gin.Context) {
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
return
}
if cappedBody, changed := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); changed {
if cappedBody, changed, err := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); err != nil {
respondOpenAIReasoningEffortPolicyError(c, err, h.errorResponse)
return
} else if changed {
body = cappedBody
}
reqStream, ok := parseOpenAICompatibleStream(body)
@@ -435,7 +435,10 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "Model is not supported by this OpenAI-compatible endpoint for composite groups")
return
}
if cappedBody, changed := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); changed {
if cappedBody, changed, err := applyOpenAIReasoningEffortPolicyForRequest(c, apiKey, body); err != nil {
respondOpenAIReasoningEffortPolicyError(c, err, h.errorResponse)
return
} else if changed {
body = cappedBody
}
if normalizedBody, changed := normalizeCodexDelegationBootstrap(body); changed {
@@ -2530,7 +2533,7 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
zap.Int("candidate_count", scheduleDecision.CandidateCount),
)
maxReasoningEffort, reasoningEffortMappings, _ := openAIReasoningEffortPolicyForRequest(c, apiKey)
maxReasoningEffort, reasoningEffortMappings, maxReasoningEffortOverLimit, _ := openAIReasoningEffortPolicyForRequest(c, apiKey)
var requestPayloadHash string
var turnStartsMu sync.Mutex
turnStarts := make(map[int]time.Time, 4)
@@ -2557,12 +2560,13 @@ func (h *OpenAIGatewayHandler) ResponsesWebSocket(c *gin.Context) {
// passthrough 没有 BeforeTurn 时,AfterTurn 回退到 TurnStarted 的所属 turn 时刻。
var turnPricing openAIWSTurnPricing
hooks := &service.OpenAIWSIngressHooks{
ClientLifecycleContext: clientLifecycleCtx,
InitialRequestModel: reqModel,
InitialTurnStartedAt: firstTurnStartedAt,
MaxReasoningEffort: maxReasoningEffort,
ReasoningEffortMappings: reasoningEffortMappings,
TurnStarted: recordTurnStart,
ClientLifecycleContext: clientLifecycleCtx,
InitialRequestModel: reqModel,
InitialTurnStartedAt: firstTurnStartedAt,
MaxReasoningEffort: maxReasoningEffort,
MaxReasoningEffortOverLimit: maxReasoningEffortOverLimit,
ReasoningEffortMappings: reasoningEffortMappings,
TurnStarted: recordTurnStart,
BeforeRequest: func(turn int, payload []byte, originalModel string) error {
c.Set(securityAuditWSTurnContextKey, turn)
service.BeginOpsStreamTurn(c, turn)
@@ -2287,6 +2287,7 @@ func isOpsLocalBusinessLimitError(code string, msg string) bool {
strings.Contains(msg, "this group is restricted to claude code clients") ||
strings.Contains(msg, "this group does not allow /v1/messages dispatch") ||
strings.Contains(msg, "image generation is not enabled for this group") ||
(strings.Contains(msg, "reasoning effort") && strings.Contains(msg, "exceeds this group's limit")) ||
strings.Contains(msg, "token counting is not supported for this platform") ||
strings.Contains(msg, "images api is not supported for this platform") ||
(strings.Contains(msg, "model ") && strings.Contains(msg, " not in whitelist")) ||
@@ -1243,6 +1243,15 @@ func TestClassifyOpsLocalBusinessLimitErrorsExcludedFromSLA(t *testing.T) {
wantErrType: "permission_error",
wantPhase: "request",
},
{
name: "group reasoning effort over limit deny",
errType: "permission_error",
message: `reasoning effort "high" exceeds this group's limit of "low"`,
code: "",
status: http.StatusForbidden,
wantErrType: "permission_error",
wantPhase: "request",
},
{
name: "route token counting platform unsupported",
errType: "not_found_error",
@@ -1504,6 +1513,12 @@ func TestClassifyOpsUpstreamAuthTextStillCountsForSLA(t *testing.T) {
code: "403",
status: http.StatusForbidden,
},
{
name: "provider reasoning effort over limit shaped error",
message: `reasoning effort "high" exceeds this group's limit of "low"`,
code: "403",
status: http.StatusForbidden,
},
{
name: "provider token counting unsupported shaped error",
message: "Token counting is not supported for this platform",
@@ -215,6 +215,7 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se
group.FieldModelsListConfig,
group.FieldRpmLimit,
group.FieldMaxReasoningEffort,
group.FieldMaxReasoningEffortOverLimit,
group.FieldReasoningEffortMappings,
group.FieldPeakRateEnabled,
group.FieldPeakStart,
@@ -1014,6 +1015,7 @@ func groupEntityToService(g *dbent.Group) *service.Group {
ModelsListConfig: g.ModelsListConfig,
RPMLimit: g.RpmLimit,
MaxReasoningEffort: g.MaxReasoningEffort,
MaxReasoningEffortOverLimit: g.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: g.ReasoningEffortMappings,
PeakRateEnabled: g.PeakRateEnabled,
PeakStart: g.PeakStart,
@@ -110,6 +110,7 @@ func createGroupRecord(ctx context.Context, client *dbent.Client, groupIn *servi
SetModelsListConfig(groupIn.ModelsListConfig).
SetRpmLimit(groupIn.RPMLimit).
SetMaxReasoningEffort(groupIn.MaxReasoningEffort).
SetMaxReasoningEffortOverLimit(groupIn.MaxReasoningEffortOverLimit).
SetReasoningEffortMappings(groupIn.ReasoningEffortMappings).
SetPeakRateEnabled(groupIn.PeakRateEnabled).
SetPeakStart(groupIn.PeakStart).
@@ -286,6 +287,7 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
SetModelsListConfig(groupIn.ModelsListConfig).
SetRpmLimit(groupIn.RPMLimit).
SetMaxReasoningEffort(groupIn.MaxReasoningEffort).
SetMaxReasoningEffortOverLimit(groupIn.MaxReasoningEffortOverLimit).
SetReasoningEffortMappings(groupIn.ReasoningEffortMappings).
SetPeakRateEnabled(groupIn.PeakRateEnabled).
SetPeakStart(groupIn.PeakStart).
@@ -392,6 +392,7 @@ func TestAPIContracts(t *testing.T) {
"require_oauth_only": false,
"require_privacy_set": false,
"max_reasoning_effort": "",
"max_reasoning_effort_over_limit": "",
"reasoning_effort_mappings": null,
"rpm_limit": 0,
"created_at": "2025-01-02T03:04:05Z",
+12
View File
@@ -309,6 +309,10 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT", "%v", err)
}
maxReasoningEffortOverLimit, err := normalizeMaxReasoningEffortOverLimitForPlatform(platform, input.MaxReasoningEffortOverLimit)
if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT_OVER_LIMIT", "%v", err)
}
reasoningEffortMappings, err := NormalizeReasoningEffortMappings(platform, input.ReasoningEffortMappings)
if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_REASONING_EFFORT_MAPPING", "%v", err)
@@ -507,6 +511,7 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
ModelsListConfig: normalizeGroupModelsListConfig(input.ModelsListConfig),
RPMLimit: input.RPMLimit,
MaxReasoningEffort: maxReasoningEffort,
MaxReasoningEffortOverLimit: maxReasoningEffortOverLimit,
ReasoningEffortMappings: reasoningEffortMappings,
}
sanitizeGroupMessagesDispatchFields(group)
@@ -887,6 +892,13 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd
}
group.MaxReasoningEffort = maxReasoningEffort
}
if input.MaxReasoningEffortOverLimit != nil {
maxReasoningEffortOverLimit, err := normalizeMaxReasoningEffortOverLimitForPlatform(group.Platform, *input.MaxReasoningEffortOverLimit)
if err != nil {
return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT_OVER_LIMIT", "%v", err)
}
group.MaxReasoningEffortOverLimit = maxReasoningEffortOverLimit
}
if input.ReasoningEffortMappings != nil {
reasoningEffortMappings, err := NormalizeReasoningEffortMappings(group.Platform, *input.ReasoningEffortMappings)
if err != nil {
@@ -152,9 +152,10 @@ func cloneGroupForDuplicate(source *Group, operationID string) *Group {
Enabled: source.ModelsListConfig.Enabled,
Models: append([]string(nil), source.ModelsListConfig.Models...),
},
RPMLimit: source.RPMLimit,
MaxReasoningEffort: source.MaxReasoningEffort,
ReasoningEffortMappings: append([]ReasoningEffortMapping(nil), source.ReasoningEffortMappings...),
RPMLimit: source.RPMLimit,
MaxReasoningEffort: source.MaxReasoningEffort,
MaxReasoningEffortOverLimit: source.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: append([]ReasoningEffortMapping(nil), source.ReasoningEffortMappings...),
}
}
@@ -175,17 +175,18 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
HaikuMappedModel: "gpt-5-mini",
ExactModelMappings: map[string]string{"claude-special": "gpt-special"},
},
ModelsListConfig: GroupModelsListConfig{Enabled: true, Models: []string{"gpt-5.4", "gpt-5-mini"}},
RPMLimit: 99,
MaxReasoningEffort: "medium",
ReasoningEffortMappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}},
CreatedAt: createdAt,
UpdatedAt: createdAt,
AccountCount: 12,
ActiveAccountCount: 8,
RateLimitedAccountCount: 2,
DuplicateOperationID: "old-operation-must-not-copy",
AccountGroups: []AccountGroup{{AccountID: 13, GroupID: 41, Priority: 37}},
ModelsListConfig: GroupModelsListConfig{Enabled: true, Models: []string{"gpt-5.4", "gpt-5-mini"}},
RPMLimit: 99,
MaxReasoningEffort: "medium",
MaxReasoningEffortOverLimit: ReasoningEffortOverLimitDeny,
ReasoningEffortMappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}},
CreatedAt: createdAt,
UpdatedAt: createdAt,
AccountCount: 12,
ActiveAccountCount: 8,
RateLimitedAccountCount: 2,
DuplicateOperationID: "old-operation-must-not-copy",
AccountGroups: []AccountGroup{{AccountID: 13, GroupID: 41, Priority: 37}},
}
repo := newDuplicateGroupRepoStub(source)
repo.sourceBindings[source.ID] = []AccountGroup{
@@ -215,6 +216,7 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
require.Equal(t, source.ModelsListConfig, duplicate.ModelsListConfig)
require.Equal(t, source.RPMLimit, duplicate.RPMLimit)
require.Equal(t, source.MaxReasoningEffort, duplicate.MaxReasoningEffort)
require.Equal(t, source.MaxReasoningEffortOverLimit, duplicate.MaxReasoningEffortOverLimit)
require.Equal(t, source.ReasoningEffortMappings, duplicate.ReasoningEffortMappings)
require.EqualValues(t, 2, duplicate.AccountCount)
require.EqualValues(t, 2, duplicate.ActiveAccountCount)
@@ -275,6 +275,8 @@ type CreateGroupInput struct {
RPMLimit int
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string
// ReasoningEffortMappings OpenAI/Codex 推理强度精确映射。
ReasoningEffortMappings []ReasoningEffortMapping
// 分组利润控制(五个 token 平台分组可启用;margin/buffer 为小数,nil 按 0 处理)
@@ -350,6 +352,8 @@ type UpdateGroupInput struct {
RPMLimit *int
// MaxReasoningEffort 空字符串表示清除上限;nil 表示未提供不改动。
MaxReasoningEffort *string
// MaxReasoningEffortOverLimit 空字符串视为 downgrade;nil 表示未提供不改动。
MaxReasoningEffortOverLimit *string
// ReasoningEffortMappings nil 表示不修改,空数组表示清空,非空数组表示替换。
ReasoningEffortMappings *[]ReasoningEffortMapping
// 分组利润控制(nil 表示不修改;margin/buffer 为小数)
@@ -42,10 +42,11 @@ func TestAdminService_CreateCompositeGroupCopiesAccountsFromConcreteGroups(t *te
svc := &adminServiceImpl{groupRepo: groupRepo}
group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{
Name: "Composite",
Platform: PlatformComposite,
RateMultiplier: 1,
MaxReasoningEffort: "medium",
Name: "Composite",
Platform: PlatformComposite,
RateMultiplier: 1,
MaxReasoningEffort: "medium",
MaxReasoningEffortOverLimit: ReasoningEffortOverLimitDeny,
ReasoningEffortMappings: []ReasoningEffortMapping{
{From: "max", To: "xhigh"},
},
@@ -55,6 +56,7 @@ func TestAdminService_CreateCompositeGroupCopiesAccountsFromConcreteGroups(t *te
require.NoError(t, err)
require.Equal(t, PlatformComposite, groupRepo.created.Platform)
require.Equal(t, "medium", groupRepo.created.MaxReasoningEffort)
require.Equal(t, ReasoningEffortOverLimitDeny, groupRepo.created.MaxReasoningEffortOverLimit)
require.Equal(t, []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, groupRepo.created.ReasoningEffortMappings)
require.Equal(t, int64(99), group.ID)
require.Equal(t, int64(2), group.AccountCount)
@@ -90,17 +92,20 @@ func TestAdminService_UpdateCompositeGroupCopiesAccountsFromConcreteGroups(t *te
}
svc := &adminServiceImpl{groupRepo: groupRepo}
maxReasoningEffort := "low"
maxReasoningEffortOverLimit := ReasoningEffortOverLimitDeny
reasoningEffortMappings := []ReasoningEffortMapping{{From: "max", To: "high"}}
group, err := svc.UpdateGroup(context.Background(), 99, &UpdateGroupInput{
MaxReasoningEffort: &maxReasoningEffort,
ReasoningEffortMappings: &reasoningEffortMappings,
CopyAccountsFromGroupIDs: []int64{10, 20},
MaxReasoningEffort: &maxReasoningEffort,
MaxReasoningEffortOverLimit: &maxReasoningEffortOverLimit,
ReasoningEffortMappings: &reasoningEffortMappings,
CopyAccountsFromGroupIDs: []int64{10, 20},
})
require.NoError(t, err)
require.Equal(t, PlatformComposite, group.Platform)
require.Equal(t, "low", group.MaxReasoningEffort)
require.Equal(t, ReasoningEffortOverLimitDeny, group.MaxReasoningEffortOverLimit)
require.Equal(t, reasoningEffortMappings, group.ReasoningEffortMappings)
require.Equal(t, int64(99), clearedGroupID)
require.ElementsMatch(t, []int64{10, 20}, copiedFrom)
@@ -935,12 +935,13 @@ func TestAdminService_UpdateGroup_RejectsInvalidReasoningEffortMappings(t *testi
func TestAdminService_UpdateGroup_ClearsReasoningPolicyForUnsupportedPlatform(t *testing.T) {
existing := &Group{
ID: 1,
Name: "openai-group",
Platform: PlatformOpenAI,
Status: StatusActive,
MaxReasoningEffort: "medium",
ReasoningEffortMappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}},
ID: 1,
Name: "openai-group",
Platform: PlatformOpenAI,
Status: StatusActive,
MaxReasoningEffort: "medium",
MaxReasoningEffortOverLimit: ReasoningEffortOverLimitDeny,
ReasoningEffortMappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}},
}
repo := &groupRepoStubForAdmin{getByID: existing}
svc := &adminServiceImpl{groupRepo: repo}
@@ -949,6 +950,7 @@ func TestAdminService_UpdateGroup_ClearsReasoningPolicyForUnsupportedPlatform(t
require.NoError(t, err)
require.Empty(t, repo.updated.MaxReasoningEffort)
require.Equal(t, ReasoningEffortOverLimitDowngrade, repo.updated.MaxReasoningEffortOverLimit)
require.Empty(t, repo.updated.ReasoningEffortMappings)
}
@@ -112,6 +112,8 @@ type APIKeyAuthGroupSnapshot struct {
// MaxReasoningEffort OpenAI/Codex 请求的推理强度上限,空字符串表示不限制。
MaxReasoningEffort string `json:"max_reasoning_effort,omitempty"`
// MaxReasoningEffortOverLimit 超过上限时的访问控制:downgrade(默认)或 deny。
MaxReasoningEffortOverLimit string `json:"max_reasoning_effort_over_limit,omitempty"`
// ReasoningEffortMappings rewrites explicit effort values before the ceiling.
ReasoningEffortMappings []ReasoningEffortMapping `json:"reasoning_effort_mappings"`
@@ -423,6 +423,7 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey)
ModelsListConfig: apiKey.Group.ModelsListConfig,
RPMLimit: apiKey.Group.RPMLimit,
MaxReasoningEffort: apiKey.Group.MaxReasoningEffort,
MaxReasoningEffortOverLimit: apiKey.Group.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: apiKey.Group.ReasoningEffortMappings,
PeakRateEnabled: apiKey.Group.PeakRateEnabled,
PeakStart: apiKey.Group.PeakStart,
@@ -521,6 +522,7 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho
ModelsListConfig: snapshot.Group.ModelsListConfig,
RPMLimit: snapshot.Group.RPMLimit,
MaxReasoningEffort: snapshot.Group.MaxReasoningEffort,
MaxReasoningEffortOverLimit: snapshot.Group.MaxReasoningEffortOverLimit,
ReasoningEffortMappings: snapshot.Group.ReasoningEffortMappings,
PeakRateEnabled: snapshot.Group.PeakRateEnabled,
PeakStart: snapshot.Group.PeakStart,
@@ -295,13 +295,14 @@ func TestAPIKeyService_SnapshotRoundTrip_PreservesReasoningEffortPolicy(t *testi
Concurrency: 3,
},
Group: &Group{
ID: groupID,
Name: "composite",
Platform: PlatformComposite,
Status: StatusActive,
SubscriptionType: SubscriptionTypeStandard,
RateMultiplier: 1,
MaxReasoningEffort: "medium",
ID: groupID,
Name: "composite",
Platform: PlatformComposite,
Status: StatusActive,
SubscriptionType: SubscriptionTypeStandard,
RateMultiplier: 1,
MaxReasoningEffort: "medium",
MaxReasoningEffortOverLimit: ReasoningEffortOverLimitDeny,
ReasoningEffortMappings: []ReasoningEffortMapping{
{From: "max", To: "xhigh"},
},
@@ -315,6 +316,7 @@ func TestAPIKeyService_SnapshotRoundTrip_PreservesReasoningEffortPolicy(t *testi
require.NotNil(t, roundTrip.Group)
require.Equal(t, PlatformComposite, roundTrip.Group.Platform)
require.Equal(t, "medium", roundTrip.Group.MaxReasoningEffort)
require.Equal(t, ReasoningEffortOverLimitDeny, roundTrip.Group.MaxReasoningEffortOverLimit)
require.Equal(t, apiKey.Group.ReasoningEffortMappings, roundTrip.Group.ReasoningEffortMappings)
}
+3
View File
@@ -113,6 +113,9 @@ type Group struct {
// MaxReasoningEffort limits the effective OpenAI/Codex reasoning effort.
// Empty means unlimited; supported values are minimal/low/medium/high/xhigh/max.
MaxReasoningEffort string
// MaxReasoningEffortOverLimit is the access control when an explicit effort
// exceeds the ceiling: downgrade (default) or deny.
MaxReasoningEffortOverLimit string
// ReasoningEffortMappings rewrites explicit request values before applying the ceiling.
ReasoningEffortMappings []ReasoningEffortMapping
@@ -270,7 +270,16 @@ func (s *OpenAIGatewayService) ForwardAsAnthropic(
}
}
if account.Platform == PlatformOpenAI {
if policyBody, changed := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, responsesBody); changed {
policyBody, changed, policyErr := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, responsesBody)
if policyErr != nil {
var overLimit *ReasoningEffortOverLimitError
if errors.As(policyErr, &overLimit) {
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
writeAnthropicError(c, http.StatusForbidden, "forbidden_error", overLimit.Error())
}
return nil, policyErr
}
if changed {
responsesBody = policyBody
if responsesReq.Reasoning != nil {
responsesReq.Reasoning.Effort = gjson.GetBytes(responsesBody, "reasoning.effort").String()
@@ -3,6 +3,7 @@ package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@@ -80,7 +81,16 @@ func (s *OpenAIGatewayService) forwardAnthropicViaRawChatCompletions(
chatBody = normalizedBody
}
if account.Platform == PlatformOpenAI {
if policyBody, changed := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, chatBody); changed {
policyBody, changed, policyErr := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, chatBody)
if policyErr != nil {
var overLimit *ReasoningEffortOverLimitError
if errors.As(policyErr, &overLimit) {
MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalPolicyDenied)
writeAnthropicError(c, http.StatusForbidden, "forbidden_error", overLimit.Error())
}
return nil, policyErr
}
if changed {
chatBody = policyBody
if effectiveEffort := strings.TrimSpace(gjson.GetBytes(chatBody, "reasoning_effort").String()); effectiveEffort != "" {
reasoningEffort = &effectiveEffort
@@ -114,7 +114,7 @@ func TestForwardAsAnthropic_ForceChatCompletionsPreservesFinalModelReasoningEffo
svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
ctx := context.Background()
if tt.maxPolicy != "" {
ctx = WithOpenAIReasoningEffortPolicy(ctx, tt.maxPolicy, nil)
ctx = WithOpenAIReasoningEffortPolicy(ctx, tt.maxPolicy, nil, "")
}
result, err := svc.ForwardAsAnthropic(ctx, c, account, []byte(body), "", "")
require.NoError(t, err)
@@ -464,7 +464,7 @@ func TestForwardAsAnthropic_ResponsesSupportedAccountStillUsesResponsesEndpoint(
openai_compat.ExtraKeyResponsesSupported: true,
}
ctx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", nil)
ctx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", nil, "")
result, err := svc.ForwardAsAnthropic(ctx, c, account, body, "", "")
require.NoError(t, err)
require.NotNil(t, result)
@@ -12,6 +12,11 @@ import (
const (
maxReasoningEffortMappings = 64
maxReasoningEffortValueLen = 64
// ReasoningEffortOverLimitDowngrade rewrites values above the ceiling.
ReasoningEffortOverLimitDowngrade = "downgrade"
// ReasoningEffortOverLimitDeny rejects the request when the ceiling is exceeded.
ReasoningEffortOverLimitDeny = "deny"
)
var openAIReasoningEffortValues = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
@@ -21,9 +26,35 @@ type requestedReasoningEffortContextKey struct{}
type openAIReasoningEffortPolicy struct {
maxEffort string
overLimit string
mappings []ReasoningEffortMapping
}
// ReasoningEffortOverLimitError is returned when a group policy is set to deny
// requests whose explicit reasoning effort exceeds the ceiling.
type ReasoningEffortOverLimitError struct {
Requested string
Max string
}
func (e *ReasoningEffortOverLimitError) Error() string {
if e == nil {
return "reasoning effort exceeds this group's limit"
}
requested := strings.TrimSpace(e.Requested)
max := strings.TrimSpace(e.Max)
if requested == "" && max == "" {
return "reasoning effort exceeds this group's limit"
}
if requested == "" {
return fmt.Sprintf("reasoning effort exceeds this group's limit of %q", max)
}
if max == "" {
return fmt.Sprintf("reasoning effort %q exceeds this group's limit", requested)
}
return fmt.Sprintf("reasoning effort %q exceeds this group's limit of %q", requested, max)
}
// NormalizeMaxReasoningEffort validates and canonicalizes a group policy value.
// Empty means that the group does not impose a ceiling.
func NormalizeMaxReasoningEffort(raw string) string {
@@ -84,6 +115,42 @@ func normalizeMaxReasoningEffortForPlatform(platform, raw string) (string, error
)
}
// NormalizeMaxReasoningEffortOverLimit canonicalizes the over-limit action.
// Empty means the historical default: automatically downgrade to the ceiling.
func NormalizeMaxReasoningEffortOverLimit(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", ReasoningEffortOverLimitDowngrade:
return ReasoningEffortOverLimitDowngrade
case ReasoningEffortOverLimitDeny:
return ReasoningEffortOverLimitDeny
default:
return ""
}
}
func normalizeMaxReasoningEffortOverLimitForPlatform(platform, raw string) (string, error) {
value := NormalizeMaxReasoningEffortOverLimit(raw)
if value == "" {
return "", fmt.Errorf(
"reasoning effort over-limit action %q is not supported; allowed values: %s, %s",
raw,
ReasoningEffortOverLimitDowngrade,
ReasoningEffortOverLimitDeny,
)
}
if value == ReasoningEffortOverLimitDowngrade {
return value, nil
}
if len(reasoningEffortValuesForPlatform(platform)) == 0 {
return "", fmt.Errorf(
"reasoning effort policy is only supported for platforms %q and %q",
PlatformOpenAI,
PlatformComposite,
)
}
return value, nil
}
func reasoningEffortRank(raw string) (int, bool) {
switch NormalizeMaxReasoningEffort(raw) {
case "minimal":
@@ -170,12 +237,13 @@ func RequestedReasoningEffortFromContext(ctx context.Context) *string {
// WithOpenAIReasoningEffortPolicy binds a group policy to a request after its
// concrete target platform has been resolved to OpenAI. The policy is copied so
// retries and asynchronous forwarding cannot observe later slice mutations.
func WithOpenAIReasoningEffortPolicy(ctx context.Context, maxEffort string, mappings []ReasoningEffortMapping) context.Context {
func WithOpenAIReasoningEffortPolicy(ctx context.Context, maxEffort string, mappings []ReasoningEffortMapping, overLimit string) context.Context {
if ctx == nil {
ctx = context.Background()
}
policy := openAIReasoningEffortPolicy{
maxEffort: maxEffort,
overLimit: overLimit,
mappings: append([]ReasoningEffortMapping(nil), mappings...),
}
return context.WithValue(ctx, openAIReasoningEffortPolicyContextKey{}, policy)
@@ -183,15 +251,15 @@ func WithOpenAIReasoningEffortPolicy(ctx context.Context, maxEffort string, mapp
// ApplyOpenAIReasoningEffortPolicyFromContext applies a policy previously bound
// to the request. An unbound request is returned byte-for-byte unchanged.
func ApplyOpenAIReasoningEffortPolicyFromContext(ctx context.Context, body []byte) ([]byte, bool) {
func ApplyOpenAIReasoningEffortPolicyFromContext(ctx context.Context, body []byte) ([]byte, bool, error) {
if ctx == nil {
return body, false
return body, false, nil
}
policy, ok := ctx.Value(openAIReasoningEffortPolicyContextKey{}).(openAIReasoningEffortPolicy)
if !ok {
return body, false
return body, false, nil
}
return ApplyOpenAIReasoningEffortPolicy(body, policy.maxEffort, policy.mappings)
return ApplyOpenAIReasoningEffortPolicy(body, policy.maxEffort, policy.mappings, policy.overLimit)
}
func mapReasoningEffort(raw string, mappings []ReasoningEffortMapping) (string, bool) {
@@ -211,24 +279,32 @@ func sanitizeGroupReasoningEffortPolicy(group *Group) {
}
maxEffort, maxErr := normalizeMaxReasoningEffortForPlatform(group.Platform, group.MaxReasoningEffort)
mappings, mappingsErr := NormalizeReasoningEffortMappings(group.Platform, group.ReasoningEffortMappings)
overLimit := NormalizeMaxReasoningEffortOverLimit(group.MaxReasoningEffortOverLimit)
if maxErr != nil {
maxEffort = ""
}
if mappingsErr != nil {
mappings = []ReasoningEffortMapping{}
}
if overLimit == "" || (group.Platform != PlatformOpenAI && group.Platform != PlatformComposite) {
overLimit = ReasoningEffortOverLimitDowngrade
}
group.MaxReasoningEffort = maxEffort
group.MaxReasoningEffortOverLimit = overLimit
group.ReasoningEffortMappings = mappings
}
// ApplyOpenAIReasoningEffortPolicy applies one exact mapping and then caps
// known effort levels. Omitted values remain untouched so upstream defaults
// stay in control.
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping) ([]byte, bool) {
// ApplyOpenAIReasoningEffortPolicy applies one exact mapping and then either
// caps known effort levels or rejects the request when the group is configured
// to deny values above the ceiling. Omitted values remain untouched so
// upstream defaults stay in control.
func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []ReasoningEffortMapping, overLimit string) ([]byte, bool, error) {
maxRank, hasMax := reasoningEffortRank(maxEffort)
if len(body) == 0 || (!hasMax && len(mappings) == 0) {
return body, false
return body, false, nil
}
deny := hasMax && NormalizeMaxReasoningEffortOverLimit(overLimit) == ReasoningEffortOverLimitDeny
canonicalMax := NormalizeMaxReasoningEffort(maxEffort)
result := body
changed := false
@@ -246,7 +322,10 @@ func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []
if currentRank, recognized := reasoningEffortRank(effective); recognized {
effective = NormalizeMaxReasoningEffort(effective)
if hasMax && currentRank > maxRank {
effective = NormalizeMaxReasoningEffort(maxEffort)
if deny {
return body, false, &ReasoningEffortOverLimitError{Requested: effective, Max: canonicalMax}
}
effective = canonicalMax
}
}
if effective == original {
@@ -260,5 +339,19 @@ func ApplyOpenAIReasoningEffortPolicy(body []byte, maxEffort string, mappings []
result = updated
changed = true
}
return result, changed
return result, changed, nil
}
func applyOpenAIWSReasoningEffortPolicy(payload []byte, hooks *OpenAIWSIngressHooks) ([]byte, error) {
if hooks == nil || (hooks.MaxReasoningEffort == "" && len(hooks.ReasoningEffortMappings) == 0) {
return payload, nil
}
capped, changed, err := ApplyOpenAIReasoningEffortPolicy(payload, hooks.MaxReasoningEffort, hooks.ReasoningEffortMappings, hooks.MaxReasoningEffortOverLimit)
if err != nil {
return payload, err
}
if changed {
return capped, nil
}
return payload, nil
}
@@ -114,30 +114,69 @@ func TestNormalizeMaxReasoningEffortForPlatform(t *testing.T) {
require.ErrorContains(t, err, "not supported")
}
func TestNormalizeMaxReasoningEffortOverLimit(t *testing.T) {
require.Equal(t, ReasoningEffortOverLimitDowngrade, NormalizeMaxReasoningEffortOverLimit(""))
require.Equal(t, ReasoningEffortOverLimitDowngrade, NormalizeMaxReasoningEffortOverLimit(" downgrade "))
require.Equal(t, ReasoningEffortOverLimitDeny, NormalizeMaxReasoningEffortOverLimit("DENY"))
require.Empty(t, NormalizeMaxReasoningEffortOverLimit("block"))
}
func TestNormalizeMaxReasoningEffortOverLimitForPlatform(t *testing.T) {
value, err := normalizeMaxReasoningEffortOverLimitForPlatform(PlatformOpenAI, "")
require.NoError(t, err)
require.Equal(t, ReasoningEffortOverLimitDowngrade, value)
value, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformComposite, "deny")
require.NoError(t, err)
require.Equal(t, ReasoningEffortOverLimitDeny, value)
value, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformAnthropic, "")
require.NoError(t, err)
require.Equal(t, ReasoningEffortOverLimitDowngrade, value)
_, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformAnthropic, "deny")
require.ErrorContains(t, err, "only supported for platforms \"openai\" and \"composite\"")
_, err = normalizeMaxReasoningEffortOverLimitForPlatform(PlatformOpenAI, "block")
require.ErrorContains(t, err, "not supported")
}
func TestOpenAIReasoningEffortPolicyContext(t *testing.T) {
body := []byte(`{"reasoning":{"effort":"max"}}`)
unbound, changed := ApplyOpenAIReasoningEffortPolicyFromContext(context.Background(), body)
unbound, changed, err := ApplyOpenAIReasoningEffortPolicyFromContext(context.Background(), body)
require.NoError(t, err)
require.False(t, changed)
require.Equal(t, body, unbound)
mappings := []ReasoningEffortMapping{{From: "max", To: "xhigh"}}
ctx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", mappings)
ctx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", mappings, "")
mappings[0].To = "low"
got, changed := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, body)
got, changed, err := ApplyOpenAIReasoningEffortPolicyFromContext(ctx, body)
require.NoError(t, err)
require.True(t, changed)
require.Equal(t, "medium", gjson.GetBytes(got, "reasoning.effort").String())
denyCtx := WithOpenAIReasoningEffortPolicy(context.Background(), "medium", nil, ReasoningEffortOverLimitDeny)
_, _, err = ApplyOpenAIReasoningEffortPolicyFromContext(denyCtx, body)
require.Error(t, err)
var overLimit *ReasoningEffortOverLimitError
require.ErrorAs(t, err, &overLimit)
require.Equal(t, "max", overLimit.Requested)
require.Equal(t, "medium", overLimit.Max)
}
func TestApplyOpenAIReasoningEffortPolicy(t *testing.T) {
tests := []struct {
name string
body string
max string
mappings []ReasoningEffortMapping
path string
want string
changed bool
name string
body string
max string
overLimit string
mappings []ReasoningEffortMapping
path string
want string
changed bool
deny bool
}{
{name: "nested caps high", body: `{"reasoning":{"effort":"xhigh"}}`, max: "medium", path: "reasoning.effort", want: "medium", changed: true},
{name: "flat caps high", body: `{"reasoning_effort":"high"}`, max: "low", path: "reasoning_effort", want: "low", changed: true},
@@ -152,10 +191,24 @@ func TestApplyOpenAIReasoningEffortPolicy(t *testing.T) {
{name: "does not chain mappings", body: `{"reasoning_effort":"max"}`, mappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}, {From: "xhigh", To: "low"}}, path: "reasoning_effort", want: "xhigh", changed: true},
{name: "keeps unknown without mapping", body: `{"reasoning_effort":"future"}`, max: "low", path: "reasoning_effort", want: "future", changed: false},
{name: "keeps non string value", body: `{"reasoning_effort":{"level":"high"}}`, max: "low", path: "reasoning_effort.level", want: "high", changed: false},
{name: "deny rejects over ceiling", body: `{"reasoning_effort":"high"}`, max: "low", overLimit: ReasoningEffortOverLimitDeny, path: "reasoning_effort", want: "high", deny: true},
{name: "deny keeps value at ceiling", body: `{"reasoning_effort":"low"}`, max: "low", overLimit: ReasoningEffortOverLimitDeny, path: "reasoning_effort", want: "low"},
{name: "deny after mapping still over ceiling", body: `{"reasoning":{"effort":"max"}}`, max: "medium", overLimit: ReasoningEffortOverLimitDeny, mappings: []ReasoningEffortMapping{{From: "max", To: "xhigh"}}, path: "reasoning.effort", want: "max", deny: true},
{name: "deny allows mapping under ceiling", body: `{"reasoning":{"effort":"max"}}`, max: "medium", overLimit: ReasoningEffortOverLimitDeny, mappings: []ReasoningEffortMapping{{From: "max", To: "low"}}, path: "reasoning.effort", want: "low", changed: true},
{name: "deny ignored without ceiling", body: `{"reasoning_effort":"high"}`, overLimit: ReasoningEffortOverLimitDeny, path: "reasoning_effort", want: "high"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, changed := ApplyOpenAIReasoningEffortPolicy([]byte(tt.body), tt.max, tt.mappings)
got, changed, err := ApplyOpenAIReasoningEffortPolicy([]byte(tt.body), tt.max, tt.mappings, tt.overLimit)
if tt.deny {
require.Error(t, err)
var overLimit *ReasoningEffortOverLimitError
require.ErrorAs(t, err, &overLimit)
require.False(t, changed)
require.Equal(t, tt.body, string(got))
return
}
require.NoError(t, err)
require.Equal(t, tt.changed, changed)
if tt.path != "" {
require.Equal(t, tt.want, gjson.GetBytes(got, tt.path).String())
@@ -255,6 +255,9 @@ type OpenAIWSIngressHooks struct {
InitialTurnStartedAt time.Time
// MaxReasoningEffort limits explicit reasoning effort values for this WS session.
MaxReasoningEffort string
// MaxReasoningEffortOverLimit is the access control when an explicit effort
// exceeds the ceiling: downgrade (default) or deny.
MaxReasoningEffortOverLimit string
// ReasoningEffortMappings rewrites explicit effort values for this WS session.
ReasoningEffortMappings []ReasoningEffortMapping
TurnStarted func(turn int, startedAt time.Time)
@@ -263,10 +263,10 @@ func (s *OpenAIGatewayService) ProxyResponsesWebSocketFromClient(
)
}
requestedReasoningEffort := CanonicalRequestedReasoningEffort(normalized, strings.TrimSpace(values[1].String()))
if hooks != nil && (hooks.MaxReasoningEffort != "" || len(hooks.ReasoningEffortMappings) > 0) {
if capped, changed := ApplyOpenAIReasoningEffortPolicy(normalized, hooks.MaxReasoningEffort, hooks.ReasoningEffortMappings); changed {
normalized = capped
}
if next, policyErr := applyOpenAIWSReasoningEffortPolicy(normalized, hooks); policyErr != nil {
return openAIWSClientPayload{}, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, policyErr.Error(), policyErr)
} else {
normalized = next
}
responsesLite := isOpenAIResponsesLiteWebSocketPayload(normalized)
if compatibilityBody, compatibilityChanged, compatibilityErr := normalizeOpenAIResponsesWebSocketCompatibilityBody(normalized, account, responsesLite); compatibilityErr != nil {
@@ -696,10 +696,10 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
firstClientMessage = liteFirstMessage
}
originalFirstClientMessage := firstClientMessage
if hooks != nil && (hooks.MaxReasoningEffort != "" || len(hooks.ReasoningEffortMappings) > 0) {
if capped, changed := ApplyOpenAIReasoningEffortPolicy(firstClientMessage, hooks.MaxReasoningEffort, hooks.ReasoningEffortMappings); changed {
firstClientMessage = capped
}
if next, policyErr := applyOpenAIWSReasoningEffortPolicy(firstClientMessage, hooks); policyErr != nil {
return NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, policyErr.Error(), policyErr)
} else {
firstClientMessage = next
}
requestModel := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "model").String())
requestPreviousResponseID := strings.TrimSpace(gjson.GetBytes(firstClientMessage, "previous_response_id").String())
@@ -1032,10 +1032,10 @@ func (s *OpenAIGatewayService) proxyResponsesWebSocketV2Passthrough(
payload = litePayload
}
originalResponseCreate := payload
if hooks != nil && (hooks.MaxReasoningEffort != "" || len(hooks.ReasoningEffortMappings) > 0) {
if capped, changed := ApplyOpenAIReasoningEffortPolicy(payload, hooks.MaxReasoningEffort, hooks.ReasoningEffortMappings); changed {
payload = capped
}
if next, policyErr := applyOpenAIWSReasoningEffortPolicy(payload, hooks); policyErr != nil {
return payload, nil, NewOpenAIWSClientCloseError(coderws.StatusPolicyViolation, policyErr.Error(), policyErr)
} else {
payload = next
}
usageMeta.captureRequestedReasoningEffort(originalResponseCreate)
}
@@ -0,0 +1,5 @@
-- Per-group access control when an explicit OpenAI/Codex reasoning effort
-- exceeds max_reasoning_effort. Existing groups keep the previous behaviour
-- (automatically downgrade to the ceiling).
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS max_reasoning_effort_over_limit VARCHAR(20) NOT NULL DEFAULT 'downgrade';
@@ -17,6 +17,22 @@
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortHint") }}</p>
</div>
<div>
<label :for="`${idPrefix}-over-limit`" class="input-label">
{{ t("admin.groups.form.maxReasoningEffortOverLimit") }}
</label>
<Select
:id="`${idPrefix}-over-limit`"
:model-value="overLimit"
:options="overLimitOptions"
:aria-label="t('admin.groups.form.maxReasoningEffortOverLimit')"
:searchable="false"
:disabled="!maxEffort"
@update:model-value="updateOverLimit"
/>
<p class="input-hint">{{ t("admin.groups.form.maxReasoningEffortOverLimitHint") }}</p>
</div>
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
<div class="mb-3 flex items-center justify-between gap-3">
<label class="input-label mb-0">
@@ -120,6 +136,8 @@ import Select from "@/components/common/Select.vue";
import {
createReasoningEffortMappingRow,
reasoningEffortOptionsForPlatform,
reasoningEffortOverLimitDeny,
reasoningEffortOverLimitDowngrade,
validateReasoningEffortMappings,
type ReasoningEffortMappingErrorCode,
type ReasoningEffortMappingRow,
@@ -129,11 +147,13 @@ const props = defineProps<{
idPrefix: string;
platform: GroupPlatform;
maxEffort: string;
overLimit: string;
mappings: ReasoningEffortMappingRow[];
}>();
const emit = defineEmits<{
(event: "update:maxEffort", value: string): void;
(event: "update:overLimit", value: string): void;
(event: "update:mappings", value: ReasoningEffortMappingRow[]): void;
}>();
@@ -142,6 +162,16 @@ const showValidation = ref(false);
const reasoningEffortOptions = computed(() =>
reasoningEffortOptionsForPlatform(props.platform),
);
const overLimitOptions = computed(() => [
{
value: reasoningEffortOverLimitDowngrade,
label: t("admin.groups.form.maxReasoningEffortOverLimitDowngrade"),
},
{
value: reasoningEffortOverLimitDeny,
label: t("admin.groups.form.maxReasoningEffortOverLimitDeny"),
},
]);
const validationErrors = computed(() =>
validateReasoningEffortMappings(props.mappings, props.platform),
);
@@ -153,6 +183,13 @@ const updateMaxEffort = (value: string | number | boolean | null) => {
emit("update:maxEffort", asString(value));
};
const updateOverLimit = (value: string | number | boolean | null) => {
emit(
"update:overLimit",
asString(value) || reasoningEffortOverLimitDowngrade,
);
};
const updateMapping = (
id: string,
field: "from" | "to",
@@ -859,7 +859,11 @@ export default {
rpmLimitHint: 'Max requests per minute for each user in this group; 0 = unlimited. Once set, it takes over per-user rate limiting in this group (overrides the user-level rpm_limit fallback).',
maxReasoningEffort: 'Max reasoning effort',
maxReasoningEffortUnlimited: 'Unlimited (follow request)',
maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Higher values are capped; omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortHint: 'Limits explicit OpenAI reasoning effort requests only. For Composite groups, it applies only to requests resolved to OpenAI. Omitted effort stays omitted. The ceiling takes precedence over reasoning effort mappings.',
maxReasoningEffortOverLimit: 'Over-limit access control',
maxReasoningEffortOverLimitDowngrade: 'Automatically downgrade when over limit',
maxReasoningEffortOverLimitDeny: 'Deny access',
maxReasoningEffortOverLimitHint: 'Applies after a ceiling is set. Downgrade rewrites values above the ceiling to the ceiling. Deny rejects the request.',
reasoningEffortMappings: 'Reasoning effort mappings',
addReasoningEffortMapping: 'Add mapping',
removeReasoningEffortMapping: 'Remove mapping',
@@ -841,7 +841,11 @@ export default {
rpmLimitHint: '每用户在本分组每分钟最大请求数,0 = 不限制;一旦设置即接管该用户的限流(覆盖用户级 rpm_limit)',
maxReasoningEffort: '推理强度上限',
maxReasoningEffortUnlimited: '不限制(跟随请求)',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。超过上限时自动降档,不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortHint: '仅限制客户端主动请求的 OpenAI reasoning effort;Composite 分组仅对解析到 OpenAI 的请求生效。不会为缺省请求主动开启推理。上限优先级高于推理强度映射。',
maxReasoningEffortOverLimit: '超限访问控制',
maxReasoningEffortOverLimitDowngrade: '超过上限时自动降档',
maxReasoningEffortOverLimitDeny: '拒绝访问',
maxReasoningEffortOverLimitHint: '设置上限后生效。自动降档会将超过上限的请求改写为上限值后转发;拒绝访问则直接返回错误。',
reasoningEffortMappings: '推理强度映射',
addReasoningEffortMapping: '添加映射',
removeReasoningEffortMapping: '删除映射',
+3
View File
@@ -557,6 +557,7 @@ export interface Group {
rate_multiplier: number
rpm_limit?: number // Group-level RPM cap (0 = unlimited); overrides user-level rpm_limit when set
max_reasoning_effort?: string // OpenAI/Codex reasoning ceiling; empty means unlimited
max_reasoning_effort_over_limit?: string // downgrade (default) or deny when over the ceiling
reasoning_effort_mappings?: ReasoningEffortMapping[]
is_exclusive: boolean
status: 'active' | 'inactive'
@@ -818,6 +819,7 @@ export interface CreateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
@@ -880,6 +882,7 @@ export interface UpdateGroupRequest {
model_routing_enabled?: boolean
rpm_limit?: number
max_reasoning_effort?: string
max_reasoning_effort_over_limit?: string
reasoning_effort_mappings?: ReasoningEffortMapping[]
require_oauth_only?: boolean
require_privacy_set?: boolean
+25
View File
@@ -631,6 +631,7 @@
id-prefix="create-group-reasoning"
:platform="createForm.platform"
v-model:max-effort="createForm.max_reasoning_effort"
v-model:over-limit="createForm.max_reasoning_effort_over_limit"
v-model:mappings="createForm.reasoning_effort_mappings"
/>
<div
@@ -2361,6 +2362,7 @@
id-prefix="edit-group-reasoning"
:platform="editForm.platform"
v-model:max-effort="editForm.max_reasoning_effort"
v-model:over-limit="editForm.max_reasoning_effort_over_limit"
v-model:mappings="editForm.reasoning_effort_mappings"
/>
<div v-if="editForm.subscription_type !== 'subscription'">
@@ -4495,8 +4497,10 @@ import {
} from "./groupsProfitControl";
import {
normalizeReasoningEffortForPlatform,
normalizeReasoningEffortOverLimit,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOverLimitDowngrade,
supportsReasoningEffortPolicyPlatform,
type ReasoningEffortMappingRow,
} from "./groupsReasoningEffort";
@@ -5105,6 +5109,7 @@ const createForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5467,6 +5472,7 @@ const editForm = reactive({
// 分组级 RPM 限制(每用户每分钟最大请求数;0 = 不限制)
rpm_limit: 0 as number,
max_reasoning_effort: "",
max_reasoning_effort_over_limit: reasoningEffortOverLimitDowngrade,
reasoning_effort_mappings: [] as ReasoningEffortMappingRow[],
});
@@ -5907,6 +5913,7 @@ const closeCreateModal = () => {
createForm.copy_accounts_from_group_ids = [];
createForm.rpm_limit = 0;
createForm.max_reasoning_effort = "";
createForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = [];
createReasoningEffortPolicyRef.value?.resetValidation();
resetModelsListState(createModelsListState);
@@ -6177,6 +6184,9 @@ const handleEdit = async (group: AdminGroup) => {
group.platform,
group.max_reasoning_effort,
);
editForm.max_reasoning_effort_over_limit = normalizeReasoningEffortOverLimit(
group.max_reasoning_effort_over_limit,
);
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
group.reasoning_effort_mappings,
group.platform,
@@ -6198,6 +6208,7 @@ const closeEditModal = () => {
showEditModal.value = false;
editingGroup.value = null;
editForm.max_reasoning_effort = "";
editForm.max_reasoning_effort_over_limit = reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = [];
editReasoningEffortPolicyRef.value?.resetValidation();
editModelRoutingRules.value = [];
@@ -6673,6 +6684,13 @@ watch(
newVal,
createForm.max_reasoning_effort,
);
createForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
newVal,
)
? normalizeReasoningEffortOverLimit(
createForm.max_reasoning_effort_over_limit,
)
: reasoningEffortOverLimitDowngrade;
createForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(createForm.reasoning_effort_mappings),
newVal,
@@ -6723,6 +6741,13 @@ watch(
newVal,
editForm.max_reasoning_effort,
);
editForm.max_reasoning_effort_over_limit = supportsReasoningEffortPolicyPlatform(
newVal,
)
? normalizeReasoningEffortOverLimit(
editForm.max_reasoning_effort_over_limit,
)
: reasoningEffortOverLimitDowngrade;
editForm.reasoning_effort_mappings = reasoningEffortMappingsToRows(
reasoningEffortMappingsToAPI(editForm.reasoning_effort_mappings),
newVal,
@@ -3,9 +3,12 @@ import { describe, expect, it } from "vitest";
import {
createReasoningEffortMappingRow,
normalizeReasoningEffortForPlatform,
normalizeReasoningEffortOverLimit,
reasoningEffortMappingsToAPI,
reasoningEffortMappingsToRows,
reasoningEffortOptionsForPlatform,
reasoningEffortOverLimitDeny,
reasoningEffortOverLimitDowngrade,
supportsReasoningEffortPolicyPlatform,
validateReasoningEffortMappings,
} from "../groupsReasoningEffort";
@@ -89,4 +92,22 @@ describe("groupsReasoningEffort", () => {
[row.id]: { from: "unsupportedFrom" },
});
});
it("normalizes over-limit access control to downgrade or deny", () => {
expect(normalizeReasoningEffortOverLimit(undefined)).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit("")).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit(" downgrade ")).toBe(
reasoningEffortOverLimitDowngrade,
);
expect(normalizeReasoningEffortOverLimit("DENY")).toBe(
reasoningEffortOverLimitDeny,
);
expect(normalizeReasoningEffortOverLimit("block")).toBe(
reasoningEffortOverLimitDowngrade,
);
});
});
@@ -9,6 +9,9 @@ const openAIReasoningEffortValues = [
"max",
] as const;
export const reasoningEffortOverLimitDowngrade = "downgrade";
export const reasoningEffortOverLimitDeny = "deny";
const reasoningEffortValuesForPlatform = (
platform: GroupPlatform,
): readonly string[] =>
@@ -41,6 +44,14 @@ export function normalizeReasoningEffortForPlatform(
: "";
}
export function normalizeReasoningEffortOverLimit(
value: string | null | undefined,
): string {
return value?.trim().toLowerCase() === reasoningEffortOverLimitDeny
? reasoningEffortOverLimitDeny
: reasoningEffortOverLimitDowngrade;
}
export interface ReasoningEffortMappingRow extends ReasoningEffortMapping {
id: string;
}