Merge pull request #3775 from heathermhuang/codex/grok-media-pricing-labels

fix: add Grok video pricing controls
This commit is contained in:
Wesley Liddick
2026-07-09 15:03:38 +08:00
committed by GitHub
55 changed files with 3073 additions and 87 deletions
+66 -2
View File
@@ -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(", ")
+44
View File
@@ -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()
+225
View File
@@ -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))
+444
View File
@@ -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) {
+304
View File
@@ -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)
}
+6 -1
View File
@@ -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]},
},
},
}
+463 -1
View File
@@ -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
+20 -12
View File
@@ -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()
+19
View File
@@ -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").
@@ -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,
+5
View File
@@ -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,
+5
View File
@@ -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"`
+1 -1
View File
@@ -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
}
@@ -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"
@@ -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,
+25
View File
@@ -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()
}
// 处理 FallbackGroupIDnil 时清除,否则设置
if groupIn.FallbackGroupID != nil {
@@ -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'",
@@ -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:
@@ -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)
}
@@ -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,
+33
View File
@@ -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 {
+10
View File
@@ -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 平台使用)
@@ -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,
@@ -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"`
@@ -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,
+145 -1
View File
@@ -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
}
}
@@ -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()
+10
View File
@@ -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"
@@ -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)
}
+12 -3
View File
@@ -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
}
+20
View File
@@ -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 {
@@ -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
}
@@ -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
}
@@ -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) {
@@ -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{}
@@ -242,6 +242,8 @@ type OpenAIForwardResult struct {
ImageOutputSizes []string
ImageSizeSource string
ImageSizeBreakdown map[string]int
VideoCount int
VideoResolution string
wsReplayInput []json.RawMessage
wsReplayInputExists bool
+110 -11
View File
@@ -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 {
@@ -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
}
}
@@ -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 平台使用';
@@ -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;
@@ -294,7 +294,8 @@ const billingModeOptions = ref<SelectOption[]>([
{ 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')
@@ -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',
@@ -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',
@@ -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',
@@ -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',
@@ -26,6 +26,7 @@ export default {
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
billingModeVideo: '按视频',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
@@ -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: '高峰开始',
@@ -538,6 +538,7 @@ export default {
billingModeToken: '按量',
billingModePerRequest: '按次',
billingModeImage: '按次(图片)',
billingModeVideo: '按次(视频)',
allBillingModes: '全部计费模式',
ipAddress: 'IP',
clickToViewBalance: '点击查看充值记录',
@@ -483,6 +483,7 @@ export default {
billingModeToken: '按 Token',
billingModePerRequest: '按次',
billingModeImage: '按图片',
billingModeVideo: '按视频',
inputPrice: '输入',
outputPrice: '输出',
cacheWritePrice: '缓存写入',
+15
View File
@@ -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
+4 -1
View File
@@ -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<ImageBillingRow, 'image_count' | 'billing_mode'> | 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<ImageBillingRow, 'billing_mode' | 'image_count'> | null | undefined): string | null | undefined {
+309 -23
View File
@@ -825,10 +825,10 @@
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.groups.imagePricing.title") }}
{{ t(imagePricingI18nKey(createForm.platform, "title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t("admin.groups.imagePricing.description") }}
{{ t(imagePricingI18nKey(createForm.platform, "description")) }}
</p>
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
@@ -837,7 +837,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
{{ t(imagePricingI18nKey(createForm.platform, "allowImageGeneration")) }}
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
@@ -845,7 +845,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.independentMultiplier") }}
{{ t(imagePricingI18nKey(createForm.platform, "independentMultiplier")) }}
</label>
</div>
<div
@@ -853,7 +853,7 @@
class="mb-4"
>
<label class="input-label">{{
t("admin.groups.imagePricing.imageMultiplier")
t(imagePricingI18nKey(createForm.platform, "imageMultiplier"))
}}</label>
<input
v-model.number="createForm.image_rate_multiplier"
@@ -873,7 +873,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.134"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_1k')"
/>
</div>
<div>
@@ -884,7 +884,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_2k')"
/>
</div>
<div>
@@ -895,16 +895,16 @@
step="0.001"
min="0"
class="input"
placeholder="0.268"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_4k')"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.groups.imagePricing.modeHint") }}
{{ t(imagePricingI18nKey(createForm.platform, "modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t("admin.groups.imagePricing.finalPricePreview") }}
{{ t(imagePricingI18nKey(createForm.platform, "finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
@@ -969,6 +969,98 @@
</p>
</div>
<!-- 视频生成计费配置 Grok 平台 -->
<div
v-if="supportsVideoPricingPlatform(createForm.platform)"
class="border-t pt-4"
>
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t(videoPricingI18nKey("title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t(videoPricingI18nKey("description")) }}
</p>
<div class="mb-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.video_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t(videoPricingI18nKey("independentMultiplier")) }}
</label>
</div>
<div
v-if="createForm.video_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t(videoPricingI18nKey("videoMultiplier"))
}}</label>
<input
v-model.number="createForm.video_rate_multiplier"
type="number"
step="0.0001"
min="0"
class="input"
placeholder="1"
/>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="input-label">480p ($)</label>
<input
v-model.number="createForm.video_price_480p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_480p')"
/>
</div>
<div>
<label class="input-label">720p ($)</label>
<input
v-model.number="createForm.video_price_720p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_720p')"
/>
</div>
<div>
<label class="input-label">1080p ($)</label>
<input
v-model.number="createForm.video_price_1080p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_1080p')"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t(videoPricingI18nKey("modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t(videoPricingI18nKey("finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in createVideoFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 高峰时段倍率配置仅订阅类型分组 -->
<div v-if="createForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -2212,10 +2304,10 @@
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t("admin.groups.imagePricing.title") }}
{{ t(imagePricingI18nKey(editForm.platform, "title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t("admin.groups.imagePricing.description") }}
{{ t(imagePricingI18nKey(editForm.platform, "description")) }}
</p>
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
@@ -2224,7 +2316,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.allowImageGeneration") }}
{{ t(imagePricingI18nKey(editForm.platform, "allowImageGeneration")) }}
</label>
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
@@ -2232,7 +2324,7 @@
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t("admin.groups.imagePricing.independentMultiplier") }}
{{ t(imagePricingI18nKey(editForm.platform, "independentMultiplier")) }}
</label>
</div>
<div
@@ -2240,7 +2332,7 @@
class="mb-4"
>
<label class="input-label">{{
t("admin.groups.imagePricing.imageMultiplier")
t(imagePricingI18nKey(editForm.platform, "imageMultiplier"))
}}</label>
<input
v-model.number="editForm.image_rate_multiplier"
@@ -2260,7 +2352,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.134"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_1k')"
/>
</div>
<div>
@@ -2271,7 +2363,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_2k')"
/>
</div>
<div>
@@ -2282,16 +2374,16 @@
step="0.001"
min="0"
class="input"
placeholder="0.268"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_4k')"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t("admin.groups.imagePricing.modeHint") }}
{{ t(imagePricingI18nKey(editForm.platform, "modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t("admin.groups.imagePricing.finalPricePreview") }}
{{ t(imagePricingI18nKey(editForm.platform, "finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
@@ -2356,6 +2448,98 @@
</p>
</div>
<!-- 视频生成计费配置 Grok 平台 -->
<div
v-if="supportsVideoPricingPlatform(editForm.platform)"
class="border-t pt-4"
>
<label
class="block mb-2 font-medium text-gray-700 dark:text-gray-300"
>
{{ t(videoPricingI18nKey("title")) }}
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{{ t(videoPricingI18nKey("description")) }}
</p>
<div class="mb-4">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.video_rate_independent"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
{{ t(videoPricingI18nKey("independentMultiplier")) }}
</label>
</div>
<div
v-if="editForm.video_rate_independent"
class="mb-4"
>
<label class="input-label">{{
t(videoPricingI18nKey("videoMultiplier"))
}}</label>
<input
v-model.number="editForm.video_rate_multiplier"
type="number"
step="0.0001"
min="0"
class="input"
placeholder="1"
/>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="input-label">480p ($)</label>
<input
v-model.number="editForm.video_price_480p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_480p')"
/>
</div>
<div>
<label class="input-label">720p ($)</label>
<input
v-model.number="editForm.video_price_720p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_720p')"
/>
</div>
<div>
<label class="input-label">1080p ($)</label>
<input
v-model.number="editForm.video_price_1080p"
type="number"
step="0.001"
min="0"
class="input"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_1080p')"
/>
</div>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
{{ t(videoPricingI18nKey("modeHint")) }}
</p>
<div class="mt-2 rounded-lg bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<div class="mb-1 font-medium">
{{ t(videoPricingI18nKey("finalPricePreview")) }}
</div>
<div class="grid grid-cols-3 gap-2">
<div
v-for="item in editVideoFinalPricePreview"
:key="item.label"
>
{{ item.label }}: {{ item.value }}
</div>
</div>
</div>
</div>
<!-- 高峰时段倍率配置仅订阅类型分组 -->
<div v-if="editForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
@@ -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;
@@ -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();
});
});
@@ -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<ImagePricingTierKey, string>
> = {
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<VideoPricingTierKey, string>
> = {
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;
};
+1
View File
@@ -389,6 +389,7 @@ const billingModeOptions = computed<SelectOption[]>(() => [
{ 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<ApiKey[]>([])