diff --git a/backend/ent/group.go b/backend/ent/group.go index 2a0eb4d3ac..5bec594977 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -73,6 +73,16 @@ type Group struct { BatchImageDiscountMultiplier float64 `json:"batch_image_discount_multiplier,omitempty"` // 批量图片生成冻结价格比例,按普通生图原价乘以该比例冻结,结算后释放差额 BatchImageHoldMultiplier float64 `json:"batch_image_hold_multiplier,omitempty"` + // 视频生成是否使用独立倍率;false 表示共享分组有效倍率 + VideoRateIndependent bool `json:"video_rate_independent,omitempty"` + // 视频生成独立倍率,仅 video_rate_independent=true 时生效 + VideoRateMultiplier float64 `json:"video_rate_multiplier,omitempty"` + // VideoPrice480p holds the value of the "video_price_480p" field. + VideoPrice480p *float64 `json:"video_price_480p,omitempty"` + // VideoPrice720p holds the value of the "video_price_720p" field. + VideoPrice720p *float64 `json:"video_price_720p,omitempty"` + // VideoPrice1080p holds the value of the "video_price_1080p" field. + VideoPrice1080p *float64 `json:"video_price_1080p,omitempty"` // 是否仅允许 Claude Code 客户端 ClaudeCodeOnly bool `json:"claude_code_only,omitempty"` // 非 Claude Code 请求降级使用的分组 ID @@ -211,9 +221,9 @@ func (*Group) scanValues(columns []string) ([]any, error) { switch columns[i] { case group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig: values[i] = new([]byte) - case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet: + case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet: 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: + 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: values[i] = new(sql.NullFloat64) case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit: values[i] = new(sql.NullInt64) @@ -412,6 +422,39 @@ func (_m *Group) assignValues(columns []string, values []any) error { } else if value.Valid { _m.BatchImageHoldMultiplier = value.Float64 } + case group.FieldVideoRateIndependent: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field video_rate_independent", values[i]) + } else if value.Valid { + _m.VideoRateIndependent = value.Bool + } + case group.FieldVideoRateMultiplier: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field video_rate_multiplier", values[i]) + } else if value.Valid { + _m.VideoRateMultiplier = value.Float64 + } + case group.FieldVideoPrice480p: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field video_price_480p", values[i]) + } else if value.Valid { + _m.VideoPrice480p = new(float64) + *_m.VideoPrice480p = value.Float64 + } + case group.FieldVideoPrice720p: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field video_price_720p", values[i]) + } else if value.Valid { + _m.VideoPrice720p = new(float64) + *_m.VideoPrice720p = value.Float64 + } + case group.FieldVideoPrice1080p: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field video_price_1080p", values[i]) + } else if value.Valid { + _m.VideoPrice1080p = new(float64) + *_m.VideoPrice1080p = value.Float64 + } case group.FieldClaudeCodeOnly: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field claude_code_only", values[i]) @@ -685,6 +728,27 @@ func (_m *Group) String() string { builder.WriteString("batch_image_hold_multiplier=") builder.WriteString(fmt.Sprintf("%v", _m.BatchImageHoldMultiplier)) builder.WriteString(", ") + builder.WriteString("video_rate_independent=") + builder.WriteString(fmt.Sprintf("%v", _m.VideoRateIndependent)) + builder.WriteString(", ") + builder.WriteString("video_rate_multiplier=") + builder.WriteString(fmt.Sprintf("%v", _m.VideoRateMultiplier)) + builder.WriteString(", ") + if v := _m.VideoPrice480p; v != nil { + builder.WriteString("video_price_480p=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.VideoPrice720p; v != nil { + builder.WriteString("video_price_720p=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.VideoPrice1080p; v != nil { + builder.WriteString("video_price_1080p=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") builder.WriteString("claude_code_only=") builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly)) builder.WriteString(", ") diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 540ce8f9f5..769c63e6b1 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -70,6 +70,16 @@ const ( FieldBatchImageDiscountMultiplier = "batch_image_discount_multiplier" // FieldBatchImageHoldMultiplier holds the string denoting the batch_image_hold_multiplier field in the database. FieldBatchImageHoldMultiplier = "batch_image_hold_multiplier" + // FieldVideoRateIndependent holds the string denoting the video_rate_independent field in the database. + FieldVideoRateIndependent = "video_rate_independent" + // FieldVideoRateMultiplier holds the string denoting the video_rate_multiplier field in the database. + FieldVideoRateMultiplier = "video_rate_multiplier" + // FieldVideoPrice480p holds the string denoting the video_price_480p field in the database. + FieldVideoPrice480p = "video_price_480p" + // FieldVideoPrice720p holds the string denoting the video_price_720p field in the database. + FieldVideoPrice720p = "video_price_720p" + // FieldVideoPrice1080p holds the string denoting the video_price_1080p field in the database. + FieldVideoPrice1080p = "video_price_1080p" // FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database. FieldClaudeCodeOnly = "claude_code_only" // FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database. @@ -202,6 +212,11 @@ var Columns = []string{ FieldImagePrice4k, FieldBatchImageDiscountMultiplier, FieldBatchImageHoldMultiplier, + FieldVideoRateIndependent, + FieldVideoRateMultiplier, + FieldVideoPrice480p, + FieldVideoPrice720p, + FieldVideoPrice1080p, FieldClaudeCodeOnly, FieldFallbackGroupID, FieldFallbackGroupIDOnInvalidRequest, @@ -296,6 +311,10 @@ var ( DefaultBatchImageDiscountMultiplier float64 // DefaultBatchImageHoldMultiplier holds the default value on creation for the "batch_image_hold_multiplier" field. DefaultBatchImageHoldMultiplier float64 + // DefaultVideoRateIndependent holds the default value on creation for the "video_rate_independent" field. + DefaultVideoRateIndependent bool + // DefaultVideoRateMultiplier holds the default value on creation for the "video_rate_multiplier" field. + DefaultVideoRateMultiplier float64 // DefaultClaudeCodeOnly holds the default value on creation for the "claude_code_only" field. DefaultClaudeCodeOnly bool // DefaultModelRoutingEnabled holds the default value on creation for the "model_routing_enabled" field. @@ -467,6 +486,31 @@ func ByBatchImageHoldMultiplier(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldBatchImageHoldMultiplier, opts...).ToFunc() } +// ByVideoRateIndependent orders the results by the video_rate_independent field. +func ByVideoRateIndependent(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVideoRateIndependent, opts...).ToFunc() +} + +// ByVideoRateMultiplier orders the results by the video_rate_multiplier field. +func ByVideoRateMultiplier(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVideoRateMultiplier, opts...).ToFunc() +} + +// ByVideoPrice480p orders the results by the video_price_480p field. +func ByVideoPrice480p(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVideoPrice480p, opts...).ToFunc() +} + +// ByVideoPrice720p orders the results by the video_price_720p field. +func ByVideoPrice720p(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVideoPrice720p, opts...).ToFunc() +} + +// ByVideoPrice1080p orders the results by the video_price_1080p field. +func ByVideoPrice1080p(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldVideoPrice1080p, opts...).ToFunc() +} + // ByClaudeCodeOnly orders the results by the claude_code_only field. func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc() diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index a76d3a8783..5a9d92d0f4 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -190,6 +190,31 @@ func BatchImageHoldMultiplier(v float64) predicate.Group { return predicate.Group(sql.FieldEQ(FieldBatchImageHoldMultiplier, v)) } +// VideoRateIndependent applies equality check predicate on the "video_rate_independent" field. It's identical to VideoRateIndependentEQ. +func VideoRateIndependent(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoRateIndependent, v)) +} + +// VideoRateMultiplier applies equality check predicate on the "video_rate_multiplier" field. It's identical to VideoRateMultiplierEQ. +func VideoRateMultiplier(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoRateMultiplier, v)) +} + +// VideoPrice480p applies equality check predicate on the "video_price_480p" field. It's identical to VideoPrice480pEQ. +func VideoPrice480p(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice480p, v)) +} + +// VideoPrice720p applies equality check predicate on the "video_price_720p" field. It's identical to VideoPrice720pEQ. +func VideoPrice720p(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice720p, v)) +} + +// VideoPrice1080p applies equality check predicate on the "video_price_1080p" field. It's identical to VideoPrice1080pEQ. +func VideoPrice1080p(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice1080p, v)) +} + // ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ. func ClaudeCodeOnly(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v)) @@ -1430,6 +1455,206 @@ func BatchImageHoldMultiplierLTE(v float64) predicate.Group { return predicate.Group(sql.FieldLTE(FieldBatchImageHoldMultiplier, v)) } +// VideoRateIndependentEQ applies the EQ predicate on the "video_rate_independent" field. +func VideoRateIndependentEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoRateIndependent, v)) +} + +// VideoRateIndependentNEQ applies the NEQ predicate on the "video_rate_independent" field. +func VideoRateIndependentNEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldVideoRateIndependent, v)) +} + +// VideoRateMultiplierEQ applies the EQ predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoRateMultiplier, v)) +} + +// VideoRateMultiplierNEQ applies the NEQ predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldVideoRateMultiplier, v)) +} + +// VideoRateMultiplierIn applies the In predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldVideoRateMultiplier, vs...)) +} + +// VideoRateMultiplierNotIn applies the NotIn predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldVideoRateMultiplier, vs...)) +} + +// VideoRateMultiplierGT applies the GT predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldVideoRateMultiplier, v)) +} + +// VideoRateMultiplierGTE applies the GTE predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldVideoRateMultiplier, v)) +} + +// VideoRateMultiplierLT applies the LT predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldVideoRateMultiplier, v)) +} + +// VideoRateMultiplierLTE applies the LTE predicate on the "video_rate_multiplier" field. +func VideoRateMultiplierLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldVideoRateMultiplier, v)) +} + +// VideoPrice480pEQ applies the EQ predicate on the "video_price_480p" field. +func VideoPrice480pEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice480p, v)) +} + +// VideoPrice480pNEQ applies the NEQ predicate on the "video_price_480p" field. +func VideoPrice480pNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldVideoPrice480p, v)) +} + +// VideoPrice480pIn applies the In predicate on the "video_price_480p" field. +func VideoPrice480pIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldVideoPrice480p, vs...)) +} + +// VideoPrice480pNotIn applies the NotIn predicate on the "video_price_480p" field. +func VideoPrice480pNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldVideoPrice480p, vs...)) +} + +// VideoPrice480pGT applies the GT predicate on the "video_price_480p" field. +func VideoPrice480pGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldVideoPrice480p, v)) +} + +// VideoPrice480pGTE applies the GTE predicate on the "video_price_480p" field. +func VideoPrice480pGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldVideoPrice480p, v)) +} + +// VideoPrice480pLT applies the LT predicate on the "video_price_480p" field. +func VideoPrice480pLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldVideoPrice480p, v)) +} + +// VideoPrice480pLTE applies the LTE predicate on the "video_price_480p" field. +func VideoPrice480pLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldVideoPrice480p, v)) +} + +// VideoPrice480pIsNil applies the IsNil predicate on the "video_price_480p" field. +func VideoPrice480pIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldVideoPrice480p)) +} + +// VideoPrice480pNotNil applies the NotNil predicate on the "video_price_480p" field. +func VideoPrice480pNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldVideoPrice480p)) +} + +// VideoPrice720pEQ applies the EQ predicate on the "video_price_720p" field. +func VideoPrice720pEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice720p, v)) +} + +// VideoPrice720pNEQ applies the NEQ predicate on the "video_price_720p" field. +func VideoPrice720pNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldVideoPrice720p, v)) +} + +// VideoPrice720pIn applies the In predicate on the "video_price_720p" field. +func VideoPrice720pIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldVideoPrice720p, vs...)) +} + +// VideoPrice720pNotIn applies the NotIn predicate on the "video_price_720p" field. +func VideoPrice720pNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldVideoPrice720p, vs...)) +} + +// VideoPrice720pGT applies the GT predicate on the "video_price_720p" field. +func VideoPrice720pGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldVideoPrice720p, v)) +} + +// VideoPrice720pGTE applies the GTE predicate on the "video_price_720p" field. +func VideoPrice720pGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldVideoPrice720p, v)) +} + +// VideoPrice720pLT applies the LT predicate on the "video_price_720p" field. +func VideoPrice720pLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldVideoPrice720p, v)) +} + +// VideoPrice720pLTE applies the LTE predicate on the "video_price_720p" field. +func VideoPrice720pLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldVideoPrice720p, v)) +} + +// VideoPrice720pIsNil applies the IsNil predicate on the "video_price_720p" field. +func VideoPrice720pIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldVideoPrice720p)) +} + +// VideoPrice720pNotNil applies the NotNil predicate on the "video_price_720p" field. +func VideoPrice720pNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldVideoPrice720p)) +} + +// VideoPrice1080pEQ applies the EQ predicate on the "video_price_1080p" field. +func VideoPrice1080pEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pNEQ applies the NEQ predicate on the "video_price_1080p" field. +func VideoPrice1080pNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pIn applies the In predicate on the "video_price_1080p" field. +func VideoPrice1080pIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldVideoPrice1080p, vs...)) +} + +// VideoPrice1080pNotIn applies the NotIn predicate on the "video_price_1080p" field. +func VideoPrice1080pNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldVideoPrice1080p, vs...)) +} + +// VideoPrice1080pGT applies the GT predicate on the "video_price_1080p" field. +func VideoPrice1080pGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pGTE applies the GTE predicate on the "video_price_1080p" field. +func VideoPrice1080pGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pLT applies the LT predicate on the "video_price_1080p" field. +func VideoPrice1080pLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pLTE applies the LTE predicate on the "video_price_1080p" field. +func VideoPrice1080pLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldVideoPrice1080p, v)) +} + +// VideoPrice1080pIsNil applies the IsNil predicate on the "video_price_1080p" field. +func VideoPrice1080pIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldVideoPrice1080p)) +} + +// VideoPrice1080pNotNil applies the NotNil predicate on the "video_price_1080p" field. +func VideoPrice1080pNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldVideoPrice1080p)) +} + // ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field. func ClaudeCodeOnlyEQ(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v)) diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 9c635847d0..2a6c18e67d 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -399,6 +399,76 @@ func (_c *GroupCreate) SetNillableBatchImageHoldMultiplier(v *float64) *GroupCre return _c } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (_c *GroupCreate) SetVideoRateIndependent(v bool) *GroupCreate { + _c.mutation.SetVideoRateIndependent(v) + return _c +} + +// SetNillableVideoRateIndependent sets the "video_rate_independent" field if the given value is not nil. +func (_c *GroupCreate) SetNillableVideoRateIndependent(v *bool) *GroupCreate { + if v != nil { + _c.SetVideoRateIndependent(*v) + } + return _c +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (_c *GroupCreate) SetVideoRateMultiplier(v float64) *GroupCreate { + _c.mutation.SetVideoRateMultiplier(v) + return _c +} + +// SetNillableVideoRateMultiplier sets the "video_rate_multiplier" field if the given value is not nil. +func (_c *GroupCreate) SetNillableVideoRateMultiplier(v *float64) *GroupCreate { + if v != nil { + _c.SetVideoRateMultiplier(*v) + } + return _c +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (_c *GroupCreate) SetVideoPrice480p(v float64) *GroupCreate { + _c.mutation.SetVideoPrice480p(v) + return _c +} + +// SetNillableVideoPrice480p sets the "video_price_480p" field if the given value is not nil. +func (_c *GroupCreate) SetNillableVideoPrice480p(v *float64) *GroupCreate { + if v != nil { + _c.SetVideoPrice480p(*v) + } + return _c +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (_c *GroupCreate) SetVideoPrice720p(v float64) *GroupCreate { + _c.mutation.SetVideoPrice720p(v) + return _c +} + +// SetNillableVideoPrice720p sets the "video_price_720p" field if the given value is not nil. +func (_c *GroupCreate) SetNillableVideoPrice720p(v *float64) *GroupCreate { + if v != nil { + _c.SetVideoPrice720p(*v) + } + return _c +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (_c *GroupCreate) SetVideoPrice1080p(v float64) *GroupCreate { + _c.mutation.SetVideoPrice1080p(v) + return _c +} + +// SetNillableVideoPrice1080p sets the "video_price_1080p" field if the given value is not nil. +func (_c *GroupCreate) SetNillableVideoPrice1080p(v *float64) *GroupCreate { + if v != nil { + _c.SetVideoPrice1080p(*v) + } + return _c +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate { _c.mutation.SetClaudeCodeOnly(v) @@ -798,6 +868,14 @@ func (_c *GroupCreate) defaults() error { v := group.DefaultBatchImageHoldMultiplier _c.mutation.SetBatchImageHoldMultiplier(v) } + if _, ok := _c.mutation.VideoRateIndependent(); !ok { + v := group.DefaultVideoRateIndependent + _c.mutation.SetVideoRateIndependent(v) + } + if _, ok := _c.mutation.VideoRateMultiplier(); !ok { + v := group.DefaultVideoRateMultiplier + _c.mutation.SetVideoRateMultiplier(v) + } if _, ok := _c.mutation.ClaudeCodeOnly(); !ok { v := group.DefaultClaudeCodeOnly _c.mutation.SetClaudeCodeOnly(v) @@ -938,6 +1016,12 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.BatchImageHoldMultiplier(); !ok { return &ValidationError{Name: "batch_image_hold_multiplier", err: errors.New(`ent: missing required field "Group.batch_image_hold_multiplier"`)} } + if _, ok := _c.mutation.VideoRateIndependent(); !ok { + return &ValidationError{Name: "video_rate_independent", err: errors.New(`ent: missing required field "Group.video_rate_independent"`)} + } + if _, ok := _c.mutation.VideoRateMultiplier(); !ok { + return &ValidationError{Name: "video_rate_multiplier", err: errors.New(`ent: missing required field "Group.video_rate_multiplier"`)} + } if _, ok := _c.mutation.ClaudeCodeOnly(); !ok { return &ValidationError{Name: "claude_code_only", err: errors.New(`ent: missing required field "Group.claude_code_only"`)} } @@ -1114,6 +1198,26 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldBatchImageHoldMultiplier, field.TypeFloat64, value) _node.BatchImageHoldMultiplier = value } + if value, ok := _c.mutation.VideoRateIndependent(); ok { + _spec.SetField(group.FieldVideoRateIndependent, field.TypeBool, value) + _node.VideoRateIndependent = value + } + if value, ok := _c.mutation.VideoRateMultiplier(); ok { + _spec.SetField(group.FieldVideoRateMultiplier, field.TypeFloat64, value) + _node.VideoRateMultiplier = value + } + if value, ok := _c.mutation.VideoPrice480p(); ok { + _spec.SetField(group.FieldVideoPrice480p, field.TypeFloat64, value) + _node.VideoPrice480p = &value + } + if value, ok := _c.mutation.VideoPrice720p(); ok { + _spec.SetField(group.FieldVideoPrice720p, field.TypeFloat64, value) + _node.VideoPrice720p = &value + } + if value, ok := _c.mutation.VideoPrice1080p(); ok { + _spec.SetField(group.FieldVideoPrice1080p, field.TypeFloat64, value) + _node.VideoPrice1080p = &value + } if value, ok := _c.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) _node.ClaudeCodeOnly = value @@ -1762,6 +1866,108 @@ func (u *GroupUpsert) AddBatchImageHoldMultiplier(v float64) *GroupUpsert { return u } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (u *GroupUpsert) SetVideoRateIndependent(v bool) *GroupUpsert { + u.Set(group.FieldVideoRateIndependent, v) + return u +} + +// UpdateVideoRateIndependent sets the "video_rate_independent" field to the value that was provided on create. +func (u *GroupUpsert) UpdateVideoRateIndependent() *GroupUpsert { + u.SetExcluded(group.FieldVideoRateIndependent) + return u +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (u *GroupUpsert) SetVideoRateMultiplier(v float64) *GroupUpsert { + u.Set(group.FieldVideoRateMultiplier, v) + return u +} + +// UpdateVideoRateMultiplier sets the "video_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsert) UpdateVideoRateMultiplier() *GroupUpsert { + u.SetExcluded(group.FieldVideoRateMultiplier) + return u +} + +// AddVideoRateMultiplier adds v to the "video_rate_multiplier" field. +func (u *GroupUpsert) AddVideoRateMultiplier(v float64) *GroupUpsert { + u.Add(group.FieldVideoRateMultiplier, v) + return u +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (u *GroupUpsert) SetVideoPrice480p(v float64) *GroupUpsert { + u.Set(group.FieldVideoPrice480p, v) + return u +} + +// UpdateVideoPrice480p sets the "video_price_480p" field to the value that was provided on create. +func (u *GroupUpsert) UpdateVideoPrice480p() *GroupUpsert { + u.SetExcluded(group.FieldVideoPrice480p) + return u +} + +// AddVideoPrice480p adds v to the "video_price_480p" field. +func (u *GroupUpsert) AddVideoPrice480p(v float64) *GroupUpsert { + u.Add(group.FieldVideoPrice480p, v) + return u +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (u *GroupUpsert) ClearVideoPrice480p() *GroupUpsert { + u.SetNull(group.FieldVideoPrice480p) + return u +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (u *GroupUpsert) SetVideoPrice720p(v float64) *GroupUpsert { + u.Set(group.FieldVideoPrice720p, v) + return u +} + +// UpdateVideoPrice720p sets the "video_price_720p" field to the value that was provided on create. +func (u *GroupUpsert) UpdateVideoPrice720p() *GroupUpsert { + u.SetExcluded(group.FieldVideoPrice720p) + return u +} + +// AddVideoPrice720p adds v to the "video_price_720p" field. +func (u *GroupUpsert) AddVideoPrice720p(v float64) *GroupUpsert { + u.Add(group.FieldVideoPrice720p, v) + return u +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (u *GroupUpsert) ClearVideoPrice720p() *GroupUpsert { + u.SetNull(group.FieldVideoPrice720p) + return u +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (u *GroupUpsert) SetVideoPrice1080p(v float64) *GroupUpsert { + u.Set(group.FieldVideoPrice1080p, v) + return u +} + +// UpdateVideoPrice1080p sets the "video_price_1080p" field to the value that was provided on create. +func (u *GroupUpsert) UpdateVideoPrice1080p() *GroupUpsert { + u.SetExcluded(group.FieldVideoPrice1080p) + return u +} + +// AddVideoPrice1080p adds v to the "video_price_1080p" field. +func (u *GroupUpsert) AddVideoPrice1080p(v float64) *GroupUpsert { + u.Add(group.FieldVideoPrice1080p, v) + return u +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (u *GroupUpsert) ClearVideoPrice1080p() *GroupUpsert { + u.SetNull(group.FieldVideoPrice1080p) + return u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert { u.Set(group.FieldClaudeCodeOnly, v) @@ -2533,6 +2739,125 @@ func (u *GroupUpsertOne) UpdateBatchImageHoldMultiplier() *GroupUpsertOne { }) } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (u *GroupUpsertOne) SetVideoRateIndependent(v bool) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetVideoRateIndependent(v) + }) +} + +// UpdateVideoRateIndependent sets the "video_rate_independent" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateVideoRateIndependent() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoRateIndependent() + }) +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (u *GroupUpsertOne) SetVideoRateMultiplier(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetVideoRateMultiplier(v) + }) +} + +// AddVideoRateMultiplier adds v to the "video_rate_multiplier" field. +func (u *GroupUpsertOne) AddVideoRateMultiplier(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddVideoRateMultiplier(v) + }) +} + +// UpdateVideoRateMultiplier sets the "video_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateVideoRateMultiplier() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoRateMultiplier() + }) +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (u *GroupUpsertOne) SetVideoPrice480p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice480p(v) + }) +} + +// AddVideoPrice480p adds v to the "video_price_480p" field. +func (u *GroupUpsertOne) AddVideoPrice480p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice480p(v) + }) +} + +// UpdateVideoPrice480p sets the "video_price_480p" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateVideoPrice480p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice480p() + }) +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (u *GroupUpsertOne) ClearVideoPrice480p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice480p() + }) +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (u *GroupUpsertOne) SetVideoPrice720p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice720p(v) + }) +} + +// AddVideoPrice720p adds v to the "video_price_720p" field. +func (u *GroupUpsertOne) AddVideoPrice720p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice720p(v) + }) +} + +// UpdateVideoPrice720p sets the "video_price_720p" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateVideoPrice720p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice720p() + }) +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (u *GroupUpsertOne) ClearVideoPrice720p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice720p() + }) +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (u *GroupUpsertOne) SetVideoPrice1080p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice1080p(v) + }) +} + +// AddVideoPrice1080p adds v to the "video_price_1080p" field. +func (u *GroupUpsertOne) AddVideoPrice1080p(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice1080p(v) + }) +} + +// UpdateVideoPrice1080p sets the "video_price_1080p" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateVideoPrice1080p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice1080p() + }) +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (u *GroupUpsertOne) ClearVideoPrice1080p() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice1080p() + }) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { @@ -3507,6 +3832,125 @@ func (u *GroupUpsertBulk) UpdateBatchImageHoldMultiplier() *GroupUpsertBulk { }) } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (u *GroupUpsertBulk) SetVideoRateIndependent(v bool) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetVideoRateIndependent(v) + }) +} + +// UpdateVideoRateIndependent sets the "video_rate_independent" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateVideoRateIndependent() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoRateIndependent() + }) +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (u *GroupUpsertBulk) SetVideoRateMultiplier(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetVideoRateMultiplier(v) + }) +} + +// AddVideoRateMultiplier adds v to the "video_rate_multiplier" field. +func (u *GroupUpsertBulk) AddVideoRateMultiplier(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddVideoRateMultiplier(v) + }) +} + +// UpdateVideoRateMultiplier sets the "video_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateVideoRateMultiplier() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoRateMultiplier() + }) +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (u *GroupUpsertBulk) SetVideoPrice480p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice480p(v) + }) +} + +// AddVideoPrice480p adds v to the "video_price_480p" field. +func (u *GroupUpsertBulk) AddVideoPrice480p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice480p(v) + }) +} + +// UpdateVideoPrice480p sets the "video_price_480p" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateVideoPrice480p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice480p() + }) +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (u *GroupUpsertBulk) ClearVideoPrice480p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice480p() + }) +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (u *GroupUpsertBulk) SetVideoPrice720p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice720p(v) + }) +} + +// AddVideoPrice720p adds v to the "video_price_720p" field. +func (u *GroupUpsertBulk) AddVideoPrice720p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice720p(v) + }) +} + +// UpdateVideoPrice720p sets the "video_price_720p" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateVideoPrice720p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice720p() + }) +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (u *GroupUpsertBulk) ClearVideoPrice720p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice720p() + }) +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (u *GroupUpsertBulk) SetVideoPrice1080p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetVideoPrice1080p(v) + }) +} + +// AddVideoPrice1080p adds v to the "video_price_1080p" field. +func (u *GroupUpsertBulk) AddVideoPrice1080p(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddVideoPrice1080p(v) + }) +} + +// UpdateVideoPrice1080p sets the "video_price_1080p" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateVideoPrice1080p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateVideoPrice1080p() + }) +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (u *GroupUpsertBulk) ClearVideoPrice1080p() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearVideoPrice1080p() + }) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index 6f1831b1ea..3bb18d3e1a 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -524,6 +524,122 @@ func (_u *GroupUpdate) AddBatchImageHoldMultiplier(v float64) *GroupUpdate { return _u } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (_u *GroupUpdate) SetVideoRateIndependent(v bool) *GroupUpdate { + _u.mutation.SetVideoRateIndependent(v) + return _u +} + +// SetNillableVideoRateIndependent sets the "video_rate_independent" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableVideoRateIndependent(v *bool) *GroupUpdate { + if v != nil { + _u.SetVideoRateIndependent(*v) + } + return _u +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (_u *GroupUpdate) SetVideoRateMultiplier(v float64) *GroupUpdate { + _u.mutation.ResetVideoRateMultiplier() + _u.mutation.SetVideoRateMultiplier(v) + return _u +} + +// SetNillableVideoRateMultiplier sets the "video_rate_multiplier" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableVideoRateMultiplier(v *float64) *GroupUpdate { + if v != nil { + _u.SetVideoRateMultiplier(*v) + } + return _u +} + +// AddVideoRateMultiplier adds value to the "video_rate_multiplier" field. +func (_u *GroupUpdate) AddVideoRateMultiplier(v float64) *GroupUpdate { + _u.mutation.AddVideoRateMultiplier(v) + return _u +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (_u *GroupUpdate) SetVideoPrice480p(v float64) *GroupUpdate { + _u.mutation.ResetVideoPrice480p() + _u.mutation.SetVideoPrice480p(v) + return _u +} + +// SetNillableVideoPrice480p sets the "video_price_480p" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableVideoPrice480p(v *float64) *GroupUpdate { + if v != nil { + _u.SetVideoPrice480p(*v) + } + return _u +} + +// AddVideoPrice480p adds value to the "video_price_480p" field. +func (_u *GroupUpdate) AddVideoPrice480p(v float64) *GroupUpdate { + _u.mutation.AddVideoPrice480p(v) + return _u +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (_u *GroupUpdate) ClearVideoPrice480p() *GroupUpdate { + _u.mutation.ClearVideoPrice480p() + return _u +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (_u *GroupUpdate) SetVideoPrice720p(v float64) *GroupUpdate { + _u.mutation.ResetVideoPrice720p() + _u.mutation.SetVideoPrice720p(v) + return _u +} + +// SetNillableVideoPrice720p sets the "video_price_720p" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableVideoPrice720p(v *float64) *GroupUpdate { + if v != nil { + _u.SetVideoPrice720p(*v) + } + return _u +} + +// AddVideoPrice720p adds value to the "video_price_720p" field. +func (_u *GroupUpdate) AddVideoPrice720p(v float64) *GroupUpdate { + _u.mutation.AddVideoPrice720p(v) + return _u +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (_u *GroupUpdate) ClearVideoPrice720p() *GroupUpdate { + _u.mutation.ClearVideoPrice720p() + return _u +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (_u *GroupUpdate) SetVideoPrice1080p(v float64) *GroupUpdate { + _u.mutation.ResetVideoPrice1080p() + _u.mutation.SetVideoPrice1080p(v) + return _u +} + +// SetNillableVideoPrice1080p sets the "video_price_1080p" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableVideoPrice1080p(v *float64) *GroupUpdate { + if v != nil { + _u.SetVideoPrice1080p(*v) + } + return _u +} + +// AddVideoPrice1080p adds value to the "video_price_1080p" field. +func (_u *GroupUpdate) AddVideoPrice1080p(v float64) *GroupUpdate { + _u.mutation.AddVideoPrice1080p(v) + return _u +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (_u *GroupUpdate) ClearVideoPrice1080p() *GroupUpdate { + _u.mutation.ClearVideoPrice1080p() + return _u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate { _u.mutation.SetClaudeCodeOnly(v) @@ -1223,6 +1339,42 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.AddedBatchImageHoldMultiplier(); ok { _spec.AddField(group.FieldBatchImageHoldMultiplier, field.TypeFloat64, value) } + if value, ok := _u.mutation.VideoRateIndependent(); ok { + _spec.SetField(group.FieldVideoRateIndependent, field.TypeBool, value) + } + if value, ok := _u.mutation.VideoRateMultiplier(); ok { + _spec.SetField(group.FieldVideoRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoRateMultiplier(); ok { + _spec.AddField(group.FieldVideoRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.VideoPrice480p(); ok { + _spec.SetField(group.FieldVideoPrice480p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice480p(); ok { + _spec.AddField(group.FieldVideoPrice480p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice480pCleared() { + _spec.ClearField(group.FieldVideoPrice480p, field.TypeFloat64) + } + if value, ok := _u.mutation.VideoPrice720p(); ok { + _spec.SetField(group.FieldVideoPrice720p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice720p(); ok { + _spec.AddField(group.FieldVideoPrice720p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice720pCleared() { + _spec.ClearField(group.FieldVideoPrice720p, field.TypeFloat64) + } + if value, ok := _u.mutation.VideoPrice1080p(); ok { + _spec.SetField(group.FieldVideoPrice1080p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice1080p(); ok { + _spec.AddField(group.FieldVideoPrice1080p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice1080pCleared() { + _spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64) + } if value, ok := _u.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) } @@ -2096,6 +2248,122 @@ func (_u *GroupUpdateOne) AddBatchImageHoldMultiplier(v float64) *GroupUpdateOne return _u } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (_u *GroupUpdateOne) SetVideoRateIndependent(v bool) *GroupUpdateOne { + _u.mutation.SetVideoRateIndependent(v) + return _u +} + +// SetNillableVideoRateIndependent sets the "video_rate_independent" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableVideoRateIndependent(v *bool) *GroupUpdateOne { + if v != nil { + _u.SetVideoRateIndependent(*v) + } + return _u +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (_u *GroupUpdateOne) SetVideoRateMultiplier(v float64) *GroupUpdateOne { + _u.mutation.ResetVideoRateMultiplier() + _u.mutation.SetVideoRateMultiplier(v) + return _u +} + +// SetNillableVideoRateMultiplier sets the "video_rate_multiplier" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableVideoRateMultiplier(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetVideoRateMultiplier(*v) + } + return _u +} + +// AddVideoRateMultiplier adds value to the "video_rate_multiplier" field. +func (_u *GroupUpdateOne) AddVideoRateMultiplier(v float64) *GroupUpdateOne { + _u.mutation.AddVideoRateMultiplier(v) + return _u +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (_u *GroupUpdateOne) SetVideoPrice480p(v float64) *GroupUpdateOne { + _u.mutation.ResetVideoPrice480p() + _u.mutation.SetVideoPrice480p(v) + return _u +} + +// SetNillableVideoPrice480p sets the "video_price_480p" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableVideoPrice480p(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetVideoPrice480p(*v) + } + return _u +} + +// AddVideoPrice480p adds value to the "video_price_480p" field. +func (_u *GroupUpdateOne) AddVideoPrice480p(v float64) *GroupUpdateOne { + _u.mutation.AddVideoPrice480p(v) + return _u +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (_u *GroupUpdateOne) ClearVideoPrice480p() *GroupUpdateOne { + _u.mutation.ClearVideoPrice480p() + return _u +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (_u *GroupUpdateOne) SetVideoPrice720p(v float64) *GroupUpdateOne { + _u.mutation.ResetVideoPrice720p() + _u.mutation.SetVideoPrice720p(v) + return _u +} + +// SetNillableVideoPrice720p sets the "video_price_720p" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableVideoPrice720p(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetVideoPrice720p(*v) + } + return _u +} + +// AddVideoPrice720p adds value to the "video_price_720p" field. +func (_u *GroupUpdateOne) AddVideoPrice720p(v float64) *GroupUpdateOne { + _u.mutation.AddVideoPrice720p(v) + return _u +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (_u *GroupUpdateOne) ClearVideoPrice720p() *GroupUpdateOne { + _u.mutation.ClearVideoPrice720p() + return _u +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (_u *GroupUpdateOne) SetVideoPrice1080p(v float64) *GroupUpdateOne { + _u.mutation.ResetVideoPrice1080p() + _u.mutation.SetVideoPrice1080p(v) + return _u +} + +// SetNillableVideoPrice1080p sets the "video_price_1080p" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableVideoPrice1080p(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetVideoPrice1080p(*v) + } + return _u +} + +// AddVideoPrice1080p adds value to the "video_price_1080p" field. +func (_u *GroupUpdateOne) AddVideoPrice1080p(v float64) *GroupUpdateOne { + _u.mutation.AddVideoPrice1080p(v) + return _u +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (_u *GroupUpdateOne) ClearVideoPrice1080p() *GroupUpdateOne { + _u.mutation.ClearVideoPrice1080p() + return _u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne { _u.mutation.SetClaudeCodeOnly(v) @@ -2825,6 +3093,42 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if value, ok := _u.mutation.AddedBatchImageHoldMultiplier(); ok { _spec.AddField(group.FieldBatchImageHoldMultiplier, field.TypeFloat64, value) } + if value, ok := _u.mutation.VideoRateIndependent(); ok { + _spec.SetField(group.FieldVideoRateIndependent, field.TypeBool, value) + } + if value, ok := _u.mutation.VideoRateMultiplier(); ok { + _spec.SetField(group.FieldVideoRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoRateMultiplier(); ok { + _spec.AddField(group.FieldVideoRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.VideoPrice480p(); ok { + _spec.SetField(group.FieldVideoPrice480p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice480p(); ok { + _spec.AddField(group.FieldVideoPrice480p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice480pCleared() { + _spec.ClearField(group.FieldVideoPrice480p, field.TypeFloat64) + } + if value, ok := _u.mutation.VideoPrice720p(); ok { + _spec.SetField(group.FieldVideoPrice720p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice720p(); ok { + _spec.AddField(group.FieldVideoPrice720p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice720pCleared() { + _spec.ClearField(group.FieldVideoPrice720p, field.TypeFloat64) + } + if value, ok := _u.mutation.VideoPrice1080p(); ok { + _spec.SetField(group.FieldVideoPrice1080p, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedVideoPrice1080p(); ok { + _spec.AddField(group.FieldVideoPrice1080p, field.TypeFloat64, value) + } + if _u.mutation.VideoPrice1080pCleared() { + _spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64) + } if value, ok := _u.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) } diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index a584cbe39d..edae57d212 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -860,6 +860,11 @@ var ( {Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "batch_image_discount_multiplier", Type: field.TypeFloat64, Default: 0.5, SchemaType: map[string]string{"postgres": "decimal(10,4)"}}, {Name: "batch_image_hold_multiplier", Type: field.TypeFloat64, Default: 0.6, SchemaType: map[string]string{"postgres": "decimal(10,4)"}}, + {Name: "video_rate_independent", Type: field.TypeBool, Default: false}, + {Name: "video_rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}}, + {Name: "video_price_480p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "video_price_720p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "video_price_1080p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "claude_code_only", Type: field.TypeBool, Default: false}, {Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true}, {Name: "fallback_group_id_on_invalid_request", Type: field.TypeInt64, Nullable: true}, @@ -910,7 +915,7 @@ var ( { Name: "group_sort_order", Unique: false, - Columns: []*schema.Column{GroupsColumns[35]}, + Columns: []*schema.Column{GroupsColumns[40]}, }, }, } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 987ec4146b..07f8ce623e 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -20833,6 +20833,15 @@ type GroupMutation struct { addbatch_image_discount_multiplier *float64 batch_image_hold_multiplier *float64 addbatch_image_hold_multiplier *float64 + video_rate_independent *bool + video_rate_multiplier *float64 + addvideo_rate_multiplier *float64 + video_price_480p *float64 + addvideo_price_480p *float64 + video_price_720p *float64 + addvideo_price_720p *float64 + video_price_1080p *float64 + addvideo_price_1080p *float64 claude_code_only *bool fallback_group_id *int64 addfallback_group_id *int64 @@ -22297,6 +22306,308 @@ func (m *GroupMutation) ResetBatchImageHoldMultiplier() { m.addbatch_image_hold_multiplier = nil } +// SetVideoRateIndependent sets the "video_rate_independent" field. +func (m *GroupMutation) SetVideoRateIndependent(b bool) { + m.video_rate_independent = &b +} + +// VideoRateIndependent returns the value of the "video_rate_independent" field in the mutation. +func (m *GroupMutation) VideoRateIndependent() (r bool, exists bool) { + v := m.video_rate_independent + if v == nil { + return + } + return *v, true +} + +// OldVideoRateIndependent returns the old "video_rate_independent" 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) OldVideoRateIndependent(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVideoRateIndependent is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVideoRateIndependent requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVideoRateIndependent: %w", err) + } + return oldValue.VideoRateIndependent, nil +} + +// ResetVideoRateIndependent resets all changes to the "video_rate_independent" field. +func (m *GroupMutation) ResetVideoRateIndependent() { + m.video_rate_independent = nil +} + +// SetVideoRateMultiplier sets the "video_rate_multiplier" field. +func (m *GroupMutation) SetVideoRateMultiplier(f float64) { + m.video_rate_multiplier = &f + m.addvideo_rate_multiplier = nil +} + +// VideoRateMultiplier returns the value of the "video_rate_multiplier" field in the mutation. +func (m *GroupMutation) VideoRateMultiplier() (r float64, exists bool) { + v := m.video_rate_multiplier + if v == nil { + return + } + return *v, true +} + +// OldVideoRateMultiplier returns the old "video_rate_multiplier" 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) OldVideoRateMultiplier(ctx context.Context) (v float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVideoRateMultiplier is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVideoRateMultiplier requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVideoRateMultiplier: %w", err) + } + return oldValue.VideoRateMultiplier, nil +} + +// AddVideoRateMultiplier adds f to the "video_rate_multiplier" field. +func (m *GroupMutation) AddVideoRateMultiplier(f float64) { + if m.addvideo_rate_multiplier != nil { + *m.addvideo_rate_multiplier += f + } else { + m.addvideo_rate_multiplier = &f + } +} + +// AddedVideoRateMultiplier returns the value that was added to the "video_rate_multiplier" field in this mutation. +func (m *GroupMutation) AddedVideoRateMultiplier() (r float64, exists bool) { + v := m.addvideo_rate_multiplier + if v == nil { + return + } + return *v, true +} + +// ResetVideoRateMultiplier resets all changes to the "video_rate_multiplier" field. +func (m *GroupMutation) ResetVideoRateMultiplier() { + m.video_rate_multiplier = nil + m.addvideo_rate_multiplier = nil +} + +// SetVideoPrice480p sets the "video_price_480p" field. +func (m *GroupMutation) SetVideoPrice480p(f float64) { + m.video_price_480p = &f + m.addvideo_price_480p = nil +} + +// VideoPrice480p returns the value of the "video_price_480p" field in the mutation. +func (m *GroupMutation) VideoPrice480p() (r float64, exists bool) { + v := m.video_price_480p + if v == nil { + return + } + return *v, true +} + +// OldVideoPrice480p returns the old "video_price_480p" 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) OldVideoPrice480p(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVideoPrice480p is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVideoPrice480p requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVideoPrice480p: %w", err) + } + return oldValue.VideoPrice480p, nil +} + +// AddVideoPrice480p adds f to the "video_price_480p" field. +func (m *GroupMutation) AddVideoPrice480p(f float64) { + if m.addvideo_price_480p != nil { + *m.addvideo_price_480p += f + } else { + m.addvideo_price_480p = &f + } +} + +// AddedVideoPrice480p returns the value that was added to the "video_price_480p" field in this mutation. +func (m *GroupMutation) AddedVideoPrice480p() (r float64, exists bool) { + v := m.addvideo_price_480p + if v == nil { + return + } + return *v, true +} + +// ClearVideoPrice480p clears the value of the "video_price_480p" field. +func (m *GroupMutation) ClearVideoPrice480p() { + m.video_price_480p = nil + m.addvideo_price_480p = nil + m.clearedFields[group.FieldVideoPrice480p] = struct{}{} +} + +// VideoPrice480pCleared returns if the "video_price_480p" field was cleared in this mutation. +func (m *GroupMutation) VideoPrice480pCleared() bool { + _, ok := m.clearedFields[group.FieldVideoPrice480p] + return ok +} + +// ResetVideoPrice480p resets all changes to the "video_price_480p" field. +func (m *GroupMutation) ResetVideoPrice480p() { + m.video_price_480p = nil + m.addvideo_price_480p = nil + delete(m.clearedFields, group.FieldVideoPrice480p) +} + +// SetVideoPrice720p sets the "video_price_720p" field. +func (m *GroupMutation) SetVideoPrice720p(f float64) { + m.video_price_720p = &f + m.addvideo_price_720p = nil +} + +// VideoPrice720p returns the value of the "video_price_720p" field in the mutation. +func (m *GroupMutation) VideoPrice720p() (r float64, exists bool) { + v := m.video_price_720p + if v == nil { + return + } + return *v, true +} + +// OldVideoPrice720p returns the old "video_price_720p" 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) OldVideoPrice720p(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVideoPrice720p is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVideoPrice720p requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVideoPrice720p: %w", err) + } + return oldValue.VideoPrice720p, nil +} + +// AddVideoPrice720p adds f to the "video_price_720p" field. +func (m *GroupMutation) AddVideoPrice720p(f float64) { + if m.addvideo_price_720p != nil { + *m.addvideo_price_720p += f + } else { + m.addvideo_price_720p = &f + } +} + +// AddedVideoPrice720p returns the value that was added to the "video_price_720p" field in this mutation. +func (m *GroupMutation) AddedVideoPrice720p() (r float64, exists bool) { + v := m.addvideo_price_720p + if v == nil { + return + } + return *v, true +} + +// ClearVideoPrice720p clears the value of the "video_price_720p" field. +func (m *GroupMutation) ClearVideoPrice720p() { + m.video_price_720p = nil + m.addvideo_price_720p = nil + m.clearedFields[group.FieldVideoPrice720p] = struct{}{} +} + +// VideoPrice720pCleared returns if the "video_price_720p" field was cleared in this mutation. +func (m *GroupMutation) VideoPrice720pCleared() bool { + _, ok := m.clearedFields[group.FieldVideoPrice720p] + return ok +} + +// ResetVideoPrice720p resets all changes to the "video_price_720p" field. +func (m *GroupMutation) ResetVideoPrice720p() { + m.video_price_720p = nil + m.addvideo_price_720p = nil + delete(m.clearedFields, group.FieldVideoPrice720p) +} + +// SetVideoPrice1080p sets the "video_price_1080p" field. +func (m *GroupMutation) SetVideoPrice1080p(f float64) { + m.video_price_1080p = &f + m.addvideo_price_1080p = nil +} + +// VideoPrice1080p returns the value of the "video_price_1080p" field in the mutation. +func (m *GroupMutation) VideoPrice1080p() (r float64, exists bool) { + v := m.video_price_1080p + if v == nil { + return + } + return *v, true +} + +// OldVideoPrice1080p returns the old "video_price_1080p" 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) OldVideoPrice1080p(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldVideoPrice1080p is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldVideoPrice1080p requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldVideoPrice1080p: %w", err) + } + return oldValue.VideoPrice1080p, nil +} + +// AddVideoPrice1080p adds f to the "video_price_1080p" field. +func (m *GroupMutation) AddVideoPrice1080p(f float64) { + if m.addvideo_price_1080p != nil { + *m.addvideo_price_1080p += f + } else { + m.addvideo_price_1080p = &f + } +} + +// AddedVideoPrice1080p returns the value that was added to the "video_price_1080p" field in this mutation. +func (m *GroupMutation) AddedVideoPrice1080p() (r float64, exists bool) { + v := m.addvideo_price_1080p + if v == nil { + return + } + return *v, true +} + +// ClearVideoPrice1080p clears the value of the "video_price_1080p" field. +func (m *GroupMutation) ClearVideoPrice1080p() { + m.video_price_1080p = nil + m.addvideo_price_1080p = nil + m.clearedFields[group.FieldVideoPrice1080p] = struct{}{} +} + +// VideoPrice1080pCleared returns if the "video_price_1080p" field was cleared in this mutation. +func (m *GroupMutation) VideoPrice1080pCleared() bool { + _, ok := m.clearedFields[group.FieldVideoPrice1080p] + return ok +} + +// ResetVideoPrice1080p resets all changes to the "video_price_1080p" field. +func (m *GroupMutation) ResetVideoPrice1080p() { + m.video_price_1080p = nil + m.addvideo_price_1080p = nil + delete(m.clearedFields, group.FieldVideoPrice1080p) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (m *GroupMutation) SetClaudeCodeOnly(b bool) { m.claude_code_only = &b @@ -23331,7 +23642,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, 42) + fields := make([]string, 0, 47) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -23413,6 +23724,21 @@ func (m *GroupMutation) Fields() []string { if m.batch_image_hold_multiplier != nil { fields = append(fields, group.FieldBatchImageHoldMultiplier) } + if m.video_rate_independent != nil { + fields = append(fields, group.FieldVideoRateIndependent) + } + if m.video_rate_multiplier != nil { + fields = append(fields, group.FieldVideoRateMultiplier) + } + if m.video_price_480p != nil { + fields = append(fields, group.FieldVideoPrice480p) + } + if m.video_price_720p != nil { + fields = append(fields, group.FieldVideoPrice720p) + } + if m.video_price_1080p != nil { + fields = append(fields, group.FieldVideoPrice1080p) + } if m.claude_code_only != nil { fields = append(fields, group.FieldClaudeCodeOnly) } @@ -23520,6 +23846,16 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.BatchImageDiscountMultiplier() case group.FieldBatchImageHoldMultiplier: return m.BatchImageHoldMultiplier() + case group.FieldVideoRateIndependent: + return m.VideoRateIndependent() + case group.FieldVideoRateMultiplier: + return m.VideoRateMultiplier() + case group.FieldVideoPrice480p: + return m.VideoPrice480p() + case group.FieldVideoPrice720p: + return m.VideoPrice720p() + case group.FieldVideoPrice1080p: + return m.VideoPrice1080p() case group.FieldClaudeCodeOnly: return m.ClaudeCodeOnly() case group.FieldFallbackGroupID: @@ -23613,6 +23949,16 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldBatchImageDiscountMultiplier(ctx) case group.FieldBatchImageHoldMultiplier: return m.OldBatchImageHoldMultiplier(ctx) + case group.FieldVideoRateIndependent: + return m.OldVideoRateIndependent(ctx) + case group.FieldVideoRateMultiplier: + return m.OldVideoRateMultiplier(ctx) + case group.FieldVideoPrice480p: + return m.OldVideoPrice480p(ctx) + case group.FieldVideoPrice720p: + return m.OldVideoPrice720p(ctx) + case group.FieldVideoPrice1080p: + return m.OldVideoPrice1080p(ctx) case group.FieldClaudeCodeOnly: return m.OldClaudeCodeOnly(ctx) case group.FieldFallbackGroupID: @@ -23841,6 +24187,41 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetBatchImageHoldMultiplier(v) return nil + case group.FieldVideoRateIndependent: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVideoRateIndependent(v) + return nil + case group.FieldVideoRateMultiplier: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVideoRateMultiplier(v) + return nil + case group.FieldVideoPrice480p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVideoPrice480p(v) + return nil + case group.FieldVideoPrice720p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVideoPrice720p(v) + return nil + case group.FieldVideoPrice1080p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetVideoPrice1080p(v) + return nil case group.FieldClaudeCodeOnly: v, ok := value.(bool) if !ok { @@ -23990,6 +24371,18 @@ func (m *GroupMutation) AddedFields() []string { if m.addbatch_image_hold_multiplier != nil { fields = append(fields, group.FieldBatchImageHoldMultiplier) } + if m.addvideo_rate_multiplier != nil { + fields = append(fields, group.FieldVideoRateMultiplier) + } + if m.addvideo_price_480p != nil { + fields = append(fields, group.FieldVideoPrice480p) + } + if m.addvideo_price_720p != nil { + fields = append(fields, group.FieldVideoPrice720p) + } + if m.addvideo_price_1080p != nil { + fields = append(fields, group.FieldVideoPrice1080p) + } if m.addfallback_group_id != nil { fields = append(fields, group.FieldFallbackGroupID) } @@ -24034,6 +24427,14 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { return m.AddedBatchImageDiscountMultiplier() case group.FieldBatchImageHoldMultiplier: return m.AddedBatchImageHoldMultiplier() + case group.FieldVideoRateMultiplier: + return m.AddedVideoRateMultiplier() + case group.FieldVideoPrice480p: + return m.AddedVideoPrice480p() + case group.FieldVideoPrice720p: + return m.AddedVideoPrice720p() + case group.FieldVideoPrice1080p: + return m.AddedVideoPrice1080p() case group.FieldFallbackGroupID: return m.AddedFallbackGroupID() case group.FieldFallbackGroupIDOnInvalidRequest: @@ -24135,6 +24536,34 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddBatchImageHoldMultiplier(v) return nil + case group.FieldVideoRateMultiplier: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddVideoRateMultiplier(v) + return nil + case group.FieldVideoPrice480p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddVideoPrice480p(v) + return nil + case group.FieldVideoPrice720p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddVideoPrice720p(v) + return nil + case group.FieldVideoPrice1080p: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddVideoPrice1080p(v) + return nil case group.FieldFallbackGroupID: v, ok := value.(int64) if !ok { @@ -24195,6 +24624,15 @@ func (m *GroupMutation) ClearedFields() []string { if m.FieldCleared(group.FieldImagePrice4k) { fields = append(fields, group.FieldImagePrice4k) } + if m.FieldCleared(group.FieldVideoPrice480p) { + fields = append(fields, group.FieldVideoPrice480p) + } + if m.FieldCleared(group.FieldVideoPrice720p) { + fields = append(fields, group.FieldVideoPrice720p) + } + if m.FieldCleared(group.FieldVideoPrice1080p) { + fields = append(fields, group.FieldVideoPrice1080p) + } if m.FieldCleared(group.FieldFallbackGroupID) { fields = append(fields, group.FieldFallbackGroupID) } @@ -24242,6 +24680,15 @@ func (m *GroupMutation) ClearField(name string) error { case group.FieldImagePrice4k: m.ClearImagePrice4k() return nil + case group.FieldVideoPrice480p: + m.ClearVideoPrice480p() + return nil + case group.FieldVideoPrice720p: + m.ClearVideoPrice720p() + return nil + case group.FieldVideoPrice1080p: + m.ClearVideoPrice1080p() + return nil case group.FieldFallbackGroupID: m.ClearFallbackGroupID() return nil @@ -24340,6 +24787,21 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldBatchImageHoldMultiplier: m.ResetBatchImageHoldMultiplier() return nil + case group.FieldVideoRateIndependent: + m.ResetVideoRateIndependent() + return nil + case group.FieldVideoRateMultiplier: + m.ResetVideoRateMultiplier() + return nil + case group.FieldVideoPrice480p: + m.ResetVideoPrice480p() + return nil + case group.FieldVideoPrice720p: + m.ResetVideoPrice720p() + return nil + case group.FieldVideoPrice1080p: + m.ResetVideoPrice1080p() + return nil case group.FieldClaudeCodeOnly: m.ResetClaudeCodeOnly() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 2c05c01c52..ddde08d23d 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -1035,54 +1035,62 @@ func init() { groupDescBatchImageHoldMultiplier := groupFields[23].Descriptor() // group.DefaultBatchImageHoldMultiplier holds the default value on creation for the batch_image_hold_multiplier field. group.DefaultBatchImageHoldMultiplier = groupDescBatchImageHoldMultiplier.Default.(float64) + // groupDescVideoRateIndependent is the schema descriptor for video_rate_independent field. + groupDescVideoRateIndependent := groupFields[24].Descriptor() + // group.DefaultVideoRateIndependent holds the default value on creation for the video_rate_independent field. + group.DefaultVideoRateIndependent = groupDescVideoRateIndependent.Default.(bool) + // groupDescVideoRateMultiplier is the schema descriptor for video_rate_multiplier field. + groupDescVideoRateMultiplier := groupFields[25].Descriptor() + // group.DefaultVideoRateMultiplier holds the default value on creation for the video_rate_multiplier field. + group.DefaultVideoRateMultiplier = groupDescVideoRateMultiplier.Default.(float64) // groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field. - groupDescClaudeCodeOnly := groupFields[24].Descriptor() + groupDescClaudeCodeOnly := groupFields[29].Descriptor() // group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field. group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool) // groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field. - groupDescModelRoutingEnabled := groupFields[28].Descriptor() + groupDescModelRoutingEnabled := groupFields[33].Descriptor() // group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field. group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool) // groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field. - groupDescMcpXMLInject := groupFields[29].Descriptor() + groupDescMcpXMLInject := groupFields[34].Descriptor() // group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field. group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool) // groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field. - groupDescSupportedModelScopes := groupFields[30].Descriptor() + groupDescSupportedModelScopes := groupFields[35].Descriptor() // group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field. group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string) // groupDescSortOrder is the schema descriptor for sort_order field. - groupDescSortOrder := groupFields[31].Descriptor() + groupDescSortOrder := groupFields[36].Descriptor() // group.DefaultSortOrder holds the default value on creation for the sort_order field. group.DefaultSortOrder = groupDescSortOrder.Default.(int) // groupDescAllowMessagesDispatch is the schema descriptor for allow_messages_dispatch field. - groupDescAllowMessagesDispatch := groupFields[32].Descriptor() + groupDescAllowMessagesDispatch := groupFields[37].Descriptor() // group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field. group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool) // groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field. - groupDescRequireOauthOnly := groupFields[33].Descriptor() + groupDescRequireOauthOnly := groupFields[38].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[34].Descriptor() + groupDescRequirePrivacySet := groupFields[39].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[35].Descriptor() + groupDescDefaultMappedModel := groupFields[40].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[36].Descriptor() + groupDescMessagesDispatchModelConfig := groupFields[41].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[37].Descriptor() + groupDescModelsListConfig := groupFields[42].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[38].Descriptor() + groupDescRpmLimit := groupFields[43].Descriptor() // group.DefaultRpmLimit holds the default value on creation for the rpm_limit field. group.DefaultRpmLimit = groupDescRpmLimit.Default.(int) idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin() diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index d675ca52f1..b104609a1b 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -123,6 +123,25 @@ func (Group) Fields() []ent.Field { SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}). Default(0.6). Comment("批量图片生成冻结价格比例,按普通生图原价乘以该比例冻结,结算后释放差额"), + field.Bool("video_rate_independent"). + Default(false). + Comment("视频生成是否使用独立倍率;false 表示共享分组有效倍率"), + field.Float("video_rate_multiplier"). + SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}). + Default(1.0). + Comment("视频生成独立倍率,仅 video_rate_independent=true 时生效"), + field.Float("video_price_480p"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), + field.Float("video_price_720p"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), + field.Float("video_price_1080p"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), // Claude Code 客户端限制 (added by migration 029) field.Bool("claude_code_only"). diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 4595adeb24..56a0b29ed0 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -98,6 +98,8 @@ type CreateGroupRequest struct { ImageRateMultiplier *float64 `json:"image_rate_multiplier"` BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"` BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"` + VideoRateIndependent bool `json:"video_rate_independent"` + VideoRateMultiplier *float64 `json:"video_rate_multiplier"` PeakRateEnabled bool `json:"peak_rate_enabled"` PeakStart string `json:"peak_start"` PeakEnd string `json:"peak_end"` @@ -105,6 +107,9 @@ type CreateGroupRequest struct { ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice4K *float64 `json:"image_price_4k"` + VideoPrice480P *float64 `json:"video_price_480p"` + VideoPrice720P *float64 `json:"video_price_720p"` + VideoPrice1080P *float64 `json:"video_price_1080p"` ClaudeCodeOnly bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"` @@ -146,6 +151,8 @@ type UpdateGroupRequest struct { ImageRateMultiplier *float64 `json:"image_rate_multiplier"` BatchImageDiscountMultiplier *float64 `json:"batch_image_discount_multiplier"` BatchImageHoldMultiplier *float64 `json:"batch_image_hold_multiplier"` + VideoRateIndependent *bool `json:"video_rate_independent"` + VideoRateMultiplier *float64 `json:"video_rate_multiplier"` PeakRateEnabled *bool `json:"peak_rate_enabled"` PeakStart *string `json:"peak_start"` PeakEnd *string `json:"peak_end"` @@ -153,6 +160,9 @@ type UpdateGroupRequest struct { ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice4K *float64 `json:"image_price_4k"` + VideoPrice480P *float64 `json:"video_price_480p"` + VideoPrice720P *float64 `json:"video_price_720p"` + VideoPrice1080P *float64 `json:"video_price_1080p"` ClaudeCodeOnly *bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"` @@ -312,6 +322,8 @@ func (h *GroupHandler) Create(c *gin.Context) { ImageRateMultiplier: req.ImageRateMultiplier, BatchImageDiscountMultiplier: req.BatchImageDiscountMultiplier, BatchImageHoldMultiplier: req.BatchImageHoldMultiplier, + VideoRateIndependent: req.VideoRateIndependent, + VideoRateMultiplier: req.VideoRateMultiplier, PeakRateEnabled: req.PeakRateEnabled, PeakStart: req.PeakStart, PeakEnd: req.PeakEnd, @@ -319,6 +331,9 @@ func (h *GroupHandler) Create(c *gin.Context) { ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, + VideoPrice480P: req.VideoPrice480P, + VideoPrice720P: req.VideoPrice720P, + VideoPrice1080P: req.VideoPrice1080P, ClaudeCodeOnly: req.ClaudeCodeOnly, FallbackGroupID: req.FallbackGroupID, FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest, @@ -375,6 +390,8 @@ func (h *GroupHandler) Update(c *gin.Context) { ImageRateMultiplier: req.ImageRateMultiplier, BatchImageDiscountMultiplier: req.BatchImageDiscountMultiplier, BatchImageHoldMultiplier: req.BatchImageHoldMultiplier, + VideoRateIndependent: req.VideoRateIndependent, + VideoRateMultiplier: req.VideoRateMultiplier, PeakRateEnabled: req.PeakRateEnabled, PeakStart: req.PeakStart, PeakEnd: req.PeakEnd, @@ -382,6 +399,9 @@ func (h *GroupHandler) Update(c *gin.Context) { ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, + VideoPrice480P: req.VideoPrice480P, + VideoPrice720P: req.VideoPrice720P, + VideoPrice1080P: req.VideoPrice1080P, ClaudeCodeOnly: req.ClaudeCodeOnly, FallbackGroupID: req.FallbackGroupID, FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 00e1ea829c..6270c2b982 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -187,6 +187,8 @@ func groupFromServiceBase(g *service.Group) Group { ImageRateMultiplier: g.ImageRateMultiplier, BatchImageDiscountMultiplier: g.BatchImageDiscountMultiplier, BatchImageHoldMultiplier: g.BatchImageHoldMultiplier, + VideoRateIndependent: g.VideoRateIndependent, + VideoRateMultiplier: g.VideoRateMultiplier, PeakRateEnabled: g.PeakRateEnabled, PeakStart: g.PeakStart, PeakEnd: g.PeakEnd, @@ -194,6 +196,9 @@ func groupFromServiceBase(g *service.Group) Group { ImagePrice1K: g.ImagePrice1K, ImagePrice2K: g.ImagePrice2K, ImagePrice4K: g.ImagePrice4K, + VideoPrice480P: g.VideoPrice480P, + VideoPrice720P: g.VideoPrice720P, + VideoPrice1080P: g.VideoPrice1080P, ClaudeCodeOnly: g.ClaudeCodeOnly, FallbackGroupID: g.FallbackGroupID, FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 3aa8890d62..7cfd102880 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -107,6 +107,8 @@ type Group struct { ImageRateMultiplier float64 `json:"image_rate_multiplier"` BatchImageDiscountMultiplier float64 `json:"batch_image_discount_multiplier"` BatchImageHoldMultiplier float64 `json:"batch_image_hold_multiplier"` + VideoRateIndependent bool `json:"video_rate_independent"` + VideoRateMultiplier float64 `json:"video_rate_multiplier"` // 高峰时段倍率配置 PeakRateEnabled bool `json:"peak_rate_enabled"` PeakStart string `json:"peak_start"` @@ -115,6 +117,9 @@ type Group struct { ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice4K *float64 `json:"image_price_4k"` + VideoPrice480P *float64 `json:"video_price_480p"` + VideoPrice720P *float64 `json:"video_price_720p"` + VideoPrice1080P *float64 `json:"video_price_1080p"` // Claude Code 客户端限制 ClaudeCodeOnly bool `json:"claude_code_only"` diff --git a/backend/internal/handler/usage_handler.go b/backend/internal/handler/usage_handler.go index be6dc917bb..45f72ae306 100644 --- a/backend/internal/handler/usage_handler.go +++ b/backend/internal/handler/usage_handler.go @@ -138,7 +138,7 @@ func (h *UsageHandler) parseUserUsageFilters(c *gin.Context, requireRange bool) } billingMode := strings.TrimSpace(c.Query("billing_mode")) - if billingMode != "" && !service.BillingMode(billingMode).IsValid() { + if billingMode != "" && !service.BillingMode(billingMode).IsValidUsageFilter() { response.BadRequest(c, "Invalid billing_mode") return nil, false } diff --git a/backend/internal/handler/usage_handler_request_type_test.go b/backend/internal/handler/usage_handler_request_type_test.go index 1dcb1b83a4..8dc9a8b442 100644 --- a/backend/internal/handler/usage_handler_request_type_test.go +++ b/backend/internal/handler/usage_handler_request_type_test.go @@ -162,6 +162,18 @@ func TestUserUsageListInvalidBillingMode(t *testing.T) { require.Equal(t, http.StatusBadRequest, rec.Code) } +func TestUserUsageListAllowsVideoBillingMode(t *testing.T) { + repo := &userUsageRepoCapture{} + router := newUserUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/usage?billing_mode=video", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "video", repo.listFilters.BillingMode) +} + func TestUserUsageListKeepsUserBillingAndIPWithoutAdminCostFields(t *testing.T) { ipAddress := "203.0.113.10" upstreamModel := "upstream-private-model" diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 638eec52f1..d8b38a177f 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -185,6 +185,11 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, + group.FieldVideoRateIndependent, + group.FieldVideoRateMultiplier, + group.FieldVideoPrice480p, + group.FieldVideoPrice720p, + group.FieldVideoPrice1080p, group.FieldClaudeCodeOnly, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, @@ -899,6 +904,11 @@ func groupEntityToService(g *dbent.Group) *service.Group { ImagePrice4K: g.ImagePrice4k, BatchImageDiscountMultiplier: g.BatchImageDiscountMultiplier, BatchImageHoldMultiplier: g.BatchImageHoldMultiplier, + VideoRateIndependent: g.VideoRateIndependent, + VideoRateMultiplier: g.VideoRateMultiplier, + VideoPrice480P: g.VideoPrice480p, + VideoPrice720P: g.VideoPrice720p, + VideoPrice1080P: g.VideoPrice1080p, DefaultValidityDays: g.DefaultValidityDays, ClaudeCodeOnly: g.ClaudeCodeOnly, FallbackGroupID: g.FallbackGroupID, diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 0aab6b0c05..37529c60be 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -58,6 +58,11 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er SetNillableImagePrice4k(groupIn.ImagePrice4K). SetBatchImageDiscountMultiplier(groupIn.BatchImageDiscountMultiplier). SetBatchImageHoldMultiplier(groupIn.BatchImageHoldMultiplier). + SetVideoRateIndependent(groupIn.VideoRateIndependent). + SetVideoRateMultiplier(groupIn.VideoRateMultiplier). + SetNillableVideoPrice480p(groupIn.VideoPrice480P). + SetNillableVideoPrice720p(groupIn.VideoPrice720P). + SetNillableVideoPrice1080p(groupIn.VideoPrice1080P). SetDefaultValidityDays(groupIn.DefaultValidityDays). SetClaudeCodeOnly(groupIn.ClaudeCodeOnly). SetNillableFallbackGroupID(groupIn.FallbackGroupID). @@ -143,6 +148,11 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er SetNillableImagePrice4k(groupIn.ImagePrice4K). SetBatchImageDiscountMultiplier(groupIn.BatchImageDiscountMultiplier). SetBatchImageHoldMultiplier(groupIn.BatchImageHoldMultiplier). + SetVideoRateIndependent(groupIn.VideoRateIndependent). + SetVideoRateMultiplier(groupIn.VideoRateMultiplier). + SetNillableVideoPrice480p(groupIn.VideoPrice480P). + SetNillableVideoPrice720p(groupIn.VideoPrice720P). + SetNillableVideoPrice1080p(groupIn.VideoPrice1080P). SetDefaultValidityDays(groupIn.DefaultValidityDays). SetClaudeCodeOnly(groupIn.ClaudeCodeOnly). SetModelRoutingEnabled(groupIn.ModelRoutingEnabled). @@ -190,6 +200,21 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er } else { builder = builder.ClearImagePrice4k() } + if groupIn.VideoPrice480P != nil { + builder = builder.SetVideoPrice480p(*groupIn.VideoPrice480P) + } else { + builder = builder.ClearVideoPrice480p() + } + if groupIn.VideoPrice720P != nil { + builder = builder.SetVideoPrice720p(*groupIn.VideoPrice720P) + } else { + builder = builder.ClearVideoPrice720p() + } + if groupIn.VideoPrice1080P != nil { + builder = builder.SetVideoPrice1080p(*groupIn.VideoPrice1080P) + } else { + builder = builder.ClearVideoPrice1080p() + } // 处理 FallbackGroupID:nil 时清除,否则设置 if groupIn.FallbackGroupID != nil { diff --git a/backend/internal/repository/migrations_schema_integration_test.go b/backend/internal/repository/migrations_schema_integration_test.go index d39ac39cba..3235c7404a 100644 --- a/backend/internal/repository/migrations_schema_integration_test.go +++ b/backend/internal/repository/migrations_schema_integration_test.go @@ -66,6 +66,8 @@ func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) { "usage_logs", "usage_logs_image_billing_size_check", "image_count", + "billing_mode", + "'video'", "image_size IS NOT NULL", "'1K'", "'2K'", diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index cbdc863750..341bdff57d 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -80,7 +80,9 @@ func appendUsageLogBillingModeWhereConditionWithAlias(conditions []string, args placeholder := fmt.Sprintf("$%d", len(args)+1) switch service.BillingMode(mode) { case service.BillingModeImage: - conditions = append(conditions, fmt.Sprintf("(%s = %s OR COALESCE(%s, 0) > 0)", column("billing_mode"), placeholder, column("image_count"))) + conditions = append(conditions, fmt.Sprintf("(%s = %s OR ((%s IS NULL OR %s = '') AND COALESCE(%s, 0) > 0))", column("billing_mode"), placeholder, column("billing_mode"), column("billing_mode"), column("image_count"))) + case service.BillingModeVideo: + conditions = append(conditions, fmt.Sprintf("%s = %s", column("billing_mode"), placeholder)) case service.BillingModeToken: conditions = append(conditions, fmt.Sprintf("(%s = %s OR ((%s IS NULL OR %s = '') AND COALESCE(%s, 0) <= 0))", column("billing_mode"), placeholder, column("billing_mode"), column("billing_mode"), column("image_count"))) default: diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index e4d4e9a4fa..4a32557e71 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -281,9 +281,14 @@ func TestAppendUsageLogBillingModeWhereCondition(t *testing.T) { wantCondition string }{ { - name: "image includes legacy image rows", + name: "image includes explicit image and legacy image rows", billingMode: string(service.BillingModeImage), - wantCondition: "(billing_mode = $1 OR COALESCE(image_count, 0) > 0)", + wantCondition: "(billing_mode = $1 OR ((billing_mode IS NULL OR billing_mode = '') AND COALESCE(image_count, 0) > 0))", + }, + { + name: "video remains exact", + billingMode: string(service.BillingModeVideo), + wantCondition: "billing_mode = $1", }, { name: "token includes legacy non-image rows", @@ -309,7 +314,7 @@ func TestAppendUsageLogBillingModeWhereCondition(t *testing.T) { func TestAppendUsageLogBillingModeWhereConditionWithAlias(t *testing.T) { conditions, args := appendUsageLogBillingModeWhereConditionWithAlias(nil, nil, string(service.BillingModeImage), "ul") - require.Equal(t, []string{"(ul.billing_mode = $1 OR COALESCE(ul.image_count, 0) > 0)"}, conditions) + require.Equal(t, []string{"(ul.billing_mode = $1 OR ((ul.billing_mode IS NULL OR ul.billing_mode = '') AND COALESCE(ul.image_count, 0) > 0))"}, conditions) require.Equal(t, []any{string(service.BillingModeImage)}, args) } diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index d15ccc9c2e..f48ecb060a 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -363,12 +363,17 @@ func TestAPIContracts(t *testing.T) { "image_price_1k": null, "image_price_2k": null, "image_price_4k": null, + "video_price_480p": null, + "video_price_720p": null, + "video_price_1080p": null, "allow_image_generation": false, "allow_batch_image_generation": false, "batch_image_discount_multiplier": 0, "batch_image_hold_multiplier": 0, "image_rate_independent": false, "image_rate_multiplier": 0, + "video_rate_independent": false, + "video_rate_multiplier": 0, "claude_code_only": false, "allow_messages_dispatch": false, "fallback_group_id": null, diff --git a/backend/internal/service/admin_group.go b/backend/internal/service/admin_group.go index 43f7508722..c85056d623 100644 --- a/backend/internal/service/admin_group.go +++ b/backend/internal/service/admin_group.go @@ -153,6 +153,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn imagePrice1K := normalizePrice(input.ImagePrice1K) imagePrice2K := normalizePrice(input.ImagePrice2K) imagePrice4K := normalizePrice(input.ImagePrice4K) + videoPrice480P := normalizePrice(input.VideoPrice480P) + videoPrice720P := normalizePrice(input.VideoPrice720P) + videoPrice1080P := normalizePrice(input.VideoPrice1080P) imageRateMultiplier := 1.0 if input.ImageRateMultiplier != nil { if *input.ImageRateMultiplier < 0 { @@ -179,6 +182,13 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn if batchImageHoldMultiplier < batchImageDiscountMultiplier { return nil, errors.New("batch_image_hold_multiplier must be >= batch_image_discount_multiplier") } + videoRateMultiplier := 1.0 + if input.VideoRateMultiplier != nil { + if *input.VideoRateMultiplier < 0 { + return nil, errors.New("video_rate_multiplier must be >= 0") + } + videoRateMultiplier = *input.VideoRateMultiplier + } peakRateMultiplier := 1.0 if input.PeakRateMultiplier != nil { @@ -265,6 +275,8 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn ImageRateMultiplier: imageRateMultiplier, BatchImageDiscountMultiplier: batchImageDiscountMultiplier, BatchImageHoldMultiplier: batchImageHoldMultiplier, + VideoRateIndependent: input.VideoRateIndependent, + VideoRateMultiplier: videoRateMultiplier, PeakRateEnabled: peakRateEnabled, PeakStart: peakStart, PeakEnd: peakEnd, @@ -272,6 +284,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn ImagePrice1K: imagePrice1K, ImagePrice2K: imagePrice2K, ImagePrice4K: imagePrice4K, + VideoPrice480P: videoPrice480P, + VideoPrice720P: videoPrice720P, + VideoPrice1080P: videoPrice1080P, ClaudeCodeOnly: input.ClaudeCodeOnly, FallbackGroupID: input.FallbackGroupID, FallbackGroupIDOnInvalidRequest: fallbackOnInvalidRequest, @@ -482,6 +497,15 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd group.BatchImageHoldMultiplier < group.BatchImageDiscountMultiplier { return nil, errors.New("batch_image_hold_multiplier must be >= batch_image_discount_multiplier") } + if input.VideoRateIndependent != nil { + group.VideoRateIndependent = *input.VideoRateIndependent + } + if input.VideoRateMultiplier != nil { + if *input.VideoRateMultiplier < 0 { + return nil, errors.New("video_rate_multiplier must be >= 0") + } + group.VideoRateMultiplier = *input.VideoRateMultiplier + } if input.PeakRateEnabled != nil { group.PeakRateEnabled = *input.PeakRateEnabled } @@ -510,6 +534,15 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if input.ImagePrice4K != nil { group.ImagePrice4K = normalizePrice(input.ImagePrice4K) } + if input.VideoPrice480P != nil { + group.VideoPrice480P = normalizePrice(input.VideoPrice480P) + } + if input.VideoPrice720P != nil { + group.VideoPrice720P = normalizePrice(input.VideoPrice720P) + } + if input.VideoPrice1080P != nil { + group.VideoPrice1080P = normalizePrice(input.VideoPrice1080P) + } // Claude Code 客户端限制 if input.ClaudeCodeOnly != nil { diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index beffca8e41..b6377123b1 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -201,6 +201,8 @@ type CreateGroupInput struct { ImageRateMultiplier *float64 BatchImageDiscountMultiplier *float64 BatchImageHoldMultiplier *float64 + VideoRateIndependent bool + VideoRateMultiplier *float64 // 高峰时段倍率配置(PeakRateMultiplier 为 nil 时按 1.0 处理) PeakRateEnabled bool PeakStart string @@ -209,6 +211,9 @@ type CreateGroupInput struct { ImagePrice1K *float64 ImagePrice2K *float64 ImagePrice4K *float64 + VideoPrice480P *float64 + VideoPrice720P *float64 + VideoPrice1080P *float64 ClaudeCodeOnly bool // 仅允许 Claude Code 客户端 FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) @@ -250,6 +255,8 @@ type UpdateGroupInput struct { ImageRateMultiplier *float64 BatchImageDiscountMultiplier *float64 BatchImageHoldMultiplier *float64 + VideoRateIndependent *bool + VideoRateMultiplier *float64 // 高峰时段倍率配置(nil 表示不修改) PeakRateEnabled *bool PeakStart *string @@ -258,6 +265,9 @@ type UpdateGroupInput struct { ImagePrice1K *float64 ImagePrice2K *float64 ImagePrice4K *float64 + VideoPrice480P *float64 + VideoPrice720P *float64 + VideoPrice1080P *float64 ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端 FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index d2e3c49996..7731b33bb6 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -174,6 +174,42 @@ func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { require.InDelta(t, 0.30, *repo.created.ImagePrice4K, 0.0001) } +func TestAdminService_CreateGroup_WithVideoPricing(t *testing.T) { + repo := &groupRepoStubForAdmin{} + svc := &adminServiceImpl{groupRepo: repo} + + price480P := 0.08 + price720P := 0.12 + price1080P := 0.18 + videoMultiplier := 0.75 + + input := &CreateGroupInput{ + Name: "grok-video", + Description: "Grok video group", + Platform: PlatformGrok, + RateMultiplier: 1.0, + VideoRateIndependent: true, + VideoRateMultiplier: &videoMultiplier, + VideoPrice480P: &price480P, + VideoPrice720P: &price720P, + VideoPrice1080P: &price1080P, + } + + group, err := svc.CreateGroup(context.Background(), input) + require.NoError(t, err) + require.NotNil(t, group) + + require.NotNil(t, repo.created) + require.True(t, repo.created.VideoRateIndependent) + require.InDelta(t, 0.75, repo.created.VideoRateMultiplier, 1e-12) + require.NotNil(t, repo.created.VideoPrice480P) + require.NotNil(t, repo.created.VideoPrice720P) + require.NotNil(t, repo.created.VideoPrice1080P) + require.InDelta(t, 0.08, *repo.created.VideoPrice480P, 0.0001) + require.InDelta(t, 0.12, *repo.created.VideoPrice720P, 0.0001) + require.InDelta(t, 0.18, *repo.created.VideoPrice1080P, 0.0001) +} + // TestAdminService_CreateGroup_NilImagePricing 测试 ImagePrice 为 nil 时正常创建 func TestAdminService_CreateGroup_NilImagePricing(t *testing.T) { repo := &groupRepoStubForAdmin{} @@ -307,6 +343,42 @@ func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) { require.InDelta(t, 0.36, *repo.updated.ImagePrice4K, 0.0001) } +func TestAdminService_UpdateGroup_WithVideoPricing(t *testing.T) { + existingGroup := &Group{ + ID: 1, + Name: "existing-grok", + Platform: PlatformGrok, + Status: StatusActive, + } + repo := &groupRepoStubForAdmin{getByID: existingGroup} + svc := &adminServiceImpl{groupRepo: repo} + + price480P := 0.09 + price720P := 0.13 + price1080P := 0.19 + videoMultiplier := 0.6 + independent := true + + input := &UpdateGroupInput{ + VideoRateIndependent: &independent, + VideoRateMultiplier: &videoMultiplier, + VideoPrice480P: &price480P, + VideoPrice720P: &price720P, + VideoPrice1080P: &price1080P, + } + + group, err := svc.UpdateGroup(context.Background(), 1, input) + require.NoError(t, err) + require.NotNil(t, group) + + require.NotNil(t, repo.updated) + require.True(t, repo.updated.VideoRateIndependent) + require.InDelta(t, 0.6, repo.updated.VideoRateMultiplier, 1e-12) + require.InDelta(t, 0.09, *repo.updated.VideoPrice480P, 0.0001) + require.InDelta(t, 0.13, *repo.updated.VideoPrice720P, 0.0001) + require.InDelta(t, 0.19, *repo.updated.VideoPrice1080P, 0.0001) +} + // TestAdminService_UpdateGroup_PartialImagePricing 测试仅更新部分 ImagePrice 字段 func TestAdminService_UpdateGroup_PartialImagePricing(t *testing.T) { oldPrice2K := 0.15 @@ -542,6 +614,25 @@ func TestAdminService_GroupBatchImagePricingValidation(t *testing.T) { } } +func TestAdminService_UpdateGroup_RejectsNegativeVideoRateMultiplier(t *testing.T) { + existingGroup := &Group{ + ID: 1, + Name: "existing-group", + Platform: PlatformGrok, + Status: StatusActive, + VideoRateMultiplier: 1, + } + repo := &groupRepoStubForAdmin{getByID: existingGroup} + svc := &adminServiceImpl{groupRepo: repo} + negative := -0.1 + + _, err := svc.UpdateGroup(context.Background(), 1, &UpdateGroupInput{ + VideoRateMultiplier: &negative, + }) + require.Error(t, err) + require.Nil(t, repo.updated) +} + func TestAdminService_UpdateGroup_InvalidatesAuthCacheOnRPMLimitChange(t *testing.T) { existingGroup := &Group{ ID: 1, diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index 6f927ff3b8..11b5246a1d 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -73,6 +73,11 @@ type APIKeyAuthGroupSnapshot struct { ImagePrice1K *float64 `json:"image_price_1k,omitempty"` ImagePrice2K *float64 `json:"image_price_2k,omitempty"` ImagePrice4K *float64 `json:"image_price_4k,omitempty"` + VideoRateIndependent bool `json:"video_rate_independent"` + VideoRateMultiplier float64 `json:"video_rate_multiplier"` + VideoPrice480P *float64 `json:"video_price_480p,omitempty"` + VideoPrice720P *float64 `json:"video_price_720p,omitempty"` + VideoPrice1080P *float64 `json:"video_price_1080p,omitempty"` ClaudeCodeOnly bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id,omitempty"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request,omitempty"` diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index f3da3df493..539c7375d9 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -14,7 +14,7 @@ import ( "github.com/dgraph-io/ristretto" ) -const apiKeyAuthSnapshotVersion = 13 // v13: include group peak rate fields +const apiKeyAuthSnapshotVersion = 14 // v14: include group video pricing fields type apiKeyAuthCacheConfig struct { l1Size int @@ -265,6 +265,11 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey) ImagePrice1K: apiKey.Group.ImagePrice1K, ImagePrice2K: apiKey.Group.ImagePrice2K, ImagePrice4K: apiKey.Group.ImagePrice4K, + VideoRateIndependent: apiKey.Group.VideoRateIndependent, + VideoRateMultiplier: apiKey.Group.VideoRateMultiplier, + VideoPrice480P: apiKey.Group.VideoPrice480P, + VideoPrice720P: apiKey.Group.VideoPrice720P, + VideoPrice1080P: apiKey.Group.VideoPrice1080P, ClaudeCodeOnly: apiKey.Group.ClaudeCodeOnly, FallbackGroupID: apiKey.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: apiKey.Group.FallbackGroupIDOnInvalidRequest, @@ -343,6 +348,11 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho ImagePrice1K: snapshot.Group.ImagePrice1K, ImagePrice2K: snapshot.Group.ImagePrice2K, ImagePrice4K: snapshot.Group.ImagePrice4K, + VideoRateIndependent: snapshot.Group.VideoRateIndependent, + VideoRateMultiplier: snapshot.Group.VideoRateMultiplier, + VideoPrice480P: snapshot.Group.VideoPrice480P, + VideoPrice720P: snapshot.Group.VideoPrice720P, + VideoPrice1080P: snapshot.Group.VideoPrice1080P, ClaudeCodeOnly: snapshot.Group.ClaudeCodeOnly, FallbackGroupID: snapshot.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: snapshot.Group.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 89755c8922..a17b68fe12 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -1231,6 +1231,28 @@ type ImagePriceConfig struct { Price4K *float64 // 4K 尺寸价格(nil 表示使用默认值) } +// VideoPriceConfig 视频生成计费配置。 +type VideoPriceConfig struct { + Price480P *float64 // 480p 视频价格(nil 表示使用默认值) + Price720P *float64 // 720p 视频价格(nil 表示使用默认值) + Price1080P *float64 // 1080p 视频价格(nil 表示使用默认值) +} + +const ( + defaultImageGenerationPrice = 0.134 + + defaultGrokImagineImagePrice1K = 0.02 + defaultGrokImagineImagePrice2K = 0.02 + defaultGrokImagineImageQualityPrice1K = 0.05 + defaultGrokImagineImageQualityPrice2K = 0.07 + + defaultGrokImagineVideoPrice480P = 0.05 + defaultGrokImagineVideoPrice720P = 0.07 + defaultGrokImagineVideo15Price480P = 0.08 + defaultGrokImagineVideo15Price720P = 0.14 + defaultGrokImagineVideo15Price1080P = 0.25 +) + // CalculateImageCost 计算图片生成费用 // model: 请求的模型名称(用于获取 LiteLLM 默认价格) // imageSize: 图片尺寸 "1K", "2K", "4K" @@ -1262,6 +1284,33 @@ func (s *BillingService) CalculateImageCost(model string, imageSize string, imag } } +// CalculateVideoCost 计算视频生成费用。 +// model: 请求的模型名称(用于获取默认价格) +// resolution: 视频分辨率 "480p", "720p", "1080p" +// videoCount: 生成的视频数量 +// groupConfig: 分组配置的价格(可能为 nil,表示使用默认值) +// rateMultiplier: 费率倍数 +func (s *BillingService) CalculateVideoCost(model string, resolution string, videoCount int, groupConfig *VideoPriceConfig, rateMultiplier float64) *CostBreakdown { + if videoCount <= 0 { + return &CostBreakdown{} + } + resolution = NormalizeVideoBillingResolutionOrDefault(resolution) + + unitPrice := s.getVideoUnitPrice(model, resolution, groupConfig) + totalCost := unitPrice * float64(videoCount) + + if rateMultiplier < 0 { + rateMultiplier = 0 + } + actualCost := totalCost * rateMultiplier + + return &CostBreakdown{ + TotalCost: totalCost, + ActualCost: actualCost, + BillingMode: string(BillingModeVideo), + } +} + // getImageUnitPrice 获取图片单价 func (s *BillingService) getImageUnitPrice(model string, imageSize string, groupConfig *ImagePriceConfig) float64 { // 优先使用分组配置的价格 @@ -1286,8 +1335,33 @@ func (s *BillingService) getImageUnitPrice(model string, imageSize string, group return s.getDefaultImagePrice(model, imageSize) } +func (s *BillingService) getVideoUnitPrice(model string, resolution string, groupConfig *VideoPriceConfig) float64 { + if groupConfig != nil { + switch resolution { + case VideoBillingResolution480P: + if groupConfig.Price480P != nil { + return *groupConfig.Price480P + } + case VideoBillingResolution720P: + if groupConfig.Price720P != nil { + return *groupConfig.Price720P + } + case VideoBillingResolution1080P: + if groupConfig.Price1080P != nil { + return *groupConfig.Price1080P + } + } + } + + return s.getDefaultVideoPrice(model, resolution) +} + // getDefaultImagePrice 获取 LiteLLM 默认图片价格 func (s *BillingService) getDefaultImagePrice(model string, imageSize string) float64 { + if price, ok := getDefaultGrokImagineImagePrice(model, imageSize); ok { + return price + } + basePrice := 0.0 // 从 PricingService 获取 output_cost_per_image @@ -1300,7 +1374,7 @@ func (s *BillingService) getDefaultImagePrice(model string, imageSize string) fl // 如果没有找到价格,使用硬编码默认值($0.134,来自 gemini-3-pro-image-preview) if basePrice <= 0 { - basePrice = 0.134 + basePrice = defaultImageGenerationPrice } // 2K 尺寸 1.5 倍,4K 尺寸翻倍 @@ -1313,3 +1387,73 @@ func (s *BillingService) getDefaultImagePrice(model string, imageSize string) fl return basePrice } + +func (s *BillingService) getDefaultVideoPrice(model string, resolution string) float64 { + if price, ok := getDefaultGrokImagineVideoPrice(model, resolution); ok { + return price + } + + // The bundled LiteLLM schema does not expose an output video generation price. + // Keep the historical model default as the fallback, while letting group-level + // video prices override it independently from image prices. + return s.getDefaultImagePrice(model, ImageBillingSize2K) +} + +func getDefaultGrokImagineImagePrice(model string, imageSize string) (float64, bool) { + model = strings.ToLower(strings.TrimSpace(model)) + switch model { + case "grok-imagine-image-quality": + return getGrokImagineImageTierPrice( + imageSize, + defaultGrokImagineImageQualityPrice1K, + defaultGrokImagineImageQualityPrice2K, + ), true + case "grok-imagine", "grok-imagine-image", "grok-imagine-edit": + return getGrokImagineImageTierPrice( + imageSize, + defaultGrokImagineImagePrice1K, + defaultGrokImagineImagePrice2K, + ), true + default: + return 0, false + } +} + +func getGrokImagineImageTierPrice(imageSize string, price1K float64, price2K float64) float64 { + switch NormalizeImageBillingTierOrDefault(imageSize) { + case ImageBillingSize1K: + return price1K + case ImageBillingSize2K, ImageBillingSize4K: + return price2K + default: + return price2K + } +} + +func getDefaultGrokImagineVideoPrice(model string, resolution string) (float64, bool) { + model = strings.ToLower(strings.TrimSpace(model)) + switch { + case strings.HasPrefix(model, "grok-imagine-video-1.5"): + switch NormalizeVideoBillingResolutionOrDefault(resolution) { + case VideoBillingResolution480P: + return defaultGrokImagineVideo15Price480P, true + case VideoBillingResolution720P: + return defaultGrokImagineVideo15Price720P, true + case VideoBillingResolution1080P: + return defaultGrokImagineVideo15Price1080P, true + default: + return defaultGrokImagineVideo15Price480P, true + } + case strings.HasPrefix(model, "grok-imagine-video"): + switch NormalizeVideoBillingResolutionOrDefault(resolution) { + case VideoBillingResolution480P: + return defaultGrokImagineVideoPrice480P, true + case VideoBillingResolution720P, VideoBillingResolution1080P: + return defaultGrokImagineVideoPrice720P, true + default: + return defaultGrokImagineVideoPrice480P, true + } + default: + return 0, false + } +} diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index 92c143c6ff..f4c4213533 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -872,6 +872,50 @@ func TestCalculateImageCost(t *testing.T) { require.InDelta(t, 0.134*3, cost.ActualCost, 1e-10) } +func TestCalculateVideoCostUsesSeparateConfig(t *testing.T) { + svc := newTestBillingService() + + imagePrice := 0.4 + videoPrice := 0.08 + imageCost := svc.CalculateImageCost("grok-imagine-video", "2K", 1, &ImagePriceConfig{Price2K: &imagePrice}, 1.0) + videoCost := svc.CalculateVideoCost("grok-imagine-video", "480p", 1, &VideoPriceConfig{Price480P: &videoPrice}, 0.5) + + require.InDelta(t, 0.4, imageCost.TotalCost, 1e-10) + require.InDelta(t, 0.08, videoCost.TotalCost, 1e-10) + require.InDelta(t, 0.04, videoCost.ActualCost, 1e-10) + require.Equal(t, string(BillingModeVideo), videoCost.BillingMode) +} + +func TestCalculateGrokImagineImageCostUsesDefaultRateCard(t *testing.T) { + svc := newTestBillingService() + + standard1K := svc.CalculateImageCost("grok-imagine-image", "1K", 1, nil, 1.0) + standard2K := svc.CalculateImageCost("grok-imagine-image", "2K", 1, nil, 1.0) + quality1K := svc.CalculateImageCost("grok-imagine-image-quality", "1K", 1, nil, 1.0) + quality2K := svc.CalculateImageCost("grok-imagine-image-quality", "2K", 1, nil, 1.0) + + require.InDelta(t, 0.02, standard1K.TotalCost, 1e-10) + require.InDelta(t, 0.02, standard2K.TotalCost, 1e-10) + require.InDelta(t, 0.05, quality1K.TotalCost, 1e-10) + require.InDelta(t, 0.07, quality2K.TotalCost, 1e-10) +} + +func TestCalculateGrokImagineVideoCostUsesDefaultRateCard(t *testing.T) { + svc := newTestBillingService() + + standard480P := svc.CalculateVideoCost("grok-imagine-video", "480p", 1, nil, 1.0) + standard720P := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, nil, 1.0) + video15_480P := svc.CalculateVideoCost("grok-imagine-video-1.5", "480p", 1, nil, 1.0) + video15_720P := svc.CalculateVideoCost("grok-imagine-video-1.5", "720p", 1, nil, 1.0) + video15_1080P := svc.CalculateVideoCost("grok-imagine-video-1.5", "1080p", 1, nil, 1.0) + + require.InDelta(t, 0.05, standard480P.TotalCost, 1e-10) + require.InDelta(t, 0.07, standard720P.TotalCost, 1e-10) + require.InDelta(t, 0.08, video15_480P.TotalCost, 1e-10) + require.InDelta(t, 0.14, video15_720P.TotalCost, 1e-10) + require.InDelta(t, 0.25, video15_1080P.TotalCost, 1e-10) +} + func TestIsModelSupported(t *testing.T) { svc := newTestBillingService() diff --git a/backend/internal/service/channel.go b/backend/internal/service/channel.go index 88ed2df79c..1fd9e57068 100644 --- a/backend/internal/service/channel.go +++ b/backend/internal/service/channel.go @@ -14,6 +14,7 @@ const ( BillingModeToken BillingMode = "token" // 按 token 区间计费 BillingModePerRequest BillingMode = "per_request" // 按次计费(支持上下文窗口分层) BillingModeImage BillingMode = "image" // 图片计费(当前按次,预留 token 计费) + BillingModeVideo BillingMode = "video" // 视频生成计费(按视频生成次数) ) // IsValid 检查 BillingMode 是否为合法值 @@ -25,6 +26,15 @@ func (m BillingMode) IsValid() bool { return false } +// IsValidUsageFilter 检查 BillingMode 是否可用于使用记录筛选。 +func (m BillingMode) IsValidUsageFilter() bool { + switch m { + case BillingModeToken, BillingModePerRequest, BillingModeImage, BillingModeVideo, "": + return true + } + return false +} + const ( BillingModelSourceRequested = "requested" BillingModelSourceUpstream = "upstream" diff --git a/backend/internal/service/gateway_usage_billing.go b/backend/internal/service/gateway_usage_billing.go index 21685caae8..8a95915981 100644 --- a/backend/internal/service/gateway_usage_billing.go +++ b/backend/internal/service/gateway_usage_billing.go @@ -796,6 +796,10 @@ func (s *GatewayService) calculateImageCost( multiplier float64, ) *CostBreakdown { sizeTier := NormalizeImageBillingTierOrDefault(result.ImageSize) + groupConfig := imagePriceConfigFromAPIKey(apiKey) + if apiKeyHasConfiguredImagePrice(apiKey, sizeTier) { + return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) + } if resolved := s.resolveChannelPricing(ctx, billingModel, apiKey); resolved != nil { tokens := UsageTokens{ InputTokens: result.Usage.InputTokens, @@ -821,14 +825,6 @@ func (s *GatewayService) calculateImageCost( return cost } - var groupConfig *ImagePriceConfig - if apiKey.Group != nil { - groupConfig = &ImagePriceConfig{ - Price1K: apiKey.Group.ImagePrice1K, - Price2K: apiKey.Group.ImagePrice2K, - Price4K: apiKey.Group.ImagePrice4K, - } - } return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) } diff --git a/backend/internal/service/grok_media.go b/backend/internal/service/grok_media.go index 09d70eb0bd..70e439e46e 100644 --- a/backend/internal/service/grok_media.go +++ b/backend/internal/service/grok_media.go @@ -48,6 +48,7 @@ type GrokMediaRequestInfo struct { N int Size string SizeTier string + Resolution string InputImageURLs []string MaskImageURL string Uploads []OpenAIImagesUpload @@ -114,6 +115,7 @@ func ParseGrokMediaRequest(contentType string, body []byte) GrokMediaRequestInfo info.Prompt = strings.TrimSpace(info.Prompt) info.Size = strings.TrimSpace(info.Size) info.SizeTier = NormalizeImageBillingTierOrDefault(info.Size) + info.Resolution = NormalizeVideoBillingResolutionOrDefault(info.Resolution) if info.N <= 0 { info.N = 1 } @@ -127,6 +129,7 @@ func parseGrokMediaJSONRequest(body []byte, info *GrokMediaRequestInfo) { info.Model = strings.TrimSpace(gjson.GetBytes(body, "model").String()) info.Prompt = strings.TrimSpace(gjson.GetBytes(body, "prompt").String()) info.Size = strings.TrimSpace(gjson.GetBytes(body, "size").String()) + info.Resolution = strings.TrimSpace(gjson.GetBytes(body, "resolution").String()) if n := gjson.GetBytes(body, "n"); n.Exists() && n.Type == gjson.Number { info.N = int(n.Int()) } @@ -226,6 +229,8 @@ func parseGrokMediaMultipartRequest(contentType string, body []byte, info *GrokM info.Prompt = value case "size": info.Size = value + case "resolution": + info.Resolution = value case "n": if n, err := strconv.Atoi(value); err == nil { info.N = n @@ -363,6 +368,8 @@ func (s *OpenAIGatewayService) ForwardGrokMedia( ImageSize: usage.ImageSize, ImageInputSize: usage.ImageInputSize, ImageOutputSizes: usage.ImageOutputSizes, + VideoCount: usage.VideoCount, + VideoResolution: usage.VideoResolution, }, nil } @@ -471,6 +478,8 @@ type grokMediaUsageMetadata struct { ImageSize string ImageInputSize string ImageOutputSizes []string + VideoCount int + VideoResolution string } func grokMediaUsageFromResponse(endpoint GrokMediaEndpoint, requestInfo GrokMediaRequestInfo, responseBody []byte) grokMediaUsageMetadata { @@ -491,10 +500,10 @@ func grokMediaUsageFromResponse(endpoint GrokMediaEndpoint, requestInfo GrokMedi meta.ImageOutputSizes = collectOpenAIResponseImageOutputSizesFromJSONBytes(responseBody) case GrokMediaEndpointVideosGenerations: meta.ResponseID = extractGrokMediaVideoRequestID(responseBody) - // Video generation is one billable media unit; the legacy usage schema stores it in ImageCount. + meta.VideoCount = 1 + meta.VideoResolution = requestInfo.Resolution + // Keep the legacy media-unit counter populated for existing usage displays. meta.ImageCount = 1 - meta.ImageSize = requestInfo.SizeTier - meta.ImageInputSize = requestInfo.Size } return meta } diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index e3a1697b57..a61e356a01 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -45,6 +45,11 @@ type Group struct { ImagePrice4K *float64 BatchImageDiscountMultiplier float64 BatchImageHoldMultiplier float64 + VideoRateIndependent bool + VideoRateMultiplier float64 + VideoPrice480P *float64 + VideoPrice720P *float64 + VideoPrice1080P *float64 // Claude Code 客户端限制 ClaudeCodeOnly bool @@ -125,6 +130,21 @@ func (g *Group) GetImagePrice(imageSize string) *float64 { } } +// GetVideoPrice 根据 resolution 返回对应的视频生成价格。 +// 如果分组未配置价格,返回 nil(调用方应使用默认值)。 +func (g *Group) GetVideoPrice(resolution string) *float64 { + switch NormalizeVideoBillingResolutionOrDefault(resolution) { + case VideoBillingResolution480P: + return g.VideoPrice480P + case VideoBillingResolution720P: + return g.VideoPrice720P + case VideoBillingResolution1080P: + return g.VideoPrice1080P + default: + return g.VideoPrice480P + } +} + // IsGroupContextValid reports whether a group from context has the fields required for routing decisions. func IsGroupContextValid(group *Group) bool { if group == nil { diff --git a/backend/internal/service/image_billing_multiplier.go b/backend/internal/service/image_billing_multiplier.go index 23ec5ac104..6b7172c2a0 100644 --- a/backend/internal/service/image_billing_multiplier.go +++ b/backend/internal/service/image_billing_multiplier.go @@ -9,3 +9,13 @@ func resolveImageRateMultiplier(apiKey *APIKey, effectiveGroupMultiplier float64 } return effectiveGroupMultiplier } + +func resolveVideoRateMultiplier(apiKey *APIKey, effectiveGroupMultiplier float64) float64 { + if apiKey != nil && apiKey.Group != nil && apiKey.Group.VideoRateIndependent { + if apiKey.Group.VideoRateMultiplier < 0 { + return 0 + } + return apiKey.Group.VideoRateMultiplier + } + return effectiveGroupMultiplier +} diff --git a/backend/internal/service/media_price_config.go b/backend/internal/service/media_price_config.go new file mode 100644 index 0000000000..ed84998906 --- /dev/null +++ b/backend/internal/service/media_price_config.go @@ -0,0 +1,31 @@ +package service + +func imagePriceConfigFromAPIKey(apiKey *APIKey) *ImagePriceConfig { + if apiKey == nil || apiKey.Group == nil { + return nil + } + return &ImagePriceConfig{ + Price1K: apiKey.Group.ImagePrice1K, + Price2K: apiKey.Group.ImagePrice2K, + Price4K: apiKey.Group.ImagePrice4K, + } +} + +func apiKeyHasConfiguredImagePrice(apiKey *APIKey, imageSize string) bool { + return apiKey != nil && apiKey.Group != nil && apiKey.Group.GetImagePrice(imageSize) != nil +} + +func videoPriceConfigFromAPIKey(apiKey *APIKey) *VideoPriceConfig { + if apiKey == nil || apiKey.Group == nil { + return nil + } + return &VideoPriceConfig{ + Price480P: apiKey.Group.VideoPrice480P, + Price720P: apiKey.Group.VideoPrice720P, + Price1080P: apiKey.Group.VideoPrice1080P, + } +} + +func apiKeyHasConfiguredVideoPrice(apiKey *APIKey, resolution string) bool { + return apiKey != nil && apiKey.Group != nil && apiKey.Group.GetVideoPrice(resolution) != nil +} diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index f6aa4b6cd1..c7d1cd552c 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -201,6 +201,13 @@ func TestParseGrokMediaRequestBuildsMultipartModerationBody(t *testing.T) { require.True(t, strings.HasPrefix(gjson.GetBytes(moderationBody, "images.0.image_url").String(), "data:image/")) } +func TestParseGrokMediaVideoRequestResolution(t *testing.T) { + info := ParseGrokMediaRequest("application/json", []byte(`{"model":"grok-imagine-video","prompt":"waves","resolution":"720p"}`)) + + require.Equal(t, "grok-imagine-video", info.Model) + require.Equal(t, "720p", info.Resolution) +} + func TestNormalizeGrokMediaModelForEndpoint(t *testing.T) { tests := []struct { name string @@ -330,7 +337,7 @@ func TestForwardGrokMediaVideoGenerationReturnsUsageAndResponseID(t *testing.T) recorder := httptest.NewRecorder() c, _ := gin.CreateTestContext(recorder) - body := []byte(`{"model":"grok-imagine-video-1.5","prompt":"waves"}`) + body := []byte(`{"model":"grok-imagine-video-1.5","prompt":"waves","resolution":"720p"}`) c.Request = httptest.NewRequest(http.MethodPost, "/v1/videos/generations", bytes.NewReader(body)) c.Request.Header.Set("Content-Type", "application/json") @@ -358,12 +365,15 @@ func TestForwardGrokMediaVideoGenerationReturnsUsageAndResponseID(t *testing.T) result, err := svc.ForwardGrokMedia(context.Background(), c, account, GrokMediaEndpointVideosGenerations, "", body, "application/json") require.NoError(t, err) require.Equal(t, "https://xai.test/v1/videos/generations", upstream.lastReq.URL.String()) - require.JSONEq(t, `{"model":"grok-imagine-video","prompt":"waves"}`, string(upstream.lastBody)) + require.JSONEq(t, `{"model":"grok-imagine-video","prompt":"waves","resolution":"720p"}`, string(upstream.lastBody)) require.Equal(t, "video-request-123", result.ResponseID) require.Equal(t, "grok-imagine-video", result.BillingModel) require.Equal(t, 3, result.Usage.InputTokens) require.Equal(t, 4, result.Usage.OutputTokens) require.Equal(t, 1, result.ImageCount) + require.Empty(t, result.ImageSize) + require.Equal(t, 1, result.VideoCount) + require.Equal(t, VideoBillingResolution720P, result.VideoResolution) } func TestForwardGrokMediaVideoGenerationPreservesImageToVideoModel(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index 697d89e81c..c3622a82e9 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -1803,8 +1803,9 @@ func TestOpenAIGatewayServiceRecordUsage_ImageIndependentMultiplierUsesImageRate require.Equal(t, string(BillingModeImage), *usageRepo.lastLog.BillingMode) } -func TestGrokVideoMediaBillingUsesImageRateMultiplier(t *testing.T) { - mediaPrice2K := 0.4 +func TestGrokVideoBillingUsesSeparateVideoRateMultiplier(t *testing.T) { + imagePrice2K := 0.4 + videoPrice480P := 0.08 groupID := int64(126) usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} @@ -1812,14 +1813,14 @@ func TestGrokVideoMediaBillingUsesImageRateMultiplier(t *testing.T) { err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ Result: &OpenAIForwardResult{ - RequestID: "video-request-123", - ResponseID: "video-request-123", - Model: "grok-imagine-video-1.5", - BillingModel: "grok-imagine-video-1.5", - // The usage schema has no separate video count; video generation is billed as one media unit. - ImageCount: 1, - ImageSize: ImageBillingSize2K, - Duration: time.Second, + RequestID: "video-request-123", + ResponseID: "video-request-123", + Model: "grok-imagine-video-1.5", + BillingModel: "grok-imagine-video-1.5", + ImageCount: 1, + VideoCount: 1, + VideoResolution: VideoBillingResolution480P, + Duration: time.Second, }, APIKey: &APIKey{ ID: 10126, @@ -1830,7 +1831,10 @@ func TestGrokVideoMediaBillingUsesImageRateMultiplier(t *testing.T) { RateMultiplier: 0.15, ImageRateIndependent: true, ImageRateMultiplier: 0.5, - ImagePrice2K: &mediaPrice2K, + ImagePrice2K: &imagePrice2K, + VideoRateIndependent: true, + VideoRateMultiplier: 0.25, + VideoPrice480P: &videoPrice480P, }, }, User: &User{ID: 20126}, @@ -1841,14 +1845,236 @@ func TestGrokVideoMediaBillingUsesImageRateMultiplier(t *testing.T) { require.NotNil(t, usageRepo.lastLog) require.Equal(t, "grok-imagine-video-1.5", usageRepo.lastLog.Model) require.Equal(t, 1, usageRepo.lastLog.ImageCount) + require.Nil(t, usageRepo.lastLog.ImageSize) + require.InDelta(t, 0.08, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.02, usageRepo.lastLog.ActualCost, 1e-12) + require.InDelta(t, 0.25, usageRepo.lastLog.RateMultiplier, 1e-12) + require.NotNil(t, usageRepo.lastLog.BillingMode) + require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode) +} + +func TestOpenAIGatewayServiceRecordUsage_GrokVideoUsesDefaultRateCard(t *testing.T) { + groupID := int64(1261) + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "video-default-rate-card", + ResponseID: "video-default-rate-card", + Model: "grok-imagine-video-1.5", + BillingModel: "grok-imagine-video-1.5", + ImageCount: 1, + VideoCount: 1, + VideoResolution: VideoBillingResolution720P, + Duration: time.Second, + }, + APIKey: &APIKey{ + ID: 101261, + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + }, + }, + User: &User{ID: 201261}, + Account: &Account{ID: 301261, Platform: PlatformGrok}, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Nil(t, usageRepo.lastLog.ImageSize) + require.InDelta(t, 0.14, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.14, usageRepo.lastLog.ActualCost, 1e-12) + require.Equal(t, 1, usageRepo.lastLog.ImageCount) + require.NotNil(t, usageRepo.lastLog.BillingMode) + require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode) +} + +func TestOpenAIGatewayServiceRecordUsage_GroupImagePriceOverridesChannelImagePrice(t *testing.T) { + groupID := int64(127) + channelPrice := 0.201 + groupImagePrice2K := 0.021 + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + svc.resolver = newOpenAIImageChannelPricingResolverForTest(t, groupID, "grok-imagine-image-quality", channelPrice) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_image_group_price", + Model: "grok-imagine-image-quality", + BillingModel: "grok-imagine-image-quality", + ImageCount: 1, + ImageSize: ImageBillingSize2K, + Duration: time.Second, + }, + APIKey: &APIKey{ + ID: 10127, + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + ImageRateIndependent: true, + ImageRateMultiplier: 1, + ImagePrice2K: &groupImagePrice2K, + }, + }, + User: &User{ID: 20127}, + Account: &Account{ID: 30127, Platform: PlatformGrok}, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Equal(t, 1, usageRepo.lastLog.ImageCount) require.Equal(t, ImageBillingSize2K, *usageRepo.lastLog.ImageSize) - require.InDelta(t, 0.4, usageRepo.lastLog.TotalCost, 1e-12) - require.InDelta(t, 0.2, usageRepo.lastLog.ActualCost, 1e-12) - require.InDelta(t, 0.5, usageRepo.lastLog.RateMultiplier, 1e-12) + require.InDelta(t, 0.021, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.021, usageRepo.lastLog.ActualCost, 1e-12) require.NotNil(t, usageRepo.lastLog.BillingMode) require.Equal(t, string(BillingModeImage), *usageRepo.lastLog.BillingMode) } +func TestOpenAIGatewayServiceRecordUsage_GroupVideoPriceOverridesChannelImagePrice(t *testing.T) { + groupID := int64(128) + channelPrice := 0.201 + groupVideoPrice720P := 0.037 + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + svc.resolver = newOpenAIImageChannelPricingResolverForTest(t, groupID, "grok-imagine-video", channelPrice) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_video_group_price", + Model: "grok-imagine-video", + BillingModel: "grok-imagine-video", + ImageCount: 1, + VideoCount: 1, + VideoResolution: VideoBillingResolution720P, + Duration: time.Second, + }, + APIKey: &APIKey{ + ID: 10128, + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + VideoRateIndependent: true, + VideoRateMultiplier: 1, + VideoPrice720P: &groupVideoPrice720P, + }, + }, + User: &User{ID: 20128}, + Account: &Account{ID: 30128, Platform: PlatformGrok}, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Equal(t, 1, usageRepo.lastLog.ImageCount) + require.Nil(t, usageRepo.lastLog.ImageSize) + require.InDelta(t, 0.037, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.037, usageRepo.lastLog.ActualCost, 1e-12) + require.NotNil(t, usageRepo.lastLog.BillingMode) + require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode) +} + +func TestOpenAIGatewayServiceRecordUsage_HydratesGroupImagePriceWhenAuthSnapshotOmitsIt(t *testing.T) { + groupID := int64(130) + groupImagePrice2K := 0.021 + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + channelService := &ChannelService{groupRepo: &openAIMediaPriceGroupRepoStub{group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + ImagePrice2K: &groupImagePrice2K, + }}} + channelCache := newEmptyChannelCache() + channelCache.loadedAt = time.Now() + channelService.cache.Store(channelCache) + svc.channelService = channelService + refreshed := svc.apiKeyWithFreshGroupMediaPricing(context.Background(), &APIKey{GroupID: i64p(groupID), Group: &Group{ID: groupID}}) + require.NotNil(t, refreshed.Group.ImagePrice2K) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_image_hydrated_price", + Model: "grok-imagine-image-quality", + BillingModel: "grok-imagine-image-quality", + ImageCount: 1, + ImageSize: ImageBillingSize2K, + Duration: time.Second, + }, + APIKey: &APIKey{ + ID: 10130, + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + }, + }, + User: &User{ID: 20130}, + Account: &Account{ID: 30130, Platform: PlatformGrok}, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.InDelta(t, 0.021, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.021, usageRepo.lastLog.ActualCost, 1e-12) + require.Equal(t, string(BillingModeImage), *usageRepo.lastLog.BillingMode) +} + +func TestOpenAIGatewayServiceRecordUsage_HydratesGroupVideoPriceWhenAuthSnapshotOmitsIt(t *testing.T) { + groupID := int64(131) + groupVideoPrice720P := 0.037 + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + channelService := &ChannelService{groupRepo: &openAIMediaPriceGroupRepoStub{group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + VideoPrice720P: &groupVideoPrice720P, + }}} + channelCache := newEmptyChannelCache() + channelCache.loadedAt = time.Now() + channelService.cache.Store(channelCache) + svc.channelService = channelService + refreshed := svc.apiKeyWithFreshGroupMediaPricing(context.Background(), &APIKey{GroupID: i64p(groupID), Group: &Group{ID: groupID}}) + require.NotNil(t, refreshed.Group.VideoPrice720P) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_video_hydrated_price", + Model: "grok-imagine-video", + BillingModel: "grok-imagine-video", + ImageCount: 1, + VideoCount: 1, + VideoResolution: VideoBillingResolution720P, + Duration: time.Second, + }, + APIKey: &APIKey{ + ID: 10131, + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + Platform: PlatformGrok, + RateMultiplier: 1, + }, + }, + User: &User{ID: 20131}, + Account: &Account{ID: 30131, Platform: PlatformGrok}, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Nil(t, usageRepo.lastLog.ImageSize) + require.InDelta(t, 0.037, usageRepo.lastLog.TotalCost, 1e-12) + require.InDelta(t, 0.037, usageRepo.lastLog.ActualCost, 1e-12) + require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode) +} + func TestOpenAIGatewayServiceRecordUsage_ChannelImageBillingUsesImageCountAndSharedMultiplier(t *testing.T) { groupID := int64(123) usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} @@ -1960,6 +2186,19 @@ func newOpenAITokenImageChannelPricingResolverForTest(t *testing.T, groupID int6 return NewModelPricingResolver(cs, NewBillingService(&config.Config{}, nil)) } +type openAIMediaPriceGroupRepoStub struct { + GroupRepository + group *Group + err error +} + +func (s *openAIMediaPriceGroupRepoStub) GetByIDLite(context.Context, int64) (*Group, error) { + if s.err != nil { + return nil, s.err + } + return s.group, nil +} + func TestGatewayServiceCalculateRecordUsageCost_ChannelImageBillingUsesImageCount(t *testing.T) { groupID := int64(126) billingService := NewBillingService(&config.Config{}, nil) @@ -2023,6 +2262,38 @@ func TestGatewayServiceCalculateRecordUsageCost_ChannelImageBillingUsesSizeTier( require.InDelta(t, 0.80, cost.ActualCost, 1e-12) } +func TestGatewayServiceCalculateRecordUsageCost_GroupImagePriceOverridesChannelImagePrice(t *testing.T) { + groupID := int64(129) + channelPrice := 0.25 + groupImagePrice2K := 0.021 + + svc := &GatewayService{ + billingService: NewBillingService(&config.Config{}, nil), + resolver: newOpenAIImageChannelPricingResolverForTest(t, groupID, "gemini-image", channelPrice), + } + + cost := svc.calculateRecordUsageCost( + context.Background(), + &ForwardResult{Model: "gemini-image", ImageCount: 2, ImageSize: ImageBillingSize2K}, + &APIKey{ + GroupID: i64p(groupID), + Group: &Group{ + ID: groupID, + ImagePrice2K: &groupImagePrice2K, + }, + }, + "gemini-image", + 1.0, + 1.0, + nil, + ) + + require.NotNil(t, cost) + require.Equal(t, string(BillingModeImage), cost.BillingMode) + require.InDelta(t, 0.042, cost.TotalCost, 1e-12) + require.InDelta(t, 0.042, cost.ActualCost, 1e-12) +} + func TestRecordUsageMarksCyberRequestType(t *testing.T) { logStub := &openAIRecordUsageLogRepoStub{inserted: true} userStub := &openAIRecordUsageUserRepoStub{} diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index c1d1670e07..822a5f65d3 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -242,6 +242,8 @@ type OpenAIForwardResult struct { ImageOutputSizes []string ImageSizeSource string ImageSizeBreakdown map[string]int + VideoCount int + VideoResolution string wsReplayInput []json.RawMessage wsReplayInputExists bool diff --git a/backend/internal/service/openai_gateway_usage.go b/backend/internal/service/openai_gateway_usage.go index 58b6029ffa..569a7a74ba 100644 --- a/backend/internal/service/openai_gateway_usage.go +++ b/backend/internal/service/openai_gateway_usage.go @@ -115,7 +115,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec user := input.User account := input.Account subscription := input.Subscription - ApplyOpenAIImageBillingResolution(result) + if !isGrokVideoUsageResult(result, nil) { + ApplyOpenAIImageBillingResolution(result) + } // 计算实际的新输入token(减去缓存读取的token) // 因为 input_tokens 包含了 cache_read_tokens,而缓存读取的token不应按输入价格计费 @@ -148,7 +150,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec } // token 倍率叠加高峰因子(token 计费含图片 token,图片按次倍率不受影响)。高峰因子按请求时刻现算, // 不并入上面的 Resolve,以免污染 user:group 倍率缓存。 - multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, multiplier, timezone.Now()) + baseMultiplier := multiplier + multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, baseMultiplier, timezone.Now()) + videoMultiplier := resolveVideoRateMultiplier(apiKey, baseMultiplier) var cost *CostBreakdown var err error @@ -174,7 +178,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec if result.ServiceTier != nil { serviceTier = strings.TrimSpace(*result.ServiceTier) } - cost, err = s.calculateOpenAIRecordUsageCost(ctx, result, apiKey, billingModels, multiplier, imageMultiplier, tokens, serviceTier) + cost, err = s.calculateOpenAIRecordUsageCost(ctx, result, apiKey, billingModels, multiplier, imageMultiplier, videoMultiplier, tokens, serviceTier) if err != nil { if !isUsagePricingUnavailableError(err) { return err @@ -238,6 +242,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec ImageSizeSource: optionalTrimmedStringPtr(result.ImageSizeSource), ImageSizeBreakdown: result.ImageSizeBreakdown, } + isVideoUsage := isGrokVideoUsageResult(result, billingModels) if cost != nil { usageLog.InputCost = cost.InputCost usageLog.OutputCost = cost.OutputCost @@ -247,7 +252,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec usageLog.TotalCost = cost.TotalCost usageLog.ActualCost = cost.ActualCost } - if result.ImageCount > 0 && (cost == nil || cost.BillingMode != string(BillingModeToken)) { + if isVideoUsage && (cost == nil || cost.BillingMode != string(BillingModeToken)) { + usageLog.RateMultiplier = videoMultiplier + } else if result.ImageCount > 0 && (cost == nil || cost.BillingMode != string(BillingModeToken)) { usageLog.RateMultiplier = imageMultiplier } else { usageLog.RateMultiplier = multiplier @@ -269,6 +276,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec if cost != nil && cost.BillingMode != "" { billingMode := cost.BillingMode usageLog.BillingMode = &billingMode + } else if isVideoUsage { + billingMode := string(BillingModeVideo) + usageLog.BillingMode = &billingMode } else if result.ImageCount > 0 { billingMode := string(BillingModeImage) usageLog.BillingMode = &billingMode @@ -346,10 +356,16 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost( billingModels []string, multiplier float64, imageMultiplier float64, + videoMultiplier float64, tokens UsageTokens, serviceTier string, ) (*CostBreakdown, error) { billingModel := firstUsageBillingModel(billingModels) + if isGrokVideoUsageResult(result, billingModels) { + if resolved := s.resolveOpenAIChannelPricing(ctx, billingModel, apiKey); resolved == nil || resolved.Mode != BillingModeToken { + return s.calculateOpenAIVideoCost(ctx, billingModel, apiKey, result, videoMultiplier), nil + } + } if result != nil && result.ImageCount > 0 { // 渠道定价为 token 计费时走 token 路径,否则走图片计费 if resolved := s.resolveOpenAIChannelPricing(ctx, billingModel, apiKey); resolved == nil || resolved.Mode != BillingModeToken { @@ -377,6 +393,24 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost( return nil, fmt.Errorf("calculate OpenAI usage cost failed for billing models %s: %w", strings.Join(billingModels, ","), lastErr) } +func isGrokVideoBillingModel(model string) bool { + return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "grok-imagine-video") +} + +func isGrokVideoUsageResult(result *OpenAIForwardResult, billingModels []string) bool { + if result == nil || result.VideoCount <= 0 { + return false + } + candidates := append([]string{}, billingModels...) + candidates = append(candidates, result.BillingModel, result.Model, result.UpstreamModel) + for _, candidate := range candidates { + if isGrokVideoBillingModel(candidate) { + return true + } + } + return false +} + func isUsagePricingUnavailableError(err error) bool { if err == nil { return false @@ -420,6 +454,17 @@ func (s *OpenAIGatewayService) calculateOpenAIImageCost( multiplier float64, ) *CostBreakdown { sizeTier := NormalizeImageBillingTierOrDefault(result.ImageSize) + groupConfig := imagePriceConfigFromAPIKey(apiKey) + if apiKeyHasConfiguredImagePrice(apiKey, sizeTier) { + return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) + } + if refreshed := s.apiKeyWithFreshGroupMediaPricing(ctx, apiKey); refreshed != apiKey { + apiKey = refreshed + groupConfig = imagePriceConfigFromAPIKey(apiKey) + if apiKeyHasConfiguredImagePrice(apiKey, sizeTier) { + return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) + } + } if resolved := s.resolveOpenAIChannelPricing(ctx, billingModel, apiKey); resolved != nil && (resolved.Mode == BillingModePerRequest || resolved.Mode == BillingModeImage) { gid := apiKey.Group.ID @@ -439,15 +484,69 @@ func (s *OpenAIGatewayService) calculateOpenAIImageCost( logger.LegacyPrintf("service.openai_gateway", "Calculate image channel cost failed: %v", err) } - var groupConfig *ImagePriceConfig - if apiKey != nil && apiKey.Group != nil { - groupConfig = &ImagePriceConfig{ - Price1K: apiKey.Group.ImagePrice1K, - Price2K: apiKey.Group.ImagePrice2K, - Price4K: apiKey.Group.ImagePrice4K, + return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) +} + +func (s *OpenAIGatewayService) calculateOpenAIVideoCost( + ctx context.Context, + billingModel string, + apiKey *APIKey, + result *OpenAIForwardResult, + multiplier float64, +) *CostBreakdown { + videoCount := result.VideoCount + if videoCount <= 0 { + videoCount = 1 + } + resolution := NormalizeVideoBillingResolutionOrDefault(result.VideoResolution) + groupConfig := videoPriceConfigFromAPIKey(apiKey) + if apiKeyHasConfiguredVideoPrice(apiKey, resolution) { + return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, groupConfig, multiplier) + } + if refreshed := s.apiKeyWithFreshGroupMediaPricing(ctx, apiKey); refreshed != apiKey { + apiKey = refreshed + groupConfig = videoPriceConfigFromAPIKey(apiKey) + if apiKeyHasConfiguredVideoPrice(apiKey, resolution) { + return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, groupConfig, multiplier) } } - return s.billingService.CalculateImageCost(billingModel, sizeTier, result.ImageCount, groupConfig, multiplier) + if resolved := s.resolveOpenAIChannelPricing(ctx, billingModel, apiKey); resolved != nil && + (resolved.Mode == BillingModePerRequest || resolved.Mode == BillingModeImage) { + gid := apiKey.Group.ID + cost, err := s.billingService.CalculateCostUnified(CostInput{ + Ctx: ctx, + Model: billingModel, + GroupID: &gid, + RequestCount: videoCount, + SizeTier: resolution, + RateMultiplier: multiplier, + Resolver: s.resolver, + Resolved: resolved, + }) + if err == nil { + cost.BillingMode = string(BillingModeVideo) + return cost + } + logger.LegacyPrintf("service.openai_gateway", "Calculate video channel cost failed: %v", err) + } + + return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, groupConfig, multiplier) +} + +func (s *OpenAIGatewayService) apiKeyWithFreshGroupMediaPricing(ctx context.Context, apiKey *APIKey) *APIKey { + if apiKey == nil || apiKey.GroupID == nil || *apiKey.GroupID <= 0 { + return apiKey + } + if s == nil || s.channelService == nil || s.channelService.groupRepo == nil { + return apiKey + } + group, err := s.channelService.groupRepo.GetByIDLite(ctx, *apiKey.GroupID) + if err != nil || group == nil { + return apiKey + } + clone := *apiKey + clone.Group = group + return &clone } func (s *OpenAIGatewayService) resolveOpenAIChannelPricing(ctx context.Context, billingModel string, apiKey *APIKey) *ResolvedPricing { diff --git a/backend/internal/service/video_billing_resolution.go b/backend/internal/service/video_billing_resolution.go new file mode 100644 index 0000000000..bca068f097 --- /dev/null +++ b/backend/internal/service/video_billing_resolution.go @@ -0,0 +1,22 @@ +package service + +import "strings" + +const ( + VideoBillingResolution480P = "480p" + VideoBillingResolution720P = "720p" + VideoBillingResolution1080P = "1080p" +) + +func NormalizeVideoBillingResolutionOrDefault(resolution string) string { + switch strings.ToLower(strings.TrimSpace(resolution)) { + case "480", "480p", "sd": + return VideoBillingResolution480P + case "720", "720p", "hd": + return VideoBillingResolution720P + case "1080", "1080p", "full_hd", "full-hd", "fhd": + return VideoBillingResolution1080P + default: + return VideoBillingResolution480P + } +} diff --git a/backend/migrations/170_add_grok_video_pricing_controls.sql b/backend/migrations/170_add_grok_video_pricing_controls.sql new file mode 100644 index 0000000000..9c798a9945 --- /dev/null +++ b/backend/migrations/170_add_grok_video_pricing_controls.sql @@ -0,0 +1,16 @@ +-- Add independent group pricing controls for Grok video generation. +-- Video prices intentionally do not backfill from image prices: image and video +-- generation must be priced separately. + +ALTER TABLE groups + ADD COLUMN IF NOT EXISTS video_rate_independent BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS video_rate_multiplier DECIMAL(10,4) NOT NULL DEFAULT 1.0, + ADD COLUMN IF NOT EXISTS video_price_480p DECIMAL(20,8), + ADD COLUMN IF NOT EXISTS video_price_720p DECIMAL(20,8), + ADD COLUMN IF NOT EXISTS video_price_1080p DECIMAL(20,8); + +COMMENT ON COLUMN groups.video_rate_independent IS '视频生成是否使用独立倍率;false 表示共享分组有效倍率'; +COMMENT ON COLUMN groups.video_rate_multiplier IS '视频生成独立倍率,仅 video_rate_independent=true 时生效'; +COMMENT ON COLUMN groups.video_price_480p IS '480p 视频生成单价 (USD),Grok 平台使用'; +COMMENT ON COLUMN groups.video_price_720p IS '720p 视频生成单价 (USD),Grok 平台使用'; +COMMENT ON COLUMN groups.video_price_1080p IS '1080p 视频生成单价 (USD),Grok 平台使用'; diff --git a/backend/migrations/171_allow_video_usage_without_image_size.sql b/backend/migrations/171_allow_video_usage_without_image_size.sql new file mode 100644 index 0000000000..767a15c00e --- /dev/null +++ b/backend/migrations/171_allow_video_usage_without_image_size.sql @@ -0,0 +1,17 @@ +-- Grok video generation stores billing_mode='video' and keeps image_count=1 +-- only as a legacy media-unit counter. It must not be forced to carry an +-- image_size, because video pricing uses video_resolution/request metadata. + +ALTER TABLE usage_logs + DROP CONSTRAINT IF EXISTS usage_logs_image_billing_size_check; + +ALTER TABLE usage_logs + ADD CONSTRAINT usage_logs_image_billing_size_check + CHECK ( + image_count <= 0 + OR billing_mode = 'video' + OR ( + image_size IS NOT NULL + AND image_size IN ('1K', '2K', '4K', 'mixed') + ) + ) NOT VALID; diff --git a/frontend/src/components/admin/usage/UsageFilters.vue b/frontend/src/components/admin/usage/UsageFilters.vue index bb63d9b8ee..2c8127609d 100644 --- a/frontend/src/components/admin/usage/UsageFilters.vue +++ b/frontend/src/components/admin/usage/UsageFilters.vue @@ -294,7 +294,8 @@ const billingModeOptions = ref([ { value: null, label: t('admin.usage.allBillingModes') }, { value: 'token', label: t('admin.usage.billingModeToken') }, { value: 'per_request', label: t('admin.usage.billingModePerRequest') }, - { value: 'image', label: t('admin.usage.billingModeImage') } + { value: 'image', label: t('admin.usage.billingModeImage') }, + { value: 'video', label: t('admin.usage.billingModeVideo') } ]) const emitChange = () => emit('change') diff --git a/frontend/src/i18n/locales/en/admin/channels.ts b/frontend/src/i18n/locales/en/admin/channels.ts index 399b9f3208..07a24d0fd5 100644 --- a/frontend/src/i18n/locales/en/admin/channels.ts +++ b/frontend/src/i18n/locales/en/admin/channels.ts @@ -26,6 +26,7 @@ export default { billingModeToken: 'Per Token', billingModePerRequest: 'Per Request', billingModeImage: 'Per Image', + billingModeVideo: 'Per Video', inputPrice: 'Input', outputPrice: 'Output', cacheWritePrice: 'Cache Write', diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts index 57e7f21137..545135d3a1 100644 --- a/frontend/src/i18n/locales/en/admin/overview.ts +++ b/frontend/src/i18n/locales/en/admin/overview.ts @@ -841,6 +841,28 @@ export default { finalPricePreview: 'Final per-image price preview', notConfigured: 'Not configured' }, + mediaPricing: { + title: 'Image / Video Generation Pricing', + description: + 'Configure Grok image and video generation access plus base media prices. Leave empty to use default prices.', + allowImageGeneration: 'Allow image and video generation for this group', + independentMultiplier: 'Use independent media multiplier', + imageMultiplier: 'Media multiplier', + modeHint: + 'By default, Grok media billing uses media price × current effective group multiplier. Independent mode uses media price × media multiplier. One video generation is billed as one media unit.', + finalPricePreview: 'Final per-media-unit price preview', + notConfigured: 'Not configured' + }, + videoPricing: { + title: 'Video Generation Pricing', + description: 'Configure Grok video generation base prices. Leave empty to use default video prices.', + independentMultiplier: 'Use independent video multiplier', + videoMultiplier: 'Video multiplier', + modeHint: + 'By default, video billing uses video price × current effective group multiplier. Independent mode uses video price × video multiplier.', + finalPricePreview: 'Final per-video price preview', + notConfigured: 'Not configured' + }, peakRate: { enable: 'Enable peak rate multiplier', peakStart: 'Peak start', diff --git a/frontend/src/i18n/locales/en/admin/resources.ts b/frontend/src/i18n/locales/en/admin/resources.ts index 30b05e5e6b..c6c42d4494 100644 --- a/frontend/src/i18n/locales/en/admin/resources.ts +++ b/frontend/src/i18n/locales/en/admin/resources.ts @@ -477,6 +477,7 @@ export default { billingModeToken: 'Token', billingModePerRequest: 'Per Request', billingModeImage: 'Image', + billingModeVideo: 'Video', allBillingModes: 'All Billing Modes', ipAddress: 'IP', clickToViewBalance: 'Click to view balance history', diff --git a/frontend/src/i18n/locales/en/dashboard.ts b/frontend/src/i18n/locales/en/dashboard.ts index 046179cd94..5d960103ed 100644 --- a/frontend/src/i18n/locales/en/dashboard.ts +++ b/frontend/src/i18n/locales/en/dashboard.ts @@ -478,6 +478,7 @@ export default { billingModeToken: 'Per Token', billingModePerRequest: 'Per Request', billingModeImage: 'Per Image', + billingModeVideo: 'Per Video', inputPrice: 'Input', outputPrice: 'Output', cacheWritePrice: 'Cache Write', diff --git a/frontend/src/i18n/locales/zh/admin/channels.ts b/frontend/src/i18n/locales/zh/admin/channels.ts index 036f3f77e0..93890e25b0 100644 --- a/frontend/src/i18n/locales/zh/admin/channels.ts +++ b/frontend/src/i18n/locales/zh/admin/channels.ts @@ -26,6 +26,7 @@ export default { billingModeToken: '按 Token', billingModePerRequest: '按次', billingModeImage: '按图片', + billingModeVideo: '按视频', inputPrice: '输入', outputPrice: '输出', cacheWritePrice: '缓存写入', diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts index c15cc52990..b56fcde5d0 100644 --- a/frontend/src/i18n/locales/zh/admin/overview.ts +++ b/frontend/src/i18n/locales/zh/admin/overview.ts @@ -919,6 +919,27 @@ export default { finalPricePreview: '最终单张价格预览', notConfigured: '未配置' }, + mediaPricing: { + title: '图片/视频生成计费', + description: '配置 Grok 图片和视频生成能力及媒体基础单价,留空则使用默认价格', + allowImageGeneration: '允许当前分组生图和视频生成', + independentMultiplier: '媒体倍率独立', + imageMultiplier: '媒体独立倍率', + modeHint: + '默认关闭独立倍率时,Grok 媒体费用 = 媒体价格 × 当前分组有效倍率;开启独立倍率后,Grok 媒体费用 = 媒体价格 × 媒体独立倍率。一次视频生成按 1 个媒体单位计费。', + finalPricePreview: '最终单次媒体价格预览', + notConfigured: '未配置' + }, + videoPricing: { + title: '视频生成计费', + description: '配置 Grok 视频生成基础单价,留空则使用默认视频价格', + independentMultiplier: '视频倍率独立', + videoMultiplier: '视频独立倍率', + modeHint: + '默认关闭独立倍率时,视频费用 = 视频价格 × 当前分组有效倍率;开启独立倍率后,视频费用 = 视频价格 × 视频独立倍率。', + finalPricePreview: '最终单次视频价格预览', + notConfigured: '未配置' + }, peakRate: { enable: '启用高峰倍率', peakStart: '高峰开始', diff --git a/frontend/src/i18n/locales/zh/admin/resources.ts b/frontend/src/i18n/locales/zh/admin/resources.ts index ec4753cf4b..8398130d84 100644 --- a/frontend/src/i18n/locales/zh/admin/resources.ts +++ b/frontend/src/i18n/locales/zh/admin/resources.ts @@ -538,6 +538,7 @@ export default { billingModeToken: '按量', billingModePerRequest: '按次', billingModeImage: '按次(图片)', + billingModeVideo: '按次(视频)', allBillingModes: '全部计费模式', ipAddress: 'IP', clickToViewBalance: '点击查看充值记录', diff --git a/frontend/src/i18n/locales/zh/dashboard.ts b/frontend/src/i18n/locales/zh/dashboard.ts index 104e111f3a..d65c1042cf 100644 --- a/frontend/src/i18n/locales/zh/dashboard.ts +++ b/frontend/src/i18n/locales/zh/dashboard.ts @@ -483,6 +483,7 @@ export default { billingModeToken: '按 Token', billingModePerRequest: '按次', billingModeImage: '按图片', + billingModeVideo: '按视频', inputPrice: '输入', outputPrice: '输出', cacheWritePrice: '缓存写入', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 78779c75fc..990ef0575c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -526,6 +526,11 @@ export interface Group { image_price_1k: number | null image_price_2k: number | null image_price_4k: number | null + video_rate_independent: boolean + video_rate_multiplier: number + video_price_480p: number | null + video_price_720p: number | null + video_price_1080p: number | null // 高峰时段倍率配置 peak_rate_enabled: boolean peak_start: string @@ -654,6 +659,11 @@ export interface CreateGroupRequest { image_price_1k?: number | null image_price_2k?: number | null image_price_4k?: number | null + video_rate_independent?: boolean + video_rate_multiplier?: number + video_price_480p?: number | null + video_price_720p?: number | null + video_price_1080p?: number | null peak_rate_enabled?: boolean peak_start?: string peak_end?: string @@ -696,6 +706,11 @@ export interface UpdateGroupRequest { image_price_1k?: number | null image_price_2k?: number | null image_price_4k?: number | null + video_rate_independent?: boolean + video_rate_multiplier?: number + video_price_480p?: number | null + video_price_720p?: number | null + video_price_1080p?: number | null peak_rate_enabled?: boolean peak_start?: string peak_end?: string diff --git a/frontend/src/utils/billingMode.ts b/frontend/src/utils/billingMode.ts index 0db770f758..1ba03ed377 100644 --- a/frontend/src/utils/billingMode.ts +++ b/frontend/src/utils/billingMode.ts @@ -1,11 +1,13 @@ export const BILLING_MODE_TOKEN = 'token' export const BILLING_MODE_PER_REQUEST = 'per_request' export const BILLING_MODE_IMAGE = 'image' +export const BILLING_MODE_VIDEO = 'video' export function getBillingModeLabel(mode: string | null | undefined, t: (key: string) => string): string { switch (mode) { case BILLING_MODE_PER_REQUEST: return t('admin.usage.billingModePerRequest') case BILLING_MODE_IMAGE: return t('admin.usage.billingModeImage') + case BILLING_MODE_VIDEO: return t('admin.usage.billingModeVideo') default: return t('admin.usage.billingModeToken') } } @@ -14,6 +16,7 @@ export function getBillingModeBadgeClass(mode: string | null | undefined): strin switch (mode) { case BILLING_MODE_PER_REQUEST: return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300' case BILLING_MODE_IMAGE: return 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300' + case BILLING_MODE_VIDEO: return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300' default: return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300' } } @@ -25,7 +28,7 @@ interface ImageBillingRow { } export function isImageUsage(row: Pick | null | undefined): boolean { - return (row?.image_count ?? 0) > 0 && row?.billing_mode !== BILLING_MODE_TOKEN + return (row?.image_count ?? 0) > 0 && row?.billing_mode !== BILLING_MODE_TOKEN && row?.billing_mode !== BILLING_MODE_VIDEO } export function getDisplayBillingMode(row: Pick | null | undefined): string | null | undefined { diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 404ca0dc64..ef75e6ab97 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -825,10 +825,10 @@

- {{ t("admin.groups.imagePricing.description") }} + {{ t(imagePricingI18nKey(createForm.platform, "description")) }}

@@ -884,7 +884,7 @@ step="0.001" min="0" class="input" - placeholder="0.201" + :placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_2k')" />
@@ -895,16 +895,16 @@ step="0.001" min="0" class="input" - placeholder="0.268" + :placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_4k')" />

- {{ t("admin.groups.imagePricing.modeHint") }} + {{ t(imagePricingI18nKey(createForm.platform, "modeHint")) }}

- {{ t("admin.groups.imagePricing.finalPricePreview") }} + {{ t(imagePricingI18nKey(createForm.platform, "finalPricePreview")) }}
+ +
+ +

+ {{ t(videoPricingI18nKey("description")) }} +

+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+

+ {{ t(videoPricingI18nKey("modeHint")) }} +

+
+
+ {{ t(videoPricingI18nKey("finalPricePreview")) }} +
+
+
+ {{ item.label }}: {{ item.value }} +
+
+
+
+
@@ -2212,10 +2304,10 @@

- {{ t("admin.groups.imagePricing.description") }} + {{ t(imagePricingI18nKey(editForm.platform, "description")) }}

@@ -2271,7 +2363,7 @@ step="0.001" min="0" class="input" - placeholder="0.201" + :placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_2k')" />
@@ -2282,16 +2374,16 @@ step="0.001" min="0" class="input" - placeholder="0.268" + :placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_4k')" />

- {{ t("admin.groups.imagePricing.modeHint") }} + {{ t(imagePricingI18nKey(editForm.platform, "modeHint")) }}

- {{ t("admin.groups.imagePricing.finalPricePreview") }} + {{ t(imagePricingI18nKey(editForm.platform, "finalPricePreview")) }}
+ +
+ +

+ {{ t(videoPricingI18nKey("description")) }} +

+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+

+ {{ t(videoPricingI18nKey("modeHint")) }} +

+
+
+ {{ t(videoPricingI18nKey("finalPricePreview")) }} +
+
+
+ {{ item.label }}: {{ item.value }} +
+
+
+
+
@@ -3342,7 +3526,16 @@ import { } from "./groupsModelsList"; import { createModelsListCandidatesTracker } from "./groupsModelsListCandidates"; import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModelScopes"; -import { supportsImagePricingPlatform } from "./groupsImagePricing"; +import { + getDefaultImagePreviewPrice, + getDefaultVideoPreviewPrice, + getImagePricePlaceholder, + getVideoPricePlaceholder, + imagePricingI18nKey, + supportsImagePricingPlatform, + supportsVideoPricingPlatform, + videoPricingI18nKey, +} from "./groupsImagePricing"; const { t } = useI18n(); const appStore = useAppStore(); @@ -3689,6 +3882,12 @@ const createForm = reactive({ image_price_1k: null as number | null, image_price_2k: null as number | null, image_price_4k: null as number | null, + // 视频生成计费配置(仅 Grok 平台) + video_rate_independent: false, + video_rate_multiplier: 1, + video_price_480p: null as number | null, + video_price_720p: null as number | null, + video_price_1080p: null as number | null, // 高峰时段倍率配置 peak_rate_enabled: false, peak_start: "", @@ -4028,6 +4227,12 @@ const editForm = reactive({ image_price_1k: null as number | null, image_price_2k: null as number | null, image_price_4k: null as number | null, + // 视频生成计费配置(仅 Grok 平台) + video_rate_independent: false, + video_rate_multiplier: 1, + video_price_480p: null as number | null, + video_price_720p: null as number | null, + video_price_1080p: null as number | null, // 高峰时段倍率配置 peak_rate_enabled: false, peak_start: "", @@ -4077,12 +4282,28 @@ type ImagePricingFormState = { peak_rate_multiplier: number; }; +type VideoPricingFormState = { + platform: GroupPlatform; + rate_multiplier: number; + video_rate_independent: boolean; + video_rate_multiplier: number; + video_price_480p: number | string | null; + video_price_720p: number | string | null; + video_price_1080p: number | string | null; +}; + const imagePricingTiers = [ { key: "image_price_1k", label: "1K" }, { key: "image_price_2k", label: "2K" }, { key: "image_price_4k", label: "4K" }, ] as const; +const videoPricingTiers = [ + { key: "video_price_480p", label: "480p" }, + { key: "video_price_720p", label: "720p" }, + { key: "video_price_1080p", label: "1080p" }, +] as const; + const normalizePreviewNumber = (value: number | string | null | undefined, fallback = 0) => { if (value === null || value === undefined || value === "") { return fallback; @@ -4091,6 +4312,14 @@ const normalizePreviewNumber = (value: number | string | null | undefined, fallb return Number.isFinite(parsed) ? parsed : fallback; }; +const parsePreviewPrice = (value: number | string | null | undefined) => { + if (value === null || value === undefined || value === "") { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : null; +}; + const formatImagePricePreview = (value: number | string | null | undefined) => { if (value === null || value === undefined || value === "") { return t("admin.groups.imagePricing.notConfigured"); @@ -4102,28 +4331,64 @@ const formatImagePricePreview = (value: number | string | null | undefined) => { return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`; }; +const formatVideoPricePreview = (value: number | string | null | undefined) => { + if (value === null || value === undefined || value === "") { + return t("admin.groups.videoPricing.notConfigured"); + } + const price = Number(value); + if (!Number.isFinite(price) || price < 0) { + return t("admin.groups.videoPricing.notConfigured"); + } + return `$${price.toFixed(6).replace(/0+$/, "").replace(/\.$/, "")}`; +}; + const buildImageFinalPricePreview = (form: ImagePricingFormState) => { const imageMultiplier = form.image_rate_independent ? normalizePreviewNumber(form.image_rate_multiplier, 1) : normalizePreviewNumber(form.rate_multiplier, 1); const multiplier = imageMultiplier; return imagePricingTiers.map((tier) => { - const basePrice = normalizePreviewNumber(form[tier.key]); + const basePrice = + parsePreviewPrice(form[tier.key]) ?? + getDefaultImagePreviewPrice(form.platform, tier.key); return { label: tier.label, - value: basePrice > 0 + value: basePrice !== null ? formatImagePricePreview(basePrice * multiplier) : t("admin.groups.imagePricing.notConfigured"), }; }); }; +const buildVideoFinalPricePreview = (form: VideoPricingFormState) => { + const multiplier = form.video_rate_independent + ? normalizePreviewNumber(form.video_rate_multiplier, 1) + : normalizePreviewNumber(form.rate_multiplier, 1); + return videoPricingTiers.map((tier) => { + const basePrice = + parsePreviewPrice(form[tier.key]) ?? + getDefaultVideoPreviewPrice(form.platform, tier.key); + return { + label: tier.label, + value: basePrice !== null + ? formatVideoPricePreview(basePrice * multiplier) + : t("admin.groups.videoPricing.notConfigured"), + }; + }); +}; + const createImageFinalPricePreview = computed(() => buildImageFinalPricePreview(createForm), ); const editImageFinalPricePreview = computed(() => buildImageFinalPricePreview(editForm), ); +const createVideoFinalPricePreview = computed(() => + buildVideoFinalPricePreview(createForm), +); +const editVideoFinalPricePreview = computed(() => + buildVideoFinalPricePreview(editForm), +); const resetDisabledBatchImagePricing = ( form: Pick< @@ -4345,6 +4610,11 @@ const closeCreateModal = () => { createForm.image_price_1k = null; createForm.image_price_2k = null; createForm.image_price_4k = null; + createForm.video_rate_independent = false; + createForm.video_rate_multiplier = 1; + createForm.video_price_480p = null; + createForm.video_price_720p = null; + createForm.video_price_1080p = null; createForm.peak_rate_enabled = false; createForm.peak_start = ""; createForm.peak_end = ""; @@ -4445,6 +4715,9 @@ const handleCreateGroup = async () => { requestData.batch_image_hold_multiplier = normalizeRateMultiplier( requestData.batch_image_hold_multiplier, ); + requestData.video_rate_multiplier = normalizeRateMultiplier( + requestData.video_rate_multiplier, + ); requestData.peak_rate_enabled = createForm.peak_rate_enabled; requestData.peak_start = createForm.peak_start; requestData.peak_end = createForm.peak_end; @@ -4493,6 +4766,11 @@ const handleEdit = async (group: AdminGroup) => { editForm.image_price_1k = group.image_price_1k; editForm.image_price_2k = group.image_price_2k; editForm.image_price_4k = group.image_price_4k; + editForm.video_rate_independent = group.video_rate_independent ?? false; + editForm.video_rate_multiplier = group.video_rate_multiplier ?? 1; + editForm.video_price_480p = group.video_price_480p; + editForm.video_price_720p = group.video_price_720p; + editForm.video_price_1080p = group.video_price_1080p; editForm.peak_rate_enabled = group.peak_rate_enabled ?? false; editForm.peak_start = group.peak_start ?? ""; editForm.peak_end = group.peak_end ?? ""; @@ -4545,6 +4823,11 @@ const closeEditModal = () => { editForm.peak_start = ""; editForm.peak_end = ""; editForm.peak_rate_multiplier = 1.0; + editForm.video_rate_independent = false; + editForm.video_rate_multiplier = 1; + editForm.video_price_480p = null; + editForm.video_price_720p = null; + editForm.video_price_1080p = null; resetMessagesDispatchFormState(editForm); resetModelsListState(editModelsListState); }; @@ -4610,6 +4893,9 @@ const handleUpdateGroup = async () => { payload.batch_image_hold_multiplier = normalizeRateMultiplier( payload.batch_image_hold_multiplier, ); + payload.video_rate_multiplier = normalizeRateMultiplier( + payload.video_rate_multiplier, + ); payload.peak_rate_enabled = editForm.peak_rate_enabled; payload.peak_start = editForm.peak_start; payload.peak_end = editForm.peak_end; diff --git a/frontend/src/views/admin/__tests__/groupsImagePricing.spec.ts b/frontend/src/views/admin/__tests__/groupsImagePricing.spec.ts index 5f6ebce2fa..e83e22319d 100644 --- a/frontend/src/views/admin/__tests__/groupsImagePricing.spec.ts +++ b/frontend/src/views/admin/__tests__/groupsImagePricing.spec.ts @@ -1,17 +1,50 @@ import { describe, expect, it } from "vitest"; import { + getDefaultImagePreviewPrice, + getDefaultVideoPreviewPrice, + getImagePricePlaceholder, + getVideoPricePlaceholder, imagePricingPlatforms, + imagePricingI18nKey, supportsImagePricingPlatform, + supportsVideoPricingPlatform, + videoPricingI18nKey, } from "../groupsImagePricing"; describe("groups image pricing platform support", () => { - it("includes Grok media groups", () => { + it("includes Grok image groups", () => { expect(supportsImagePricingPlatform("grok")).toBe(true); expect(imagePricingPlatforms.has("grok")).toBe(true); }); + it("enables video pricing controls for Grok only", () => { + expect(supportsVideoPricingPlatform("grok")).toBe(true); + expect(supportsVideoPricingPlatform("openai")).toBe(false); + }); + it("keeps non-media group platforms out of the image pricing controls", () => { expect(supportsImagePricingPlatform("anthropic")).toBe(false); }); + + it("keeps image and video pricing copy separate", () => { + expect(imagePricingI18nKey("grok", "title")).toBe( + "admin.groups.imagePricing.title", + ); + expect(videoPricingI18nKey("title")).toBe("admin.groups.videoPricing.title"); + }); + + it("uses Grok media defaults instead of generic image fallback placeholders", () => { + expect(getImagePricePlaceholder("grok", "image_price_1k")).toBe("0.02"); + expect(getImagePricePlaceholder("grok", "image_price_2k")).toBe("0.02"); + expect(getVideoPricePlaceholder("grok", "video_price_480p")).toBe("0.08"); + expect(getVideoPricePlaceholder("grok", "video_price_720p")).toBe("0.14"); + expect(getVideoPricePlaceholder("grok", "video_price_1080p")).toBe("0.25"); + }); + + it("keeps non-Grok image placeholders on the generic image card", () => { + expect(getImagePricePlaceholder("openai", "image_price_1k")).toBe("0.134"); + expect(getDefaultImagePreviewPrice("openai", "image_price_2k")).toBe(0.201); + expect(getDefaultVideoPreviewPrice("openai", "video_price_480p")).toBeNull(); + }); }); diff --git a/frontend/src/views/admin/groupsImagePricing.ts b/frontend/src/views/admin/groupsImagePricing.ts index 1a2c5170ce..d88c479995 100644 --- a/frontend/src/views/admin/groupsImagePricing.ts +++ b/frontend/src/views/admin/groupsImagePricing.ts @@ -7,3 +7,85 @@ export const imagePricingPlatforms = new Set([ export const supportsImagePricingPlatform = (platform: string): boolean => imagePricingPlatforms.has(platform); + +export const supportsVideoPricingPlatform = (platform: string): boolean => + platform === "grok"; + +export const imagePricingI18nKey = (_platform: string, key: string): string => + `admin.groups.imagePricing.${key}`; + +export const videoPricingI18nKey = (key: string): string => + `admin.groups.videoPricing.${key}`; + +type ImagePricingTierKey = "image_price_1k" | "image_price_2k" | "image_price_4k"; +type VideoPricingTierKey = + | "video_price_480p" + | "video_price_720p" + | "video_price_1080p"; + +const defaultImagePricePlaceholders: Record< + string, + Record +> = { + default: { + image_price_1k: "0.134", + image_price_2k: "0.201", + image_price_4k: "0.268", + }, + grok: { + image_price_1k: "0.02", + image_price_2k: "0.02", + image_price_4k: "0.02", + }, +}; + +const defaultVideoPricePlaceholders: Record< + string, + Record +> = { + grok: { + video_price_480p: "0.08", + video_price_720p: "0.14", + video_price_1080p: "0.25", + }, +}; + +export const getImagePricePlaceholder = ( + platform: string, + tier: ImagePricingTierKey, +): string => { + const card = defaultImagePricePlaceholders[platform] ?? defaultImagePricePlaceholders.default; + return card[tier]; +}; + +export const getVideoPricePlaceholder = ( + platform: string, + tier: VideoPricingTierKey, +): string => { + const card = defaultVideoPricePlaceholders[platform]; + return card?.[tier] ?? ""; +}; + +export const getDefaultImagePreviewPrice = ( + platform: string, + tier: ImagePricingTierKey, +): number | null => { + const placeholder = getImagePricePlaceholder(platform, tier); + if (placeholder === "") { + return null; + } + const value = Number(placeholder); + return Number.isFinite(value) ? value : null; +}; + +export const getDefaultVideoPreviewPrice = ( + platform: string, + tier: VideoPricingTierKey, +): number | null => { + const placeholder = getVideoPricePlaceholder(platform, tier); + if (placeholder === "") { + return null; + } + const value = Number(placeholder); + return Number.isFinite(value) ? value : null; +}; diff --git a/frontend/src/views/user/UsageView.vue b/frontend/src/views/user/UsageView.vue index eb2b124839..7ccc348471 100644 --- a/frontend/src/views/user/UsageView.vue +++ b/frontend/src/views/user/UsageView.vue @@ -389,6 +389,7 @@ const billingModeOptions = computed(() => [ { value: 'token', label: t('admin.usage.billingModeToken') }, { value: 'per_request', label: t('admin.usage.billingModePerRequest') }, { value: 'image', label: t('admin.usage.billingModeImage') }, + { value: 'video', label: t('admin.usage.billingModeVideo') }, ]) const apiKeys = ref([])