Merge pull request #6444 from Ritel-T/pr/group-free-openai-fast

Add group-level Standard billing for OpenAI Fast requests
This commit is contained in:
Wesley Liddick
2026-09-02 09:23:49 +08:00
committed by GitHub
38 changed files with 481 additions and 25 deletions
+12 -1
View File
@@ -124,6 +124,8 @@ type Group struct {
AllowLive bool `json:"allow_live,omitempty"`
// 是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority
ForceOpenaiFast bool `json:"force_openai_fast,omitempty"`
// 是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费
FreeOpenaiFast bool `json:"free_openai_fast,omitempty"`
// 仅允许非 apikey 类型账号关联到此分组
RequireOauthOnly bool `json:"require_oauth_only,omitempty"`
// 调度时仅允许 privacy 已成功设置的账号
@@ -256,7 +258,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
switch columns[i] {
case group.FieldVideoModelPrices, group.FieldModelPricing, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
values[i] = new([]byte)
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldForceOpenaiFast, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldLongContextPricingEnabled, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldForceOpenaiFast, group.FieldFreeOpenaiFast, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
values[i] = new(sql.NullBool)
case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldBatchImageDiscountMultiplier, group.FieldBatchImageHoldMultiplier, group.FieldVideoRateMultiplier, group.FieldVideoPrice480p, group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, group.FieldSearchPricePer1k, group.FieldAudioRealtimePricePerMin, group.FieldAudioTtsPricePerMillionChars, group.FieldAudioSttPricePerHour, group.FieldProfitMinMargin, group.FieldProfitSafetyBuffer:
values[i] = new(sql.NullFloat64)
@@ -626,6 +628,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.ForceOpenaiFast = value.Bool
}
case group.FieldFreeOpenaiFast:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field free_openai_fast", values[i])
} else if value.Valid {
_m.FreeOpenaiFast = value.Bool
}
case group.FieldRequireOauthOnly:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field require_oauth_only", values[i])
@@ -974,6 +982,9 @@ func (_m *Group) String() string {
builder.WriteString("force_openai_fast=")
builder.WriteString(fmt.Sprintf("%v", _m.ForceOpenaiFast))
builder.WriteString(", ")
builder.WriteString("free_openai_fast=")
builder.WriteString(fmt.Sprintf("%v", _m.FreeOpenaiFast))
builder.WriteString(", ")
builder.WriteString("require_oauth_only=")
builder.WriteString(fmt.Sprintf("%v", _m.RequireOauthOnly))
builder.WriteString(", ")
+10
View File
@@ -120,6 +120,8 @@ const (
FieldAllowLive = "allow_live"
// FieldForceOpenaiFast holds the string denoting the force_openai_fast field in the database.
FieldForceOpenaiFast = "force_openai_fast"
// FieldFreeOpenaiFast holds the string denoting the free_openai_fast field in the database.
FieldFreeOpenaiFast = "free_openai_fast"
// FieldRequireOauthOnly holds the string denoting the require_oauth_only field in the database.
FieldRequireOauthOnly = "require_oauth_only"
// FieldRequirePrivacySet holds the string denoting the require_privacy_set field in the database.
@@ -271,6 +273,7 @@ var Columns = []string{
FieldAllowMessagesDispatch,
FieldAllowLive,
FieldForceOpenaiFast,
FieldFreeOpenaiFast,
FieldRequireOauthOnly,
FieldRequirePrivacySet,
FieldDefaultMappedModel,
@@ -394,6 +397,8 @@ var (
DefaultAllowLive bool
// DefaultForceOpenaiFast holds the default value on creation for the "force_openai_fast" field.
DefaultForceOpenaiFast bool
// DefaultFreeOpenaiFast holds the default value on creation for the "free_openai_fast" field.
DefaultFreeOpenaiFast bool
// DefaultRequireOauthOnly holds the default value on creation for the "require_oauth_only" field.
DefaultRequireOauthOnly bool
// DefaultRequirePrivacySet holds the default value on creation for the "require_privacy_set" field.
@@ -674,6 +679,11 @@ func ByForceOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldForceOpenaiFast, opts...).ToFunc()
}
// ByFreeOpenaiFast orders the results by the free_openai_fast field.
func ByFreeOpenaiFast(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFreeOpenaiFast, opts...).ToFunc()
}
// ByRequireOauthOnly orders the results by the require_oauth_only field.
func ByRequireOauthOnly(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRequireOauthOnly, opts...).ToFunc()
+15
View File
@@ -295,6 +295,11 @@ func ForceOpenaiFast(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldForceOpenaiFast, v))
}
// FreeOpenaiFast applies equality check predicate on the "free_openai_fast" field. It's identical to FreeOpenaiFastEQ.
func FreeOpenaiFast(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
}
// RequireOauthOnly applies equality check predicate on the "require_oauth_only" field. It's identical to RequireOauthOnlyEQ.
func RequireOauthOnly(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
@@ -2290,6 +2295,16 @@ func ForceOpenaiFastNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldForceOpenaiFast, v))
}
// FreeOpenaiFastEQ applies the EQ predicate on the "free_openai_fast" field.
func FreeOpenaiFastEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldFreeOpenaiFast, v))
}
// FreeOpenaiFastNEQ applies the NEQ predicate on the "free_openai_fast" field.
func FreeOpenaiFastNEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldNEQ(FieldFreeOpenaiFast, v))
}
// RequireOauthOnlyEQ applies the EQ predicate on the "require_oauth_only" field.
func RequireOauthOnlyEQ(v bool) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v))
+65
View File
@@ -718,6 +718,20 @@ func (_c *GroupCreate) SetNillableForceOpenaiFast(v *bool) *GroupCreate {
return _c
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_c *GroupCreate) SetFreeOpenaiFast(v bool) *GroupCreate {
_c.mutation.SetFreeOpenaiFast(v)
return _c
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_c *GroupCreate) SetNillableFreeOpenaiFast(v *bool) *GroupCreate {
if v != nil {
_c.SetFreeOpenaiFast(*v)
}
return _c
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_c *GroupCreate) SetRequireOauthOnly(v bool) *GroupCreate {
_c.mutation.SetRequireOauthOnly(v)
@@ -1127,6 +1141,10 @@ func (_c *GroupCreate) defaults() error {
v := group.DefaultForceOpenaiFast
_c.mutation.SetForceOpenaiFast(v)
}
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
v := group.DefaultFreeOpenaiFast
_c.mutation.SetFreeOpenaiFast(v)
}
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
v := group.DefaultRequireOauthOnly
_c.mutation.SetRequireOauthOnly(v)
@@ -1325,6 +1343,9 @@ func (_c *GroupCreate) check() error {
if _, ok := _c.mutation.ForceOpenaiFast(); !ok {
return &ValidationError{Name: "force_openai_fast", err: errors.New(`ent: missing required field "Group.force_openai_fast"`)}
}
if _, ok := _c.mutation.FreeOpenaiFast(); !ok {
return &ValidationError{Name: "free_openai_fast", err: errors.New(`ent: missing required field "Group.free_openai_fast"`)}
}
if _, ok := _c.mutation.RequireOauthOnly(); !ok {
return &ValidationError{Name: "require_oauth_only", err: errors.New(`ent: missing required field "Group.require_oauth_only"`)}
}
@@ -1611,6 +1632,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
_node.ForceOpenaiFast = value
}
if value, ok := _c.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
_node.FreeOpenaiFast = value
}
if value, ok := _c.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
_node.RequireOauthOnly = value
@@ -2685,6 +2710,18 @@ func (u *GroupUpsert) UpdateForceOpenaiFast() *GroupUpsert {
return u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsert) SetFreeOpenaiFast(v bool) *GroupUpsert {
u.Set(group.FieldFreeOpenaiFast, v)
return u
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsert) UpdateFreeOpenaiFast() *GroupUpsert {
u.SetExcluded(group.FieldFreeOpenaiFast)
return u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsert) SetRequireOauthOnly(v bool) *GroupUpsert {
u.Set(group.FieldRequireOauthOnly, v)
@@ -3910,6 +3947,20 @@ func (u *GroupUpsertOne) UpdateForceOpenaiFast() *GroupUpsertOne {
})
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsertOne) SetFreeOpenaiFast(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetFreeOpenaiFast(v)
})
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateFreeOpenaiFast() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateFreeOpenaiFast()
})
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsertOne) SetRequireOauthOnly(v bool) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
@@ -5328,6 +5379,20 @@ func (u *GroupUpsertBulk) UpdateForceOpenaiFast() *GroupUpsertBulk {
})
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (u *GroupUpsertBulk) SetFreeOpenaiFast(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetFreeOpenaiFast(v)
})
}
// UpdateFreeOpenaiFast sets the "free_openai_fast" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateFreeOpenaiFast() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateFreeOpenaiFast()
})
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (u *GroupUpsertBulk) SetRequireOauthOnly(v bool) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
+34
View File
@@ -1003,6 +1003,20 @@ func (_u *GroupUpdate) SetNillableForceOpenaiFast(v *bool) *GroupUpdate {
return _u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_u *GroupUpdate) SetFreeOpenaiFast(v bool) *GroupUpdate {
_u.mutation.SetFreeOpenaiFast(v)
return _u
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_u *GroupUpdate) SetNillableFreeOpenaiFast(v *bool) *GroupUpdate {
if v != nil {
_u.SetFreeOpenaiFast(*v)
}
return _u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_u *GroupUpdate) SetRequireOauthOnly(v bool) *GroupUpdate {
_u.mutation.SetRequireOauthOnly(v)
@@ -1833,6 +1847,9 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
}
@@ -3163,6 +3180,20 @@ func (_u *GroupUpdateOne) SetNillableForceOpenaiFast(v *bool) *GroupUpdateOne {
return _u
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (_u *GroupUpdateOne) SetFreeOpenaiFast(v bool) *GroupUpdateOne {
_u.mutation.SetFreeOpenaiFast(v)
return _u
}
// SetNillableFreeOpenaiFast sets the "free_openai_fast" field if the given value is not nil.
func (_u *GroupUpdateOne) SetNillableFreeOpenaiFast(v *bool) *GroupUpdateOne {
if v != nil {
_u.SetFreeOpenaiFast(*v)
}
return _u
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (_u *GroupUpdateOne) SetRequireOauthOnly(v bool) *GroupUpdateOne {
_u.mutation.SetRequireOauthOnly(v)
@@ -4023,6 +4054,9 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if value, ok := _u.mutation.ForceOpenaiFast(); ok {
_spec.SetField(group.FieldForceOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.FreeOpenaiFast(); ok {
_spec.SetField(group.FieldFreeOpenaiFast, field.TypeBool, value)
}
if value, ok := _u.mutation.RequireOauthOnly(); ok {
_spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value)
}
+1
View File
@@ -955,6 +955,7 @@ var (
{Name: "allow_messages_dispatch", Type: field.TypeBool, Default: false},
{Name: "allow_live", Type: field.TypeBool, Default: false},
{Name: "force_openai_fast", Type: field.TypeBool, Default: false},
{Name: "free_openai_fast", Type: field.TypeBool, Default: false},
{Name: "require_oauth_only", Type: field.TypeBool, Default: false},
{Name: "require_privacy_set", Type: field.TypeBool, Default: false},
{Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""},
+55 -1
View File
@@ -22159,6 +22159,7 @@ type GroupMutation struct {
allow_messages_dispatch *bool
allow_live *bool
force_openai_fast *bool
free_openai_fast *bool
require_oauth_only *bool
require_privacy_set *bool
default_mapped_model *string
@@ -24982,6 +24983,42 @@ func (m *GroupMutation) ResetForceOpenaiFast() {
m.force_openai_fast = nil
}
// SetFreeOpenaiFast sets the "free_openai_fast" field.
func (m *GroupMutation) SetFreeOpenaiFast(b bool) {
m.free_openai_fast = &b
}
// FreeOpenaiFast returns the value of the "free_openai_fast" field in the mutation.
func (m *GroupMutation) FreeOpenaiFast() (r bool, exists bool) {
v := m.free_openai_fast
if v == nil {
return
}
return *v, true
}
// OldFreeOpenaiFast returns the old "free_openai_fast" 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) OldFreeOpenaiFast(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldFreeOpenaiFast is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldFreeOpenaiFast requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldFreeOpenaiFast: %w", err)
}
return oldValue.FreeOpenaiFast, nil
}
// ResetFreeOpenaiFast resets all changes to the "free_openai_fast" field.
func (m *GroupMutation) ResetFreeOpenaiFast() {
m.free_openai_fast = nil
}
// SetRequireOauthOnly sets the "require_oauth_only" field.
func (m *GroupMutation) SetRequireOauthOnly(b bool) {
m.require_oauth_only = &b
@@ -25847,7 +25884,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, 63)
fields := make([]string, 0, 64)
if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt)
}
@@ -26004,6 +26041,9 @@ func (m *GroupMutation) Fields() []string {
if m.force_openai_fast != nil {
fields = append(fields, group.FieldForceOpenaiFast)
}
if m.free_openai_fast != nil {
fields = append(fields, group.FieldFreeOpenaiFast)
}
if m.require_oauth_only != nil {
fields = append(fields, group.FieldRequireOauthOnly)
}
@@ -26152,6 +26192,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
return m.AllowLive()
case group.FieldForceOpenaiFast:
return m.ForceOpenaiFast()
case group.FieldFreeOpenaiFast:
return m.FreeOpenaiFast()
case group.FieldRequireOauthOnly:
return m.RequireOauthOnly()
case group.FieldRequirePrivacySet:
@@ -26289,6 +26331,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldAllowLive(ctx)
case group.FieldForceOpenaiFast:
return m.OldForceOpenaiFast(ctx)
case group.FieldFreeOpenaiFast:
return m.OldFreeOpenaiFast(ctx)
case group.FieldRequireOauthOnly:
return m.OldRequireOauthOnly(ctx)
case group.FieldRequirePrivacySet:
@@ -26686,6 +26730,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
}
m.SetForceOpenaiFast(v)
return nil
case group.FieldFreeOpenaiFast:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetFreeOpenaiFast(v)
return nil
case group.FieldRequireOauthOnly:
v, ok := value.(bool)
if !ok {
@@ -27437,6 +27488,9 @@ func (m *GroupMutation) ResetField(name string) error {
case group.FieldForceOpenaiFast:
m.ResetForceOpenaiFast()
return nil
case group.FieldFreeOpenaiFast:
m.ResetFreeOpenaiFast()
return nil
case group.FieldRequireOauthOnly:
m.ResetRequireOauthOnly()
return nil
+15 -11
View File
@@ -1161,34 +1161,38 @@ func init() {
groupDescForceOpenaiFast := groupFields[48].Descriptor()
// group.DefaultForceOpenaiFast holds the default value on creation for the force_openai_fast field.
group.DefaultForceOpenaiFast = groupDescForceOpenaiFast.Default.(bool)
// groupDescFreeOpenaiFast is the schema descriptor for free_openai_fast field.
groupDescFreeOpenaiFast := groupFields[49].Descriptor()
// group.DefaultFreeOpenaiFast holds the default value on creation for the free_openai_fast field.
group.DefaultFreeOpenaiFast = groupDescFreeOpenaiFast.Default.(bool)
// groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field.
groupDescRequireOauthOnly := groupFields[49].Descriptor()
groupDescRequireOauthOnly := groupFields[50].Descriptor()
// group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field.
group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool)
// groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field.
groupDescRequirePrivacySet := groupFields[50].Descriptor()
groupDescRequirePrivacySet := groupFields[51].Descriptor()
// group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field.
group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool)
// groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field.
groupDescDefaultMappedModel := groupFields[51].Descriptor()
groupDescDefaultMappedModel := groupFields[52].Descriptor()
// group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field.
group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string)
// group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save.
group.DefaultMappedModelValidator = groupDescDefaultMappedModel.Validators[0].(func(string) error)
// groupDescMessagesDispatchModelConfig is the schema descriptor for messages_dispatch_model_config field.
groupDescMessagesDispatchModelConfig := groupFields[52].Descriptor()
groupDescMessagesDispatchModelConfig := groupFields[53].Descriptor()
// group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field.
group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig)
// groupDescModelsListConfig is the schema descriptor for models_list_config field.
groupDescModelsListConfig := groupFields[53].Descriptor()
groupDescModelsListConfig := groupFields[54].Descriptor()
// group.DefaultModelsListConfig holds the default value on creation for the models_list_config field.
group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig)
// groupDescRpmLimit is the schema descriptor for rpm_limit field.
groupDescRpmLimit := groupFields[54].Descriptor()
groupDescRpmLimit := groupFields[55].Descriptor()
// group.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
group.DefaultRpmLimit = groupDescRpmLimit.Default.(int)
// groupDescMaxReasoningEffort is the schema descriptor for max_reasoning_effort field.
groupDescMaxReasoningEffort := groupFields[55].Descriptor()
groupDescMaxReasoningEffort := groupFields[56].Descriptor()
// group.DefaultMaxReasoningEffort holds the default value on creation for the max_reasoning_effort field.
group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string)
// group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
@@ -1200,19 +1204,19 @@ func init() {
// 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[56].Descriptor()
groupDescReasoningEffortMappings := groupFields[57].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[57].Descriptor()
groupDescProfitControlEnabled := groupFields[58].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[58].Descriptor()
groupDescProfitMinMargin := groupFields[59].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[59].Descriptor()
groupDescProfitSafetyBuffer := groupFields[60].Descriptor()
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
+3
View File
@@ -245,6 +245,9 @@ func (Group) Fields() []ent.Field {
field.Bool("force_openai_fast").
Default(false).
Comment("是否强制此 OpenAI/Composite 分组请求使用 service_tier=priority"),
field.Bool("free_openai_fast").
Default(false).
Comment("是否让此 OpenAI/Composite 分组的 Fast 请求按 Standard 价格计费"),
field.Bool("require_oauth_only").
Default(false).
Comment("仅允许非 apikey 类型账号关联到此分组"),
@@ -148,6 +148,7 @@ type CreateGroupRequest struct {
AllowMessagesDispatch bool `json:"allow_messages_dispatch"`
AllowLive bool `json:"allow_live"`
ForceOpenAIFast bool `json:"force_openai_fast"`
FreeOpenAIFast bool `json:"free_openai_fast"`
RequireOAuthOnly bool `json:"require_oauth_only"`
RequirePrivacySet bool `json:"require_privacy_set"`
DefaultMappedModel string `json:"default_mapped_model"`
@@ -220,6 +221,7 @@ type UpdateGroupRequest struct {
AllowMessagesDispatch *bool `json:"allow_messages_dispatch"`
AllowLive *bool `json:"allow_live"`
ForceOpenAIFast *bool `json:"force_openai_fast"`
FreeOpenAIFast *bool `json:"free_openai_fast"`
RequireOAuthOnly *bool `json:"require_oauth_only"`
RequirePrivacySet *bool `json:"require_privacy_set"`
DefaultMappedModel *string `json:"default_mapped_model"`
@@ -557,6 +559,7 @@ func (h *GroupHandler) Create(c *gin.Context) {
AllowMessagesDispatch: req.AllowMessagesDispatch,
AllowLive: req.AllowLive,
ForceOpenAIFast: req.ForceOpenAIFast,
FreeOpenAIFast: req.FreeOpenAIFast,
RequireOAuthOnly: req.RequireOAuthOnly,
RequirePrivacySet: req.RequirePrivacySet,
DefaultMappedModel: req.DefaultMappedModel,
@@ -688,6 +691,7 @@ func (h *GroupHandler) Update(c *gin.Context) {
AllowMessagesDispatch: req.AllowMessagesDispatch,
AllowLive: req.AllowLive,
ForceOpenAIFast: req.ForceOpenAIFast,
FreeOpenAIFast: req.FreeOpenAIFast,
RequireOAuthOnly: req.RequireOAuthOnly,
RequirePrivacySet: req.RequirePrivacySet,
DefaultMappedModel: req.DefaultMappedModel,
@@ -9,15 +9,19 @@ import (
func TestGroupRequestsDecodeForceOpenAIFast(t *testing.T) {
var createReq CreateGroupRequest
require.NoError(t, json.Unmarshal([]byte(`{"name":"fast","force_openai_fast":true}`), &createReq))
require.NoError(t, json.Unmarshal([]byte(`{"name":"fast","force_openai_fast":true,"free_openai_fast":true}`), &createReq))
require.True(t, createReq.ForceOpenAIFast)
require.True(t, createReq.FreeOpenAIFast)
var updateReq UpdateGroupRequest
require.NoError(t, json.Unmarshal([]byte(`{"force_openai_fast":false}`), &updateReq))
require.NoError(t, json.Unmarshal([]byte(`{"force_openai_fast":false,"free_openai_fast":false}`), &updateReq))
require.NotNil(t, updateReq.ForceOpenAIFast)
require.False(t, *updateReq.ForceOpenAIFast)
require.NotNil(t, updateReq.FreeOpenAIFast)
require.False(t, *updateReq.FreeOpenAIFast)
var omitted UpdateGroupRequest
require.NoError(t, json.Unmarshal([]byte(`{}`), &omitted))
require.Nil(t, omitted.ForceOpenAIFast)
require.Nil(t, omitted.FreeOpenAIFast)
}
@@ -10,14 +10,16 @@ import (
func TestGroupMapperExposesForceOpenAIFastOnlyToAdmins(t *testing.T) {
group := &service.Group{
ID: 7, Name: "fast", Platform: service.PlatformOpenAI, Status: service.StatusActive, ForceOpenAIFast: true,
ID: 7, Name: "fast", Platform: service.PlatformOpenAI, Status: service.StatusActive, ForceOpenAIFast: true, FreeOpenAIFast: true,
}
userJSON, err := json.Marshal(GroupFromService(group))
require.NoError(t, err)
require.NotContains(t, string(userJSON), "force_openai_fast")
require.NotContains(t, string(userJSON), "free_openai_fast")
adminJSON, err := json.Marshal(GroupFromServiceAdmin(group))
require.NoError(t, err)
require.Contains(t, string(adminJSON), `"force_openai_fast":true`)
require.Contains(t, string(adminJSON), `"free_openai_fast":true`)
}
+1
View File
@@ -149,6 +149,7 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
out := &AdminGroup{
Group: groupFromServiceBase(g),
ForceOpenAIFast: g.ForceOpenAIFast,
FreeOpenAIFast: g.FreeOpenAIFast,
ProfitControlEnabled: g.ProfitControlEnabled,
ProfitMinMargin: g.ProfitMinMargin,
ProfitSafetyBuffer: g.ProfitSafetyBuffer,
+2
View File
@@ -168,6 +168,8 @@ type AdminGroup struct {
Group
// ForceOpenAIFast 是管理端请求策略,用户侧分组 DTO 无需暴露。
ForceOpenAIFast bool `json:"force_openai_fast"`
// FreeOpenAIFast 是管理端计费策略,用户侧分组 DTO 无需暴露。
FreeOpenAIFast bool `json:"free_openai_fast"`
// 分组利润控制(五个 token 平台分组可启用;margin/buffer 为小数存储)。
// 仅管理员可见:这三个字段与同响应中的 rate_multiplier 相乘即可反推出
@@ -211,6 +211,7 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se
group.FieldAllowMessagesDispatch,
group.FieldAllowLive,
group.FieldForceOpenaiFast,
group.FieldFreeOpenaiFast,
group.FieldDefaultMappedModel,
group.FieldMessagesDispatchModelConfig,
group.FieldModelsListConfig,
@@ -1010,6 +1011,7 @@ func groupEntityToService(g *dbent.Group) *service.Group {
AllowMessagesDispatch: g.AllowMessagesDispatch,
AllowLive: g.AllowLive,
ForceOpenAIFast: g.ForceOpenaiFast,
FreeOpenAIFast: g.FreeOpenaiFast,
RequireOAuthOnly: g.RequireOauthOnly,
RequirePrivacySet: g.RequirePrivacySet,
DefaultMappedModel: g.DefaultMappedModel,
@@ -17,7 +17,7 @@ func TestGetByKeyForAuthCarriesGroupForceOpenAIFast(t *testing.T) {
suffix := time.Now().UnixNano()
group := mustCreateGroup(t, integrationEntClient, &service.Group{
Name: fmt.Sprintf("fast-proj-group-%d", suffix), Platform: service.PlatformOpenAI,
RateMultiplier: 1, ForceOpenAIFast: true,
RateMultiplier: 1, ForceOpenAIFast: true, FreeOpenAIFast: true,
})
user := mustCreateUser(t, integrationEntClient, &service.User{
Email: fmt.Sprintf("fast-proj-%d@example.com", suffix), Concurrency: 5,
@@ -42,4 +42,5 @@ func TestGetByKeyForAuthCarriesGroupForceOpenAIFast(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, got.Group)
require.True(t, got.Group.ForceOpenAIFast)
require.True(t, got.Group.FreeOpenAIFast)
}
@@ -91,6 +91,7 @@ func mustCreateGroup(t *testing.T, client *dbent.Client, g *service.Group) *serv
SetRateMultiplier(g.RateMultiplier).
SetIsExclusive(g.IsExclusive).
SetForceOpenaiFast(g.ForceOpenAIFast).
SetFreeOpenaiFast(g.FreeOpenAIFast).
SetProfitControlEnabled(g.ProfitControlEnabled).
SetProfitMinMargin(g.ProfitMinMargin).
SetProfitSafetyBuffer(g.ProfitSafetyBuffer)
@@ -104,6 +104,7 @@ func createGroupRecord(ctx context.Context, client *dbent.Client, groupIn *servi
SetAllowMessagesDispatch(groupIn.AllowMessagesDispatch).
SetAllowLive(groupIn.AllowLive).
SetForceOpenaiFast(groupIn.ForceOpenAIFast).
SetFreeOpenaiFast(groupIn.FreeOpenAIFast).
SetRequireOauthOnly(groupIn.RequireOAuthOnly).
SetRequirePrivacySet(groupIn.RequirePrivacySet).
SetDefaultMappedModel(groupIn.DefaultMappedModel).
@@ -282,6 +283,7 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
SetAllowMessagesDispatch(groupIn.AllowMessagesDispatch).
SetAllowLive(groupIn.AllowLive).
SetForceOpenaiFast(groupIn.ForceOpenAIFast).
SetFreeOpenaiFast(groupIn.FreeOpenAIFast).
SetRequireOauthOnly(groupIn.RequireOAuthOnly).
SetRequirePrivacySet(groupIn.RequirePrivacySet).
SetDefaultMappedModel(groupIn.DefaultMappedModel).
@@ -58,6 +58,7 @@ func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) {
// groups: OpenAI Live 与 Fast 强制策略都默认关闭,管理员显式开启后才生效。
requireColumn(t, tx, "groups", "allow_live", "boolean", 0, false)
requireColumn(t, tx, "groups", "force_openai_fast", "boolean", 0, false)
requireColumn(t, tx, "groups", "free_openai_fast", "boolean", 0, false)
// api_keys: key length should be 128
requireColumn(t, tx, "api_keys", "key", "character varying", 128, false)
+5
View File
@@ -303,6 +303,7 @@ func sanitizeGroupOpenAIFast(group *Group) {
if group == nil || !groupSupportsOpenAIFast(group.Platform) {
if group != nil {
group.ForceOpenAIFast = false
group.FreeOpenAIFast = false
}
}
}
@@ -517,6 +518,7 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn
AllowMessagesDispatch: input.AllowMessagesDispatch,
AllowLive: input.AllowLive,
ForceOpenAIFast: input.ForceOpenAIFast,
FreeOpenAIFast: input.FreeOpenAIFast,
RequireOAuthOnly: input.RequireOAuthOnly,
RequirePrivacySet: input.RequirePrivacySet,
DefaultMappedModel: input.DefaultMappedModel,
@@ -884,6 +886,9 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd
if input.ForceOpenAIFast != nil {
group.ForceOpenAIFast = *input.ForceOpenAIFast
}
if input.FreeOpenAIFast != nil {
group.FreeOpenAIFast = *input.FreeOpenAIFast
}
if input.RequireOAuthOnly != nil {
group.RequireOAuthOnly = *input.RequireOAuthOnly
}
@@ -145,6 +145,7 @@ func cloneGroupForDuplicate(source *Group, operationID string) *Group {
AllowMessagesDispatch: source.AllowMessagesDispatch,
AllowLive: source.AllowLive,
ForceOpenAIFast: source.ForceOpenAIFast,
FreeOpenAIFast: source.FreeOpenAIFast,
RequireOAuthOnly: source.RequireOAuthOnly,
RequirePrivacySet: source.RequirePrivacySet,
DefaultMappedModel: source.DefaultMappedModel,
@@ -167,6 +167,7 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
AllowMessagesDispatch: true,
AllowLive: true,
ForceOpenAIFast: true,
FreeOpenAIFast: true,
RequireOAuthOnly: true,
RequirePrivacySet: true,
DefaultMappedModel: "gpt-5.4",
@@ -215,6 +216,7 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
require.Equal(t, source.ModelRouting, duplicate.ModelRouting)
require.Equal(t, source.MessagesDispatchModelConfig, duplicate.MessagesDispatchModelConfig)
require.Equal(t, source.ForceOpenAIFast, duplicate.ForceOpenAIFast)
require.Equal(t, source.FreeOpenAIFast, duplicate.FreeOpenAIFast)
require.Equal(t, source.ModelsListConfig, duplicate.ModelsListConfig)
require.Equal(t, source.RPMLimit, duplicate.RPMLimit)
require.Equal(t, source.MaxReasoningEffort, duplicate.MaxReasoningEffort)
@@ -267,6 +267,7 @@ type CreateGroupInput struct {
AllowMessagesDispatch bool
AllowLive bool
ForceOpenAIFast bool
FreeOpenAIFast bool
DefaultMappedModel string
RequireOAuthOnly bool
RequirePrivacySet bool
@@ -345,6 +346,7 @@ type UpdateGroupInput struct {
AllowMessagesDispatch *bool
AllowLive *bool
ForceOpenAIFast *bool
FreeOpenAIFast *bool
DefaultMappedModel *string
RequireOAuthOnly *bool
RequirePrivacySet *bool
@@ -1099,19 +1099,20 @@ func TestAdminService_CreateGroup_NormalizesForceOpenAIFastByPlatform(t *testing
svc := &adminServiceImpl{groupRepo: repo}
group, err := svc.CreateGroup(context.Background(), &CreateGroupInput{
Name: "fast-" + tt.name, Platform: tt.platform, RateMultiplier: 1, ForceOpenAIFast: true,
Name: "fast-" + tt.name, Platform: tt.platform, RateMultiplier: 1, ForceOpenAIFast: true, FreeOpenAIFast: true,
})
require.NoError(t, err)
require.NotNil(t, group)
require.Equal(t, tt.want, repo.created.ForceOpenAIFast)
require.Equal(t, tt.want, repo.created.FreeOpenAIFast)
})
}
}
func TestAdminService_UpdateGroup_ClearsForceOpenAIFastWhenPlatformChanges(t *testing.T) {
existingGroup := &Group{
ID: 1, Name: "existing-fast", Platform: PlatformOpenAI, Status: StatusActive, ForceOpenAIFast: true,
ID: 1, Name: "existing-fast", Platform: PlatformOpenAI, Status: StatusActive, ForceOpenAIFast: true, FreeOpenAIFast: true,
}
repo := &groupRepoStubForAdmin{getByID: existingGroup}
svc := &adminServiceImpl{groupRepo: repo}
@@ -1123,6 +1124,7 @@ func TestAdminService_UpdateGroup_ClearsForceOpenAIFastWhenPlatformChanges(t *te
require.NoError(t, err)
require.NotNil(t, group)
require.False(t, repo.updated.ForceOpenAIFast)
require.False(t, repo.updated.FreeOpenAIFast)
}
func TestAdminService_UpdateGroup_ForceOpenAIFastInvalidatesAuthCache(t *testing.T) {
@@ -1136,11 +1138,13 @@ func TestAdminService_UpdateGroup_ForceOpenAIFastInvalidatesAuthCache(t *testing
group, err := svc.UpdateGroup(context.Background(), existingGroup.ID, &UpdateGroupInput{
ForceOpenAIFast: &enabled,
FreeOpenAIFast: &enabled,
})
require.NoError(t, err)
require.NotNil(t, group)
require.True(t, repo.updated.ForceOpenAIFast)
require.True(t, repo.updated.FreeOpenAIFast)
require.Equal(t, []int64{existingGroup.ID}, invalidator.groupIDs)
}
@@ -104,6 +104,7 @@ type APIKeyAuthGroupSnapshot struct {
AllowMessagesDispatch bool `json:"allow_messages_dispatch"`
AllowLive bool `json:"allow_live"`
ForceOpenAIFast bool `json:"force_openai_fast"`
FreeOpenAIFast bool `json:"free_openai_fast"`
DefaultMappedModel string `json:"default_mapped_model,omitempty"`
MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config,omitempty"`
ModelsListConfig GroupModelsListConfig `json:"models_list_config,omitempty"`
@@ -14,7 +14,7 @@ import (
"github.com/dgraph-io/ristretto"
)
const apiKeyAuthSnapshotVersion = 21 // v21: group force_openai_fast field
const apiKeyAuthSnapshotVersion = 22 // v22: group free_openai_fast field
type apiKeyAuthCacheConfig struct {
l1Size int
@@ -419,6 +419,7 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey)
AllowMessagesDispatch: apiKey.Group.AllowMessagesDispatch,
AllowLive: apiKey.Group.AllowLive,
ForceOpenAIFast: apiKey.Group.ForceOpenAIFast,
FreeOpenAIFast: apiKey.Group.FreeOpenAIFast,
DefaultMappedModel: apiKey.Group.DefaultMappedModel,
MessagesDispatchModelConfig: apiKey.Group.MessagesDispatchModelConfig,
ModelsListConfig: apiKey.Group.ModelsListConfig,
@@ -519,6 +520,7 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho
AllowMessagesDispatch: snapshot.Group.AllowMessagesDispatch,
AllowLive: snapshot.Group.AllowLive,
ForceOpenAIFast: snapshot.Group.ForceOpenAIFast,
FreeOpenAIFast: snapshot.Group.FreeOpenAIFast,
DefaultMappedModel: snapshot.Group.DefaultMappedModel,
MessagesDispatchModelConfig: snapshot.Group.MessagesDispatchModelConfig,
ModelsListConfig: snapshot.Group.ModelsListConfig,
@@ -15,7 +15,7 @@ func TestAPIKeyAuthSnapshotGroupForceOpenAIFastRoundtrip(t *testing.T) {
User: &User{ID: 40, Status: StatusActive},
Group: &Group{
ID: groupID, Name: "fast-roundtrip", Platform: PlatformOpenAI, Status: StatusActive,
Hydrated: true, ForceOpenAIFast: true,
Hydrated: true, ForceOpenAIFast: true, FreeOpenAIFast: true,
},
}
svc := &APIKeyService{}
@@ -31,5 +31,6 @@ func TestAPIKeyAuthSnapshotGroupForceOpenAIFastRoundtrip(t *testing.T) {
require.NotNil(t, materialized.Group)
require.True(t, materialized.Group.Hydrated)
require.True(t, materialized.Group.ForceOpenAIFast)
require.Equal(t, 21, cached.Snapshot.Version)
require.True(t, materialized.Group.FreeOpenAIFast)
require.Equal(t, 22, cached.Snapshot.Version)
}
@@ -53,7 +53,7 @@ func TestAPIKeyAuthSnapshotProfitControlRoundtrip(t *testing.T) {
snapshot := svc.snapshotFromAPIKey(context.Background(), apiKey)
require.NotNil(t, snapshot)
require.Equal(t, apiKeyAuthSnapshotVersion, snapshot.Version)
require.Equal(t, 21, snapshot.Version, "v21 起认证快照携带分组 Fast 强制开关")
require.Equal(t, 22, snapshot.Version, "v22 起认证快照携带分组免费 Fast 开关")
// 模拟 L2 缓存的完整 JSON 往返(与 apiKeyCache.SetAuthCache/GetAuthCache 同构)。
payload, err := json.Marshal(&APIKeyAuthCacheEntry{Snapshot: snapshot})
+1
View File
@@ -101,6 +101,7 @@ type Group struct {
AllowMessagesDispatch bool
AllowLive bool
ForceOpenAIFast bool // 强制 OpenAI 网关请求使用 service_tier=priority
FreeOpenAIFast bool // OpenAI Fast 请求按 Standard 价格向用户计费
RequireOAuthOnly bool // 仅允许非 apikey 类型账号关联(OpenAI/Antigravity/Anthropic/Gemini)
RequirePrivacySet bool // 调度时仅允许 privacy 已成功设置的账号(OpenAI/Antigravity/Anthropic/Gemini)
DefaultMappedModel string
@@ -3081,6 +3081,77 @@ func TestOpenAIGatewayServiceRecordUsage_ShadowUsesParentCredentialTierContract(
require.InDelta(t, fastCost.TotalCost, usageRepo.lastLog.TotalCost, 1e-10)
}
func TestOpenAIGatewayServiceRecordUsage_FreeOpenAIFastChargesStandard(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
svc := newOpenAIRecordUsageServiceForTest(
usageRepo,
&openAIRecordUsageUserRepoStub{},
&openAIRecordUsageSubRepoStub{},
nil,
)
svc.resolver = NewModelPricingResolver(nil, svc.billingService)
groupID := int64(77)
serviceTier := "priority"
tokens := UsageTokens{InputTokens: 100, OutputTokens: 50}
inputPrice := 0.001
outputPrice := 0.002
fastMultiplier := 3.0
apiKey := &APIKey{
ID: 1020,
GroupID: &groupID,
Group: &Group{
ID: groupID, Platform: PlatformOpenAI, Status: StatusActive,
Hydrated: true, RateMultiplier: 0.5, FreeOpenAIFast: true,
ModelPricing: []ChannelModelPricing{{
Models: []string{"gpt-5.6-sol"},
BillingMode: BillingModeToken,
InputPrice: &inputPrice,
OutputPrice: &outputPrice,
FastMultiplier: &fastMultiplier,
}},
},
}
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "resp_free_fast",
ServiceTier: &serviceTier,
UpstreamResponseServiceTier: "default",
Usage: OpenAIUsage{InputTokens: tokens.InputTokens, OutputTokens: tokens.OutputTokens},
Model: "gpt-5.6-sol",
Duration: time.Second,
},
APIKey: apiKey,
User: &User{ID: 2020},
Account: &Account{ID: 3020, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
})
require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog)
require.NotNil(t, usageRepo.lastLog.ServiceTier)
require.Equal(t, "priority", *usageRepo.lastLog.ServiceTier)
require.InDelta(t, 0.5, usageRepo.lastLog.RateMultiplier, 1e-12)
standardTotal := float64(tokens.InputTokens)*inputPrice + float64(tokens.OutputTokens)*outputPrice
require.InDelta(t, standardTotal*fastMultiplier, usageRepo.lastLog.TotalCost, 1e-10)
require.InDelta(t, standardTotal*0.5, usageRepo.lastLog.ActualCost, 1e-10)
}
func TestGroupBillsOpenAIFastAtStandardRequiresOpenAIAccount(t *testing.T) {
apiKey := &APIKey{Group: &Group{Platform: PlatformComposite, FreeOpenAIFast: true}}
require.True(t, groupBillsOpenAIFastAtStandard(
apiKey,
&Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
"priority",
))
require.False(t, groupBillsOpenAIFastAtStandard(
apiKey,
&Account{Platform: PlatformGrok, Type: AccountTypeAPIKey},
"priority",
))
}
func TestOpenAIGatewayServiceRecordUsage_ServiceTierNeverRaisedByUpstreamResponse(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
@@ -132,6 +132,24 @@ func openAIUsagePricingAt(input *OpenAIRecordUsageInput) time.Time {
return timezone.Now()
}
func groupBillsOpenAIFastAtStandard(apiKey *APIKey, account *Account, serviceTier string) bool {
if apiKey == nil || apiKey.Group == nil || !apiKey.Group.FreeOpenAIFast {
return false
}
if account == nil || !account.IsOpenAI() {
return false
}
if !groupSupportsOpenAIFast(apiKey.Group.Platform) {
return false
}
switch normalizeBillingServiceTier(serviceTier) {
case "priority", "fast":
return true
default:
return false
}
}
// RecordUsage records usage and deducts balance
func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRecordUsageInput) error {
if input == nil {
@@ -277,6 +295,32 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
}
}
// Free Fast changes only the customer charge. Keep priority TotalCost and
// service_tier for upstream accounting, but evaluate ActualCost once more at
// the Standard tier using the same channel, peak, and long-context policy.
if groupBillsOpenAIFastAtStandard(apiKey, billingAccount, serviceTier) {
standardCost, standardErr := s.calculateOpenAIRecordUsageCost(
ctx,
result,
apiKey,
billingModels,
multiplier,
imageMultiplier,
videoMultiplier,
baseMultiplier,
tokens,
"",
longContextBillingGate,
pricingAt,
)
if standardErr != nil {
return standardErr
}
if cost != nil && standardCost != nil {
cost.ActualCost = standardCost.ActualCost
}
}
// Determine billing type
isSubscriptionBilling := subscription != nil && apiKey.Group != nil && apiKey.Group.IsSubscriptionType()
billingType := BillingTypeBalance
@@ -0,0 +1,5 @@
ALTER TABLE groups
ADD COLUMN IF NOT EXISTS free_openai_fast BOOLEAN NOT NULL DEFAULT FALSE;
COMMENT ON COLUMN groups.free_openai_fast IS
'Whether Fast/priority requests in this OpenAI/Composite group are billed to users at Standard price';
@@ -0,0 +1,18 @@
package migrations
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestGroupFreeOpenAIFastMigration(t *testing.T) {
content, err := FS.ReadFile("233_group_free_openai_fast.sql")
require.NoError(t, err)
sql := strings.Join(strings.Fields(string(content)), " ")
require.Contains(t, sql,
"ADD COLUMN IF NOT EXISTS free_openai_fast BOOLEAN NOT NULL DEFAULT FALSE")
require.Contains(t, sql, "COMMENT ON COLUMN groups.free_openai_fast")
}
@@ -1158,7 +1158,9 @@ export default {
openaiFast: {
title: 'OpenAI Fast mode',
force: 'Force Fast (priority)',
hint: 'Forces service_tier=priority on OpenAI requests in this group. The global Fast/Flex policy can still filter or block it. New requests update immediately after saving; existing WebSocket sessions must reconnect.'
hint: 'Forces service_tier=priority on OpenAI requests in this group. The global Fast/Flex policy can still filter or block it. New requests update immediately after saving; existing WebSocket sessions must reconnect.',
free: 'Free Fast',
freeHint: 'Fast requests in this group still use the priority tier, but customers are charged the equivalent Standard price.'
},
invalidRequestFallback: {
title: 'Invalid Request Fallback Group',
@@ -1156,7 +1156,9 @@ export default {
openaiFast: {
title: 'OpenAI Fast 模式',
force: '强制使用 Fast(priority)',
hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。'
hint: '开启后,此分组的 OpenAI 请求会强制写入 service_tier=priority;全局 Fast/Flex 策略仍可过滤或拦截。保存后新请求立即生效,已建立的 WebSocket 会话需重连。',
free: '免费 Fast',
freeHint: '该分组的 Fast 请求仍使用 priority 档位,但客户实际费用按同一请求的 Standard 价格计算。'
},
invalidRequestFallback: {
title: '无效请求兜底分组',
+3
View File
@@ -613,6 +613,7 @@ export interface Group {
export interface AdminGroup extends Group {
force_openai_fast: boolean
free_openai_fast: boolean
model_pricing: import('@/api/admin/channels').ChannelModelPricing[]
// 分组利润控制(openai/anthropic/gemini/grok/antigravity 分组可启用;margin/buffer 为小数存储)。
// 仅管理员可见:与 rate_multiplier 相乘即可反推上游成本上限,不得下放到 Group。
@@ -778,6 +779,7 @@ export interface CreateGroupRequest {
monthly_limit_usd?: number | null
long_context_pricing_enabled?: boolean
force_openai_fast?: boolean
free_openai_fast?: boolean
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
allow_image_generation?: boolean
allow_batch_image_generation?: boolean
@@ -842,6 +844,7 @@ export interface UpdateGroupRequest {
monthly_limit_usd?: number | null
long_context_pricing_enabled?: boolean
force_openai_fast?: boolean
free_openai_fast?: boolean
model_pricing?: import('@/api/admin/channels').ChannelModelPricing[]
allow_image_generation?: boolean
allow_batch_image_generation?: boolean
+71
View File
@@ -1619,6 +1619,35 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.hint") }}
</p>
<div class="flex items-center justify-between gap-4 mt-4">
<label class="text-sm text-gray-600 dark:text-gray-400">
{{ t("admin.groups.openaiFast.free") }}
</label>
<button
type="button"
role="switch"
:aria-checked="createForm.free_openai_fast"
:aria-label="t('admin.groups.openaiFast.free')"
data-testid="create-free-openai-fast"
@click="createForm.free_openai_fast = !createForm.free_openai_fast"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
createForm.free_openai_fast
? 'bg-emerald-500'
: 'bg-gray-300 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"
:class="
createForm.free_openai_fast ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.freeHint") }}
</p>
</div>
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
@@ -3387,6 +3416,35 @@
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.hint") }}
</p>
<div class="flex items-center justify-between gap-4 mt-4">
<label class="text-sm text-gray-600 dark:text-gray-400">
{{ t("admin.groups.openaiFast.free") }}
</label>
<button
type="button"
role="switch"
:aria-checked="editForm.free_openai_fast"
:aria-label="t('admin.groups.openaiFast.free')"
data-testid="edit-free-openai-fast"
@click="editForm.free_openai_fast = !editForm.free_openai_fast"
class="relative inline-flex h-6 w-12 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
:class="
editForm.free_openai_fast
? 'bg-emerald-500'
: 'bg-gray-300 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"
:class="
editForm.free_openai_fast ? 'translate-x-6' : 'translate-x-1'
"
/>
</button>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ t("admin.groups.openaiFast.freeHint") }}
</p>
</div>
<!-- Codex Live 开关(OpenAI 与 Composite 平台) -->
@@ -5137,6 +5195,7 @@ const createForm = reactive({
monthly_limit_usd: null as number | null,
long_context_pricing_enabled: true,
force_openai_fast: false,
free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5500,6 +5559,7 @@ const editForm = reactive({
monthly_limit_usd: null as number | null,
long_context_pricing_enabled: true,
force_openai_fast: false,
free_openai_fast: false,
model_pricing: [] as PricingFormEntry[],
// 图片生成计费配置
allow_image_generation: false,
@@ -5976,6 +6036,7 @@ const closeCreateModal = () => {
createForm.video_model_prices = createVideoModelPricesForm();
createForm.long_context_pricing_enabled = true;
createForm.force_openai_fast = false;
createForm.free_openai_fast = false;
createForm.model_pricing = [];
createForm.web_search_price_per_call = null;
createForm.search_price_per_1k = null;
@@ -6081,6 +6142,10 @@ const handleCreateGroup = async () => {
createForm.platform,
createForm.force_openai_fast,
),
free_openai_fast: normalizeGroupOpenAIFast(
createForm.platform,
createForm.free_openai_fast,
),
model_pricing: groupPricingToAPI(
createForm.model_pricing,
createForm.platform,
@@ -6210,6 +6275,7 @@ const handleEdit = async (group: AdminGroup) => {
editForm.long_context_pricing_enabled =
group.long_context_pricing_enabled ?? true;
editForm.force_openai_fast = group.force_openai_fast ?? false;
editForm.free_openai_fast = group.free_openai_fast ?? false;
editForm.model_pricing = groupPricingFromAPI(group.model_pricing);
editForm.allow_image_generation = group.allow_image_generation ?? false;
editForm.allow_batch_image_generation =
@@ -6321,6 +6387,7 @@ const closeEditModal = () => {
editForm.video_model_prices = createVideoModelPricesForm();
editForm.long_context_pricing_enabled = true;
editForm.force_openai_fast = false;
editForm.free_openai_fast = false;
editForm.model_pricing = [];
editForm.web_search_price_per_call = null;
editForm.search_price_per_1k = null;
@@ -6358,6 +6425,10 @@ const handleUpdateGroup = async () => {
editForm.platform,
editForm.force_openai_fast,
),
free_openai_fast: normalizeGroupOpenAIFast(
editForm.platform,
editForm.free_openai_fast,
),
model_pricing: groupPricingToAPI(
editForm.model_pricing,
editForm.platform,
@@ -26,11 +26,15 @@ describe("groupsOpenAIFast", () => {
title: expect.any(String),
force: expect.any(String),
hint: expect.stringContaining("service_tier=priority"),
free: expect.any(String),
freeHint: expect.stringContaining("Standard"),
});
expect(en.groups.openaiFast).toMatchObject({
title: expect.any(String),
force: expect.any(String),
hint: expect.stringContaining("service_tier=priority"),
free: expect.any(String),
freeHint: expect.stringContaining("Standard"),
});
});
});