mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix: bill Grok video per second and harden video usage logging
Follow-up fixes for the #3775 audit findings: - Bill Grok video generation per second of output, matching the xAI rate card: parse the request duration (1-15s, upstream default 8s) and compute cost as per-second price x duration x count. The built-in rate card values were already xAI per-second prices but were previously charged per video, undercharging up to 15x with a user-controlled duration. - Group video_price_* fields are now documented and surfaced as per-second rates (USD/s); admin UI labels, placeholders and hints updated accordingly. - Persist video_count/video_resolution/video_duration_seconds on usage_logs (migration 172) so video billing is auditable, and exempt any row with video_count > 0 from the image_size check constraint: a video billed via a token-mode channel price produces billing_mode='token' with image_count=1 and no image_size, which the previous constraint rejected, dropping the whole billing transaction. - Only refetch the group in apiKeyWithFreshGroupMediaPricing when the group object actually looks like it is missing media pricing fields (both media multipliers zero and all prices nil, impossible for a normally loaded group), removing a per-usage DB query for groups without overrides. - Frontend: drop the unused admin.groups.mediaPricing locale block, map cleared price inputs to null (create) / -1 (update, cleared via backend normalizePrice) instead of sending "" that failed *float64 unmarshalling, and align video price placeholders with the text-to-video default model (grok-imagine-video 0.05/0.07, 1080p only on 1.5 at 0.25).
This commit is contained in:
@@ -1572,6 +1572,9 @@ var (
|
||||
{Name: "image_output_size", Type: field.TypeString, Nullable: true, Size: 32},
|
||||
{Name: "image_size_source", Type: field.TypeString, Nullable: true, Size: 16},
|
||||
{Name: "image_size_breakdown", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
|
||||
{Name: "video_count", Type: field.TypeInt, Default: 0},
|
||||
{Name: "video_resolution", Type: field.TypeString, Nullable: true, Size: 10},
|
||||
{Name: "video_duration_seconds", Type: field.TypeInt, Nullable: true},
|
||||
{Name: "cache_ttl_overridden", Type: field.TypeBool, Default: false},
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "api_key_id", Type: field.TypeInt64},
|
||||
@@ -1588,31 +1591,31 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "usage_logs_api_keys_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[37]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_accounts_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[38]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_groups_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_users_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -1621,32 +1624,32 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[37]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_account_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[38]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_subscription_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[36]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[39]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_model",
|
||||
@@ -1666,17 +1669,17 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[40], UsageLogsColumns[36]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43], UsageLogsColumns[39]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[37], UsageLogsColumns[36]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[40], UsageLogsColumns[39]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[39], UsageLogsColumns[36]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42], UsageLogsColumns[39]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+268
-1
@@ -41712,6 +41712,11 @@ type UsageLogMutation struct {
|
||||
image_output_size *string
|
||||
image_size_source *string
|
||||
image_size_breakdown *map[string]int
|
||||
video_count *int
|
||||
addvideo_count *int
|
||||
video_resolution *string
|
||||
video_duration_seconds *int
|
||||
addvideo_duration_seconds *int
|
||||
cache_ttl_overridden *bool
|
||||
created_at *time.Time
|
||||
clearedFields map[string]struct{}
|
||||
@@ -43850,6 +43855,181 @@ func (m *UsageLogMutation) ResetImageSizeBreakdown() {
|
||||
delete(m.clearedFields, usagelog.FieldImageSizeBreakdown)
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (m *UsageLogMutation) SetVideoCount(i int) {
|
||||
m.video_count = &i
|
||||
m.addvideo_count = nil
|
||||
}
|
||||
|
||||
// VideoCount returns the value of the "video_count" field in the mutation.
|
||||
func (m *UsageLogMutation) VideoCount() (r int, exists bool) {
|
||||
v := m.video_count
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldVideoCount returns the old "video_count" field's value of the UsageLog entity.
|
||||
// If the UsageLog 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 *UsageLogMutation) OldVideoCount(ctx context.Context) (v int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldVideoCount is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldVideoCount requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldVideoCount: %w", err)
|
||||
}
|
||||
return oldValue.VideoCount, nil
|
||||
}
|
||||
|
||||
// AddVideoCount adds i to the "video_count" field.
|
||||
func (m *UsageLogMutation) AddVideoCount(i int) {
|
||||
if m.addvideo_count != nil {
|
||||
*m.addvideo_count += i
|
||||
} else {
|
||||
m.addvideo_count = &i
|
||||
}
|
||||
}
|
||||
|
||||
// AddedVideoCount returns the value that was added to the "video_count" field in this mutation.
|
||||
func (m *UsageLogMutation) AddedVideoCount() (r int, exists bool) {
|
||||
v := m.addvideo_count
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ResetVideoCount resets all changes to the "video_count" field.
|
||||
func (m *UsageLogMutation) ResetVideoCount() {
|
||||
m.video_count = nil
|
||||
m.addvideo_count = nil
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (m *UsageLogMutation) SetVideoResolution(s string) {
|
||||
m.video_resolution = &s
|
||||
}
|
||||
|
||||
// VideoResolution returns the value of the "video_resolution" field in the mutation.
|
||||
func (m *UsageLogMutation) VideoResolution() (r string, exists bool) {
|
||||
v := m.video_resolution
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldVideoResolution returns the old "video_resolution" field's value of the UsageLog entity.
|
||||
// If the UsageLog 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 *UsageLogMutation) OldVideoResolution(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldVideoResolution is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldVideoResolution requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldVideoResolution: %w", err)
|
||||
}
|
||||
return oldValue.VideoResolution, nil
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (m *UsageLogMutation) ClearVideoResolution() {
|
||||
m.video_resolution = nil
|
||||
m.clearedFields[usagelog.FieldVideoResolution] = struct{}{}
|
||||
}
|
||||
|
||||
// VideoResolutionCleared returns if the "video_resolution" field was cleared in this mutation.
|
||||
func (m *UsageLogMutation) VideoResolutionCleared() bool {
|
||||
_, ok := m.clearedFields[usagelog.FieldVideoResolution]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetVideoResolution resets all changes to the "video_resolution" field.
|
||||
func (m *UsageLogMutation) ResetVideoResolution() {
|
||||
m.video_resolution = nil
|
||||
delete(m.clearedFields, usagelog.FieldVideoResolution)
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (m *UsageLogMutation) SetVideoDurationSeconds(i int) {
|
||||
m.video_duration_seconds = &i
|
||||
m.addvideo_duration_seconds = nil
|
||||
}
|
||||
|
||||
// VideoDurationSeconds returns the value of the "video_duration_seconds" field in the mutation.
|
||||
func (m *UsageLogMutation) VideoDurationSeconds() (r int, exists bool) {
|
||||
v := m.video_duration_seconds
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldVideoDurationSeconds returns the old "video_duration_seconds" field's value of the UsageLog entity.
|
||||
// If the UsageLog 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 *UsageLogMutation) OldVideoDurationSeconds(ctx context.Context) (v *int, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldVideoDurationSeconds is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldVideoDurationSeconds requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldVideoDurationSeconds: %w", err)
|
||||
}
|
||||
return oldValue.VideoDurationSeconds, nil
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds i to the "video_duration_seconds" field.
|
||||
func (m *UsageLogMutation) AddVideoDurationSeconds(i int) {
|
||||
if m.addvideo_duration_seconds != nil {
|
||||
*m.addvideo_duration_seconds += i
|
||||
} else {
|
||||
m.addvideo_duration_seconds = &i
|
||||
}
|
||||
}
|
||||
|
||||
// AddedVideoDurationSeconds returns the value that was added to the "video_duration_seconds" field in this mutation.
|
||||
func (m *UsageLogMutation) AddedVideoDurationSeconds() (r int, exists bool) {
|
||||
v := m.addvideo_duration_seconds
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (m *UsageLogMutation) ClearVideoDurationSeconds() {
|
||||
m.video_duration_seconds = nil
|
||||
m.addvideo_duration_seconds = nil
|
||||
m.clearedFields[usagelog.FieldVideoDurationSeconds] = struct{}{}
|
||||
}
|
||||
|
||||
// VideoDurationSecondsCleared returns if the "video_duration_seconds" field was cleared in this mutation.
|
||||
func (m *UsageLogMutation) VideoDurationSecondsCleared() bool {
|
||||
_, ok := m.clearedFields[usagelog.FieldVideoDurationSeconds]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetVideoDurationSeconds resets all changes to the "video_duration_seconds" field.
|
||||
func (m *UsageLogMutation) ResetVideoDurationSeconds() {
|
||||
m.video_duration_seconds = nil
|
||||
m.addvideo_duration_seconds = nil
|
||||
delete(m.clearedFields, usagelog.FieldVideoDurationSeconds)
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (m *UsageLogMutation) SetCacheTTLOverridden(b bool) {
|
||||
m.cache_ttl_overridden = &b
|
||||
@@ -44091,7 +44271,7 @@ func (m *UsageLogMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UsageLogMutation) Fields() []string {
|
||||
fields := make([]string, 0, 41)
|
||||
fields := make([]string, 0, 44)
|
||||
if m.user != nil {
|
||||
fields = append(fields, usagelog.FieldUserID)
|
||||
}
|
||||
@@ -44209,6 +44389,15 @@ func (m *UsageLogMutation) Fields() []string {
|
||||
if m.image_size_breakdown != nil {
|
||||
fields = append(fields, usagelog.FieldImageSizeBreakdown)
|
||||
}
|
||||
if m.video_count != nil {
|
||||
fields = append(fields, usagelog.FieldVideoCount)
|
||||
}
|
||||
if m.video_resolution != nil {
|
||||
fields = append(fields, usagelog.FieldVideoResolution)
|
||||
}
|
||||
if m.video_duration_seconds != nil {
|
||||
fields = append(fields, usagelog.FieldVideoDurationSeconds)
|
||||
}
|
||||
if m.cache_ttl_overridden != nil {
|
||||
fields = append(fields, usagelog.FieldCacheTTLOverridden)
|
||||
}
|
||||
@@ -44301,6 +44490,12 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.ImageSizeSource()
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
return m.ImageSizeBreakdown()
|
||||
case usagelog.FieldVideoCount:
|
||||
return m.VideoCount()
|
||||
case usagelog.FieldVideoResolution:
|
||||
return m.VideoResolution()
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
return m.VideoDurationSeconds()
|
||||
case usagelog.FieldCacheTTLOverridden:
|
||||
return m.CacheTTLOverridden()
|
||||
case usagelog.FieldCreatedAt:
|
||||
@@ -44392,6 +44587,12 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value
|
||||
return m.OldImageSizeSource(ctx)
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
return m.OldImageSizeBreakdown(ctx)
|
||||
case usagelog.FieldVideoCount:
|
||||
return m.OldVideoCount(ctx)
|
||||
case usagelog.FieldVideoResolution:
|
||||
return m.OldVideoResolution(ctx)
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
return m.OldVideoDurationSeconds(ctx)
|
||||
case usagelog.FieldCacheTTLOverridden:
|
||||
return m.OldCacheTTLOverridden(ctx)
|
||||
case usagelog.FieldCreatedAt:
|
||||
@@ -44678,6 +44879,27 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetImageSizeBreakdown(v)
|
||||
return nil
|
||||
case usagelog.FieldVideoCount:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetVideoCount(v)
|
||||
return nil
|
||||
case usagelog.FieldVideoResolution:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetVideoResolution(v)
|
||||
return nil
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetVideoDurationSeconds(v)
|
||||
return nil
|
||||
case usagelog.FieldCacheTTLOverridden:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
@@ -44757,6 +44979,12 @@ func (m *UsageLogMutation) AddedFields() []string {
|
||||
if m.addimage_count != nil {
|
||||
fields = append(fields, usagelog.FieldImageCount)
|
||||
}
|
||||
if m.addvideo_count != nil {
|
||||
fields = append(fields, usagelog.FieldVideoCount)
|
||||
}
|
||||
if m.addvideo_duration_seconds != nil {
|
||||
fields = append(fields, usagelog.FieldVideoDurationSeconds)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -44803,6 +45031,10 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) {
|
||||
return m.AddedFirstTokenMs()
|
||||
case usagelog.FieldImageCount:
|
||||
return m.AddedImageCount()
|
||||
case usagelog.FieldVideoCount:
|
||||
return m.AddedVideoCount()
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
return m.AddedVideoDurationSeconds()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -44945,6 +45177,20 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error {
|
||||
}
|
||||
m.AddImageCount(v)
|
||||
return nil
|
||||
case usagelog.FieldVideoCount:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddVideoCount(v)
|
||||
return nil
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
v, ok := value.(int)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.AddVideoDurationSeconds(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown UsageLog numeric field %s", name)
|
||||
}
|
||||
@@ -45007,6 +45253,12 @@ func (m *UsageLogMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(usagelog.FieldImageSizeBreakdown) {
|
||||
fields = append(fields, usagelog.FieldImageSizeBreakdown)
|
||||
}
|
||||
if m.FieldCleared(usagelog.FieldVideoResolution) {
|
||||
fields = append(fields, usagelog.FieldVideoResolution)
|
||||
}
|
||||
if m.FieldCleared(usagelog.FieldVideoDurationSeconds) {
|
||||
fields = append(fields, usagelog.FieldVideoDurationSeconds)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -45075,6 +45327,12 @@ func (m *UsageLogMutation) ClearField(name string) error {
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
m.ClearImageSizeBreakdown()
|
||||
return nil
|
||||
case usagelog.FieldVideoResolution:
|
||||
m.ClearVideoResolution()
|
||||
return nil
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
m.ClearVideoDurationSeconds()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown UsageLog nullable field %s", name)
|
||||
}
|
||||
@@ -45200,6 +45458,15 @@ func (m *UsageLogMutation) ResetField(name string) error {
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
m.ResetImageSizeBreakdown()
|
||||
return nil
|
||||
case usagelog.FieldVideoCount:
|
||||
m.ResetVideoCount()
|
||||
return nil
|
||||
case usagelog.FieldVideoResolution:
|
||||
m.ResetVideoResolution()
|
||||
return nil
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
m.ResetVideoDurationSeconds()
|
||||
return nil
|
||||
case usagelog.FieldCacheTTLOverridden:
|
||||
m.ResetCacheTTLOverridden()
|
||||
return nil
|
||||
|
||||
@@ -1976,12 +1976,20 @@ func init() {
|
||||
usagelogDescImageSizeSource := usagelogFields[37].Descriptor()
|
||||
// usagelog.ImageSizeSourceValidator is a validator for the "image_size_source" field. It is called by the builders before save.
|
||||
usagelog.ImageSizeSourceValidator = usagelogDescImageSizeSource.Validators[0].(func(string) error)
|
||||
// usagelogDescVideoCount is the schema descriptor for video_count field.
|
||||
usagelogDescVideoCount := usagelogFields[39].Descriptor()
|
||||
// usagelog.DefaultVideoCount holds the default value on creation for the video_count field.
|
||||
usagelog.DefaultVideoCount = usagelogDescVideoCount.Default.(int)
|
||||
// usagelogDescVideoResolution is the schema descriptor for video_resolution field.
|
||||
usagelogDescVideoResolution := usagelogFields[40].Descriptor()
|
||||
// usagelog.VideoResolutionValidator is a validator for the "video_resolution" field. It is called by the builders before save.
|
||||
usagelog.VideoResolutionValidator = usagelogDescVideoResolution.Validators[0].(func(string) error)
|
||||
// usagelogDescCacheTTLOverridden is the schema descriptor for cache_ttl_overridden field.
|
||||
usagelogDescCacheTTLOverridden := usagelogFields[39].Descriptor()
|
||||
usagelogDescCacheTTLOverridden := usagelogFields[42].Descriptor()
|
||||
// usagelog.DefaultCacheTTLOverridden holds the default value on creation for the cache_ttl_overridden field.
|
||||
usagelog.DefaultCacheTTLOverridden = usagelogDescCacheTTLOverridden.Default.(bool)
|
||||
// usagelogDescCreatedAt is the schema descriptor for created_at field.
|
||||
usagelogDescCreatedAt := usagelogFields[40].Descriptor()
|
||||
usagelogDescCreatedAt := usagelogFields[43].Descriptor()
|
||||
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
|
||||
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
|
||||
userMixin := schema.User{}.Mixin()
|
||||
|
||||
@@ -149,6 +149,20 @@ func (UsageLog) Fields() []ent.Field {
|
||||
field.JSON("image_size_breakdown", map[string]int{}).
|
||||
Optional().
|
||||
SchemaType(map[string]string{dialect.Postgres: "jsonb"}),
|
||||
|
||||
// 视频生成字段(Grok 视频按秒计费;billing_mode 走 token/其他模式时这些列仍标记视频用量)
|
||||
field.Int("video_count").
|
||||
Default(0).
|
||||
Comment("视频生成数量;>0 表示本行是视频生成用量"),
|
||||
field.String("video_resolution").
|
||||
MaxLen(10).
|
||||
Optional().
|
||||
Nillable().
|
||||
Comment("计费用视频分辨率 480p/720p/1080p"),
|
||||
field.Int("video_duration_seconds").
|
||||
Optional().
|
||||
Nillable().
|
||||
Comment("提交时请求的视频时长(秒),按秒计费的乘数"),
|
||||
// Cache TTL Override 标记(管理员强制替换了缓存 TTL 计费)
|
||||
field.Bool("cache_ttl_overridden").
|
||||
Default(false),
|
||||
|
||||
+41
-2
@@ -101,6 +101,12 @@ type UsageLog struct {
|
||||
ImageSizeSource *string `json:"image_size_source,omitempty"`
|
||||
// ImageSizeBreakdown holds the value of the "image_size_breakdown" field.
|
||||
ImageSizeBreakdown map[string]int `json:"image_size_breakdown,omitempty"`
|
||||
// 视频生成数量;>0 表示本行是视频生成用量
|
||||
VideoCount int `json:"video_count,omitempty"`
|
||||
// 计费用视频分辨率 480p/720p/1080p
|
||||
VideoResolution *string `json:"video_resolution,omitempty"`
|
||||
// 提交时请求的视频时长(秒),按秒计费的乘数
|
||||
VideoDurationSeconds *int `json:"video_duration_seconds,omitempty"`
|
||||
// CacheTTLOverridden holds the value of the "cache_ttl_overridden" field.
|
||||
CacheTTLOverridden bool `json:"cache_ttl_overridden,omitempty"`
|
||||
// CreatedAt holds the value of the "created_at" field.
|
||||
@@ -194,9 +200,9 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
||||
values[i] = new(sql.NullBool)
|
||||
case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier, usagelog.FieldAccountRateMultiplier:
|
||||
values[i] = new(sql.NullFloat64)
|
||||
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldChannelID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
|
||||
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldChannelID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount, usagelog.FieldVideoCount, usagelog.FieldVideoDurationSeconds:
|
||||
values[i] = new(sql.NullInt64)
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldRequestedModel, usagelog.FieldUpstreamModel, usagelog.FieldModelMappingChain, usagelog.FieldBillingTier, usagelog.FieldBillingMode, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize, usagelog.FieldImageInputSize, usagelog.FieldImageOutputSize, usagelog.FieldImageSizeSource:
|
||||
case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldRequestedModel, usagelog.FieldUpstreamModel, usagelog.FieldModelMappingChain, usagelog.FieldBillingTier, usagelog.FieldBillingMode, usagelog.FieldUserAgent, usagelog.FieldIPAddress, usagelog.FieldImageSize, usagelog.FieldImageInputSize, usagelog.FieldImageOutputSize, usagelog.FieldImageSizeSource, usagelog.FieldVideoResolution:
|
||||
values[i] = new(sql.NullString)
|
||||
case usagelog.FieldCreatedAt:
|
||||
values[i] = new(sql.NullTime)
|
||||
@@ -474,6 +480,26 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field image_size_breakdown: %w", err)
|
||||
}
|
||||
}
|
||||
case usagelog.FieldVideoCount:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field video_count", values[i])
|
||||
} else if value.Valid {
|
||||
_m.VideoCount = int(value.Int64)
|
||||
}
|
||||
case usagelog.FieldVideoResolution:
|
||||
if value, ok := values[i].(*sql.NullString); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field video_resolution", values[i])
|
||||
} else if value.Valid {
|
||||
_m.VideoResolution = new(string)
|
||||
*_m.VideoResolution = value.String
|
||||
}
|
||||
case usagelog.FieldVideoDurationSeconds:
|
||||
if value, ok := values[i].(*sql.NullInt64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field video_duration_seconds", values[i])
|
||||
} else if value.Valid {
|
||||
_m.VideoDurationSeconds = new(int)
|
||||
*_m.VideoDurationSeconds = int(value.Int64)
|
||||
}
|
||||
case usagelog.FieldCacheTTLOverridden:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field cache_ttl_overridden", values[i])
|
||||
@@ -698,6 +724,19 @@ func (_m *UsageLog) String() string {
|
||||
builder.WriteString("image_size_breakdown=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.ImageSizeBreakdown))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("video_count=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.VideoCount))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.VideoResolution; v != nil {
|
||||
builder.WriteString("video_resolution=")
|
||||
builder.WriteString(*v)
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
if v := _m.VideoDurationSeconds; v != nil {
|
||||
builder.WriteString("video_duration_seconds=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
}
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("cache_ttl_overridden=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.CacheTTLOverridden))
|
||||
builder.WriteString(", ")
|
||||
|
||||
@@ -92,6 +92,12 @@ const (
|
||||
FieldImageSizeSource = "image_size_source"
|
||||
// FieldImageSizeBreakdown holds the string denoting the image_size_breakdown field in the database.
|
||||
FieldImageSizeBreakdown = "image_size_breakdown"
|
||||
// FieldVideoCount holds the string denoting the video_count field in the database.
|
||||
FieldVideoCount = "video_count"
|
||||
// FieldVideoResolution holds the string denoting the video_resolution field in the database.
|
||||
FieldVideoResolution = "video_resolution"
|
||||
// FieldVideoDurationSeconds holds the string denoting the video_duration_seconds field in the database.
|
||||
FieldVideoDurationSeconds = "video_duration_seconds"
|
||||
// FieldCacheTTLOverridden holds the string denoting the cache_ttl_overridden field in the database.
|
||||
FieldCacheTTLOverridden = "cache_ttl_overridden"
|
||||
// FieldCreatedAt holds the string denoting the created_at field in the database.
|
||||
@@ -187,6 +193,9 @@ var Columns = []string{
|
||||
FieldImageOutputSize,
|
||||
FieldImageSizeSource,
|
||||
FieldImageSizeBreakdown,
|
||||
FieldVideoCount,
|
||||
FieldVideoResolution,
|
||||
FieldVideoDurationSeconds,
|
||||
FieldCacheTTLOverridden,
|
||||
FieldCreatedAt,
|
||||
}
|
||||
@@ -260,6 +269,10 @@ var (
|
||||
ImageOutputSizeValidator func(string) error
|
||||
// ImageSizeSourceValidator is a validator for the "image_size_source" field. It is called by the builders before save.
|
||||
ImageSizeSourceValidator func(string) error
|
||||
// DefaultVideoCount holds the default value on creation for the "video_count" field.
|
||||
DefaultVideoCount int
|
||||
// VideoResolutionValidator is a validator for the "video_resolution" field. It is called by the builders before save.
|
||||
VideoResolutionValidator func(string) error
|
||||
// DefaultCacheTTLOverridden holds the default value on creation for the "cache_ttl_overridden" field.
|
||||
DefaultCacheTTLOverridden bool
|
||||
// DefaultCreatedAt holds the default value on creation for the "created_at" field.
|
||||
@@ -464,6 +477,21 @@ func ByImageSizeSource(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldImageSizeSource, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByVideoCount orders the results by the video_count field.
|
||||
func ByVideoCount(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldVideoCount, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByVideoResolution orders the results by the video_resolution field.
|
||||
func ByVideoResolution(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldVideoResolution, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByVideoDurationSeconds orders the results by the video_duration_seconds field.
|
||||
func ByVideoDurationSeconds(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldVideoDurationSeconds, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByCacheTTLOverridden orders the results by the cache_ttl_overridden field.
|
||||
func ByCacheTTLOverridden(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldCacheTTLOverridden, opts...).ToFunc()
|
||||
|
||||
@@ -245,6 +245,21 @@ func ImageSizeSource(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldImageSizeSource, v))
|
||||
}
|
||||
|
||||
// VideoCount applies equality check predicate on the "video_count" field. It's identical to VideoCountEQ.
|
||||
func VideoCount(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoResolution applies equality check predicate on the "video_resolution" field. It's identical to VideoResolutionEQ.
|
||||
func VideoResolution(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoDurationSeconds applies equality check predicate on the "video_duration_seconds" field. It's identical to VideoDurationSecondsEQ.
|
||||
func VideoDurationSeconds(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// CacheTTLOverridden applies equality check predicate on the "cache_ttl_overridden" field. It's identical to CacheTTLOverriddenEQ.
|
||||
func CacheTTLOverridden(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldCacheTTLOverridden, v))
|
||||
@@ -2150,6 +2165,171 @@ func ImageSizeBreakdownNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldImageSizeBreakdown))
|
||||
}
|
||||
|
||||
// VideoCountEQ applies the EQ predicate on the "video_count" field.
|
||||
func VideoCountEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoCountNEQ applies the NEQ predicate on the "video_count" field.
|
||||
func VideoCountNEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoCountIn applies the In predicate on the "video_count" field.
|
||||
func VideoCountIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldVideoCount, vs...))
|
||||
}
|
||||
|
||||
// VideoCountNotIn applies the NotIn predicate on the "video_count" field.
|
||||
func VideoCountNotIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldVideoCount, vs...))
|
||||
}
|
||||
|
||||
// VideoCountGT applies the GT predicate on the "video_count" field.
|
||||
func VideoCountGT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoCountGTE applies the GTE predicate on the "video_count" field.
|
||||
func VideoCountGTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoCountLT applies the LT predicate on the "video_count" field.
|
||||
func VideoCountLT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoCountLTE applies the LTE predicate on the "video_count" field.
|
||||
func VideoCountLTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldVideoCount, v))
|
||||
}
|
||||
|
||||
// VideoResolutionEQ applies the EQ predicate on the "video_resolution" field.
|
||||
func VideoResolutionEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionNEQ applies the NEQ predicate on the "video_resolution" field.
|
||||
func VideoResolutionNEQ(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionIn applies the In predicate on the "video_resolution" field.
|
||||
func VideoResolutionIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldVideoResolution, vs...))
|
||||
}
|
||||
|
||||
// VideoResolutionNotIn applies the NotIn predicate on the "video_resolution" field.
|
||||
func VideoResolutionNotIn(vs ...string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldVideoResolution, vs...))
|
||||
}
|
||||
|
||||
// VideoResolutionGT applies the GT predicate on the "video_resolution" field.
|
||||
func VideoResolutionGT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionGTE applies the GTE predicate on the "video_resolution" field.
|
||||
func VideoResolutionGTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionLT applies the LT predicate on the "video_resolution" field.
|
||||
func VideoResolutionLT(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionLTE applies the LTE predicate on the "video_resolution" field.
|
||||
func VideoResolutionLTE(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionContains applies the Contains predicate on the "video_resolution" field.
|
||||
func VideoResolutionContains(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContains(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionHasPrefix applies the HasPrefix predicate on the "video_resolution" field.
|
||||
func VideoResolutionHasPrefix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasPrefix(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionHasSuffix applies the HasSuffix predicate on the "video_resolution" field.
|
||||
func VideoResolutionHasSuffix(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldHasSuffix(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionIsNil applies the IsNil predicate on the "video_resolution" field.
|
||||
func VideoResolutionIsNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIsNull(FieldVideoResolution))
|
||||
}
|
||||
|
||||
// VideoResolutionNotNil applies the NotNil predicate on the "video_resolution" field.
|
||||
func VideoResolutionNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldVideoResolution))
|
||||
}
|
||||
|
||||
// VideoResolutionEqualFold applies the EqualFold predicate on the "video_resolution" field.
|
||||
func VideoResolutionEqualFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEqualFold(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoResolutionContainsFold applies the ContainsFold predicate on the "video_resolution" field.
|
||||
func VideoResolutionContainsFold(v string) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldContainsFold(FieldVideoResolution, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsEQ applies the EQ predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsNEQ applies the NEQ predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsNEQ(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsIn applies the In predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIn(FieldVideoDurationSeconds, vs...))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsNotIn applies the NotIn predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsNotIn(vs ...int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotIn(FieldVideoDurationSeconds, vs...))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsGT applies the GT predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsGT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGT(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsGTE applies the GTE predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsGTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldGTE(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsLT applies the LT predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsLT(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLT(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsLTE applies the LTE predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsLTE(v int) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldVideoDurationSeconds, v))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsIsNil applies the IsNil predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsIsNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldIsNull(FieldVideoDurationSeconds))
|
||||
}
|
||||
|
||||
// VideoDurationSecondsNotNil applies the NotNil predicate on the "video_duration_seconds" field.
|
||||
func VideoDurationSecondsNotNil() predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNotNull(FieldVideoDurationSeconds))
|
||||
}
|
||||
|
||||
// CacheTTLOverriddenEQ applies the EQ predicate on the "cache_ttl_overridden" field.
|
||||
func CacheTTLOverriddenEQ(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldCacheTTLOverridden, v))
|
||||
|
||||
@@ -525,6 +525,48 @@ func (_c *UsageLogCreate) SetImageSizeBreakdown(v map[string]int) *UsageLogCreat
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (_c *UsageLogCreate) SetVideoCount(v int) *UsageLogCreate {
|
||||
_c.mutation.SetVideoCount(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableVideoCount sets the "video_count" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableVideoCount(v *int) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetVideoCount(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (_c *UsageLogCreate) SetVideoResolution(v string) *UsageLogCreate {
|
||||
_c.mutation.SetVideoResolution(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableVideoResolution sets the "video_resolution" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableVideoResolution(v *string) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetVideoResolution(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (_c *UsageLogCreate) SetVideoDurationSeconds(v int) *UsageLogCreate {
|
||||
_c.mutation.SetVideoDurationSeconds(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableVideoDurationSeconds sets the "video_duration_seconds" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableVideoDurationSeconds(v *int) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetVideoDurationSeconds(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (_c *UsageLogCreate) SetCacheTTLOverridden(v bool) *UsageLogCreate {
|
||||
_c.mutation.SetCacheTTLOverridden(v)
|
||||
@@ -677,6 +719,10 @@ func (_c *UsageLogCreate) defaults() {
|
||||
v := usagelog.DefaultImageCount
|
||||
_c.mutation.SetImageCount(v)
|
||||
}
|
||||
if _, ok := _c.mutation.VideoCount(); !ok {
|
||||
v := usagelog.DefaultVideoCount
|
||||
_c.mutation.SetVideoCount(v)
|
||||
}
|
||||
if _, ok := _c.mutation.CacheTTLOverridden(); !ok {
|
||||
v := usagelog.DefaultCacheTTLOverridden
|
||||
_c.mutation.SetCacheTTLOverridden(v)
|
||||
@@ -817,6 +863,14 @@ func (_c *UsageLogCreate) check() error {
|
||||
return &ValidationError{Name: "image_size_source", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size_source": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.VideoCount(); !ok {
|
||||
return &ValidationError{Name: "video_count", err: errors.New(`ent: missing required field "UsageLog.video_count"`)}
|
||||
}
|
||||
if v, ok := _c.mutation.VideoResolution(); ok {
|
||||
if err := usagelog.VideoResolutionValidator(v); err != nil {
|
||||
return &ValidationError{Name: "video_resolution", err: fmt.Errorf(`ent: validator failed for field "UsageLog.video_resolution": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _, ok := _c.mutation.CacheTTLOverridden(); !ok {
|
||||
return &ValidationError{Name: "cache_ttl_overridden", err: errors.New(`ent: missing required field "UsageLog.cache_ttl_overridden"`)}
|
||||
}
|
||||
@@ -995,6 +1049,18 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(usagelog.FieldImageSizeBreakdown, field.TypeJSON, value)
|
||||
_node.ImageSizeBreakdown = value
|
||||
}
|
||||
if value, ok := _c.mutation.VideoCount(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoCount, field.TypeInt, value)
|
||||
_node.VideoCount = value
|
||||
}
|
||||
if value, ok := _c.mutation.VideoResolution(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoResolution, field.TypeString, value)
|
||||
_node.VideoResolution = &value
|
||||
}
|
||||
if value, ok := _c.mutation.VideoDurationSeconds(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoDurationSeconds, field.TypeInt, value)
|
||||
_node.VideoDurationSeconds = &value
|
||||
}
|
||||
if value, ok := _c.mutation.CacheTTLOverridden(); ok {
|
||||
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||
_node.CacheTTLOverridden = value
|
||||
@@ -1830,6 +1896,66 @@ func (u *UsageLogUpsert) ClearImageSizeBreakdown() *UsageLogUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (u *UsageLogUpsert) SetVideoCount(v int) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldVideoCount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateVideoCount sets the "video_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateVideoCount() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldVideoCount)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddVideoCount adds v to the "video_count" field.
|
||||
func (u *UsageLogUpsert) AddVideoCount(v int) *UsageLogUpsert {
|
||||
u.Add(usagelog.FieldVideoCount, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (u *UsageLogUpsert) SetVideoResolution(v string) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldVideoResolution, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateVideoResolution sets the "video_resolution" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateVideoResolution() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldVideoResolution)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (u *UsageLogUpsert) ClearVideoResolution() *UsageLogUpsert {
|
||||
u.SetNull(usagelog.FieldVideoResolution)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsert) SetVideoDurationSeconds(v int) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldVideoDurationSeconds, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateVideoDurationSeconds sets the "video_duration_seconds" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateVideoDurationSeconds() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldVideoDurationSeconds)
|
||||
return u
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds v to the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsert) AddVideoDurationSeconds(v int) *UsageLogUpsert {
|
||||
u.Add(usagelog.FieldVideoDurationSeconds, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsert) ClearVideoDurationSeconds() *UsageLogUpsert {
|
||||
u.SetNull(usagelog.FieldVideoDurationSeconds)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (u *UsageLogUpsert) SetCacheTTLOverridden(v bool) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldCacheTTLOverridden, v)
|
||||
@@ -2692,6 +2818,76 @@ func (u *UsageLogUpsertOne) ClearImageSizeBreakdown() *UsageLogUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (u *UsageLogUpsertOne) SetVideoCount(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddVideoCount adds v to the "video_count" field.
|
||||
func (u *UsageLogUpsertOne) AddVideoCount(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddVideoCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoCount sets the "video_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateVideoCount() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoCount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (u *UsageLogUpsertOne) SetVideoResolution(v string) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoResolution(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoResolution sets the "video_resolution" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateVideoResolution() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoResolution()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (u *UsageLogUpsertOne) ClearVideoResolution() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearVideoResolution()
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertOne) SetVideoDurationSeconds(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoDurationSeconds(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds v to the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertOne) AddVideoDurationSeconds(v int) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddVideoDurationSeconds(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoDurationSeconds sets the "video_duration_seconds" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateVideoDurationSeconds() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoDurationSeconds()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertOne) ClearVideoDurationSeconds() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearVideoDurationSeconds()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (u *UsageLogUpsertOne) SetCacheTTLOverridden(v bool) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
@@ -3722,6 +3918,76 @@ func (u *UsageLogUpsertBulk) ClearImageSizeBreakdown() *UsageLogUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (u *UsageLogUpsertBulk) SetVideoCount(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddVideoCount adds v to the "video_count" field.
|
||||
func (u *UsageLogUpsertBulk) AddVideoCount(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddVideoCount(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoCount sets the "video_count" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateVideoCount() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoCount()
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (u *UsageLogUpsertBulk) SetVideoResolution(v string) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoResolution(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoResolution sets the "video_resolution" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateVideoResolution() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoResolution()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (u *UsageLogUpsertBulk) ClearVideoResolution() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearVideoResolution()
|
||||
})
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertBulk) SetVideoDurationSeconds(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetVideoDurationSeconds(v)
|
||||
})
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds v to the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertBulk) AddVideoDurationSeconds(v int) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.AddVideoDurationSeconds(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateVideoDurationSeconds sets the "video_duration_seconds" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateVideoDurationSeconds() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateVideoDurationSeconds()
|
||||
})
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (u *UsageLogUpsertBulk) ClearVideoDurationSeconds() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.ClearVideoDurationSeconds()
|
||||
})
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (u *UsageLogUpsertBulk) SetCacheTTLOverridden(v bool) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
|
||||
@@ -811,6 +811,74 @@ func (_u *UsageLogUpdate) ClearImageSizeBreakdown() *UsageLogUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (_u *UsageLogUpdate) SetVideoCount(v int) *UsageLogUpdate {
|
||||
_u.mutation.ResetVideoCount()
|
||||
_u.mutation.SetVideoCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoCount sets the "video_count" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableVideoCount(v *int) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetVideoCount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddVideoCount adds value to the "video_count" field.
|
||||
func (_u *UsageLogUpdate) AddVideoCount(v int) *UsageLogUpdate {
|
||||
_u.mutation.AddVideoCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (_u *UsageLogUpdate) SetVideoResolution(v string) *UsageLogUpdate {
|
||||
_u.mutation.SetVideoResolution(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoResolution sets the "video_resolution" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableVideoResolution(v *string) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetVideoResolution(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (_u *UsageLogUpdate) ClearVideoResolution() *UsageLogUpdate {
|
||||
_u.mutation.ClearVideoResolution()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdate) SetVideoDurationSeconds(v int) *UsageLogUpdate {
|
||||
_u.mutation.ResetVideoDurationSeconds()
|
||||
_u.mutation.SetVideoDurationSeconds(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoDurationSeconds sets the "video_duration_seconds" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableVideoDurationSeconds(v *int) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetVideoDurationSeconds(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds value to the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdate) AddVideoDurationSeconds(v int) *UsageLogUpdate {
|
||||
_u.mutation.AddVideoDurationSeconds(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdate) ClearVideoDurationSeconds() *UsageLogUpdate {
|
||||
_u.mutation.ClearVideoDurationSeconds()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (_u *UsageLogUpdate) SetCacheTTLOverridden(v bool) *UsageLogUpdate {
|
||||
_u.mutation.SetCacheTTLOverridden(v)
|
||||
@@ -979,6 +1047,11 @@ func (_u *UsageLogUpdate) check() error {
|
||||
return &ValidationError{Name: "image_size_source", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size_source": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.VideoResolution(); ok {
|
||||
if err := usagelog.VideoResolutionValidator(v); err != nil {
|
||||
return &ValidationError{Name: "video_resolution", err: fmt.Errorf(`ent: validator failed for field "UsageLog.video_resolution": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||
}
|
||||
@@ -1210,6 +1283,27 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if _u.mutation.ImageSizeBreakdownCleared() {
|
||||
_spec.ClearField(usagelog.FieldImageSizeBreakdown, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoCount(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedVideoCount(); ok {
|
||||
_spec.AddField(usagelog.FieldVideoCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoResolution(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoResolution, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.VideoResolutionCleared() {
|
||||
_spec.ClearField(usagelog.FieldVideoResolution, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoDurationSeconds(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoDurationSeconds, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedVideoDurationSeconds(); ok {
|
||||
_spec.AddField(usagelog.FieldVideoDurationSeconds, field.TypeInt, value)
|
||||
}
|
||||
if _u.mutation.VideoDurationSecondsCleared() {
|
||||
_spec.ClearField(usagelog.FieldVideoDurationSeconds, field.TypeInt)
|
||||
}
|
||||
if value, ok := _u.mutation.CacheTTLOverridden(); ok {
|
||||
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||
}
|
||||
@@ -2157,6 +2251,74 @@ func (_u *UsageLogUpdateOne) ClearImageSizeBreakdown() *UsageLogUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoCount sets the "video_count" field.
|
||||
func (_u *UsageLogUpdateOne) SetVideoCount(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.ResetVideoCount()
|
||||
_u.mutation.SetVideoCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoCount sets the "video_count" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableVideoCount(v *int) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetVideoCount(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddVideoCount adds value to the "video_count" field.
|
||||
func (_u *UsageLogUpdateOne) AddVideoCount(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.AddVideoCount(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoResolution sets the "video_resolution" field.
|
||||
func (_u *UsageLogUpdateOne) SetVideoResolution(v string) *UsageLogUpdateOne {
|
||||
_u.mutation.SetVideoResolution(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoResolution sets the "video_resolution" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableVideoResolution(v *string) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetVideoResolution(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoResolution clears the value of the "video_resolution" field.
|
||||
func (_u *UsageLogUpdateOne) ClearVideoResolution() *UsageLogUpdateOne {
|
||||
_u.mutation.ClearVideoResolution()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetVideoDurationSeconds sets the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdateOne) SetVideoDurationSeconds(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.ResetVideoDurationSeconds()
|
||||
_u.mutation.SetVideoDurationSeconds(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableVideoDurationSeconds sets the "video_duration_seconds" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableVideoDurationSeconds(v *int) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetVideoDurationSeconds(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// AddVideoDurationSeconds adds value to the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdateOne) AddVideoDurationSeconds(v int) *UsageLogUpdateOne {
|
||||
_u.mutation.AddVideoDurationSeconds(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// ClearVideoDurationSeconds clears the value of the "video_duration_seconds" field.
|
||||
func (_u *UsageLogUpdateOne) ClearVideoDurationSeconds() *UsageLogUpdateOne {
|
||||
_u.mutation.ClearVideoDurationSeconds()
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetCacheTTLOverridden sets the "cache_ttl_overridden" field.
|
||||
func (_u *UsageLogUpdateOne) SetCacheTTLOverridden(v bool) *UsageLogUpdateOne {
|
||||
_u.mutation.SetCacheTTLOverridden(v)
|
||||
@@ -2338,6 +2500,11 @@ func (_u *UsageLogUpdateOne) check() error {
|
||||
return &ValidationError{Name: "image_size_source", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size_source": %w`, err)}
|
||||
}
|
||||
}
|
||||
if v, ok := _u.mutation.VideoResolution(); ok {
|
||||
if err := usagelog.VideoResolutionValidator(v); err != nil {
|
||||
return &ValidationError{Name: "video_resolution", err: fmt.Errorf(`ent: validator failed for field "UsageLog.video_resolution": %w`, err)}
|
||||
}
|
||||
}
|
||||
if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 {
|
||||
return errors.New(`ent: clearing a required unique edge "UsageLog.user"`)
|
||||
}
|
||||
@@ -2586,6 +2753,27 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
||||
if _u.mutation.ImageSizeBreakdownCleared() {
|
||||
_spec.ClearField(usagelog.FieldImageSizeBreakdown, field.TypeJSON)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoCount(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedVideoCount(); ok {
|
||||
_spec.AddField(usagelog.FieldVideoCount, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoResolution(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoResolution, field.TypeString, value)
|
||||
}
|
||||
if _u.mutation.VideoResolutionCleared() {
|
||||
_spec.ClearField(usagelog.FieldVideoResolution, field.TypeString)
|
||||
}
|
||||
if value, ok := _u.mutation.VideoDurationSeconds(); ok {
|
||||
_spec.SetField(usagelog.FieldVideoDurationSeconds, field.TypeInt, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AddedVideoDurationSeconds(); ok {
|
||||
_spec.AddField(usagelog.FieldVideoDurationSeconds, field.TypeInt, value)
|
||||
}
|
||||
if _u.mutation.VideoDurationSecondsCleared() {
|
||||
_spec.ClearField(usagelog.FieldVideoDurationSeconds, field.TypeInt)
|
||||
}
|
||||
if value, ok := _u.mutation.CacheTTLOverridden(); ok {
|
||||
_spec.SetField(usagelog.FieldCacheTTLOverridden, field.TypeBool, value)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) {
|
||||
requireColumn(t, tx, "usage_logs", "image_output_size", "character varying", 32, true)
|
||||
requireColumn(t, tx, "usage_logs", "image_size_source", "character varying", 16, true)
|
||||
requireColumn(t, tx, "usage_logs", "image_size_breakdown", "jsonb", 0, true)
|
||||
requireColumn(t, tx, "usage_logs", "video_count", "integer", 0, false)
|
||||
requireColumn(t, tx, "usage_logs", "video_resolution", "character varying", 10, true)
|
||||
requireColumn(t, tx, "usage_logs", "video_duration_seconds", "integer", 0, true)
|
||||
requireConstraintDefinitionContains(
|
||||
t,
|
||||
tx,
|
||||
@@ -68,6 +71,7 @@ func TestMigrationsRunner_IsIdempotent_AndSchemaIsUpToDate(t *testing.T) {
|
||||
"image_count",
|
||||
"billing_mode",
|
||||
"'video'",
|
||||
"video_count",
|
||||
"image_size IS NOT NULL",
|
||||
"'1K'",
|
||||
"'2K'",
|
||||
|
||||
@@ -63,6 +63,9 @@ var usageLogInsertArgTypes = [...]string{
|
||||
"text", // image_output_size
|
||||
"text", // image_size_source
|
||||
"jsonb", // image_size_breakdown
|
||||
"integer", // video_count
|
||||
"text", // video_resolution
|
||||
"integer", // video_duration_seconds
|
||||
"text", // service_tier
|
||||
"text", // reasoning_effort
|
||||
"text", // inbound_endpoint
|
||||
@@ -252,6 +255,9 @@ func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor,
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -269,7 +275,7 @@ func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor,
|
||||
$10, $11, $12, $13,
|
||||
$14, $15, $16, $17,
|
||||
$18, $19, $20, $21, $22, $23,
|
||||
$24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50
|
||||
$24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53
|
||||
)
|
||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||
RETURNING id, created_at
|
||||
@@ -700,6 +706,9 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -713,7 +722,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
created_at
|
||||
) AS (VALUES `)
|
||||
|
||||
args := make([]any, 0, len(keys)*50)
|
||||
args := make([]any, 0, len(keys)*53)
|
||||
argPos := 1
|
||||
for idx, key := range keys {
|
||||
if idx > 0 {
|
||||
@@ -781,6 +790,9 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -833,6 +845,9 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -925,6 +940,9 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -938,7 +956,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
created_at
|
||||
) AS (VALUES `)
|
||||
|
||||
args := make([]any, 0, len(preparedList)*50)
|
||||
args := make([]any, 0, len(preparedList)*53)
|
||||
argPos := 1
|
||||
for idx, prepared := range preparedList {
|
||||
if idx > 0 {
|
||||
@@ -1003,6 +1021,9 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -1055,6 +1076,9 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -1115,6 +1139,9 @@ func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared
|
||||
image_output_size,
|
||||
image_size_source,
|
||||
image_size_breakdown,
|
||||
video_count,
|
||||
video_resolution,
|
||||
video_duration_seconds,
|
||||
service_tier,
|
||||
reasoning_effort,
|
||||
inbound_endpoint,
|
||||
@@ -1132,7 +1159,7 @@ func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared
|
||||
$10, $11, $12, $13,
|
||||
$14, $15, $16, $17,
|
||||
$18, $19, $20, $21, $22, $23,
|
||||
$24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50
|
||||
$24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53
|
||||
)
|
||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||
`, prepared.args...)
|
||||
@@ -1163,6 +1190,8 @@ func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
|
||||
imageOutputSize := nullString(log.ImageOutputSize)
|
||||
imageSizeSource := nullString(log.ImageSizeSource)
|
||||
imageSizeBreakdown := nullStringIntMapJSON(log.ImageSizeBreakdown)
|
||||
videoResolution := nullString(log.VideoResolution)
|
||||
videoDurationSeconds := nullInt(log.VideoDurationSeconds)
|
||||
serviceTier := nullString(log.ServiceTier)
|
||||
reasoningEffort := nullString(log.ReasoningEffort)
|
||||
inboundEndpoint := nullString(log.InboundEndpoint)
|
||||
@@ -1227,6 +1256,9 @@ func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
|
||||
imageOutputSize,
|
||||
imageSizeSource,
|
||||
imageSizeBreakdown,
|
||||
log.VideoCount,
|
||||
videoResolution,
|
||||
videoDurationSeconds,
|
||||
serviceTier,
|
||||
reasoningEffort,
|
||||
inboundEndpoint,
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
)
|
||||
|
||||
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, requested_model, upstream_model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, image_output_tokens, image_output_cost, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, request_type, stream, openai_ws_mode, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, image_input_size, image_output_size, image_size_source, image_size_breakdown, service_tier, reasoning_effort, inbound_endpoint, upstream_endpoint, cache_ttl_overridden, channel_id, model_mapping_chain, billing_tier, billing_mode, account_stats_cost, created_at"
|
||||
const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, requested_model, upstream_model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, image_output_tokens, image_output_cost, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, request_type, stream, openai_ws_mode, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, image_input_size, image_output_size, image_size_source, image_size_breakdown, video_count, video_resolution, video_duration_seconds, service_tier, reasoning_effort, inbound_endpoint, upstream_endpoint, cache_ttl_overridden, channel_id, model_mapping_chain, billing_tier, billing_mode, account_stats_cost, created_at"
|
||||
|
||||
func (r *usageLogRepository) GetByID(ctx context.Context, id int64) (log *service.UsageLog, err error) {
|
||||
query := "SELECT " + usageLogSelectColumns + " FROM usage_logs WHERE id = $1"
|
||||
@@ -465,6 +465,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
imageOutputSize sql.NullString
|
||||
imageSizeSource sql.NullString
|
||||
imageSizeBreakdown sql.NullString
|
||||
videoCount int
|
||||
videoResolution sql.NullString
|
||||
videoDurationSeconds sql.NullInt64
|
||||
serviceTier sql.NullString
|
||||
reasoningEffort sql.NullString
|
||||
inboundEndpoint sql.NullString
|
||||
@@ -519,6 +522,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
&imageOutputSize,
|
||||
&imageSizeSource,
|
||||
&imageSizeBreakdown,
|
||||
&videoCount,
|
||||
&videoResolution,
|
||||
&videoDurationSeconds,
|
||||
&serviceTier,
|
||||
&reasoningEffort,
|
||||
&inboundEndpoint,
|
||||
@@ -560,6 +566,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
BillingType: int8(billingType),
|
||||
RequestType: service.RequestTypeFromInt16(requestTypeRaw),
|
||||
ImageCount: imageCount,
|
||||
VideoCount: videoCount,
|
||||
CacheTTLOverridden: cacheTTLOverridden,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
@@ -607,6 +614,13 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
log.ImageSizeSource = &imageSizeSource.String
|
||||
}
|
||||
log.ImageSizeBreakdown = stringIntMapFromNullJSON(imageSizeBreakdown)
|
||||
if videoResolution.Valid {
|
||||
log.VideoResolution = &videoResolution.String
|
||||
}
|
||||
if videoDurationSeconds.Valid {
|
||||
value := int(videoDurationSeconds.Int64)
|
||||
log.VideoDurationSeconds = &value
|
||||
}
|
||||
if serviceTier.Valid {
|
||||
log.ServiceTier = &serviceTier.String
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ func TestUsageLogRepositoryCreateSyncRequestTypeAndLegacyFields(t *testing.T) {
|
||||
sqlmock.AnyArg(), // image_output_size
|
||||
sqlmock.AnyArg(), // image_size_source
|
||||
sqlmock.AnyArg(), // image_size_breakdown
|
||||
sqlmock.AnyArg(), // video_count
|
||||
sqlmock.AnyArg(), // video_resolution
|
||||
sqlmock.AnyArg(), // video_duration_seconds
|
||||
sqlmock.AnyArg(), // service_tier
|
||||
sqlmock.AnyArg(), // reasoning_effort
|
||||
sqlmock.AnyArg(), // inbound_endpoint
|
||||
@@ -163,6 +166,9 @@ func TestUsageLogRepositoryCreate_PersistsServiceTier(t *testing.T) {
|
||||
sqlmock.AnyArg(), // image_output_size
|
||||
sqlmock.AnyArg(), // image_size_source
|
||||
sqlmock.AnyArg(), // image_size_breakdown
|
||||
sqlmock.AnyArg(), // video_count
|
||||
sqlmock.AnyArg(), // video_resolution
|
||||
sqlmock.AnyArg(), // video_duration_seconds
|
||||
serviceTier,
|
||||
sqlmock.AnyArg(),
|
||||
sqlmock.AnyArg(),
|
||||
@@ -799,6 +805,9 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{Valid: true, String: "3840x2160"},
|
||||
sql.NullString{Valid: true, String: "output"},
|
||||
sql.NullString{Valid: true, String: `{"4K":2}`},
|
||||
0, // video_count
|
||||
sql.NullString{}, // video_resolution
|
||||
sql.NullInt64{}, // video_duration_seconds
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
@@ -867,6 +876,9 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{}, // image_output_size
|
||||
sql.NullString{}, // image_size_source
|
||||
sql.NullString{}, // image_size_breakdown
|
||||
0, // video_count
|
||||
sql.NullString{}, // video_resolution
|
||||
sql.NullInt64{}, // video_duration_seconds
|
||||
sql.NullString{Valid: true, String: "priority"},
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
@@ -919,6 +931,9 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{}, // image_output_size
|
||||
sql.NullString{}, // image_size_source
|
||||
sql.NullString{}, // image_size_breakdown
|
||||
0, // video_count
|
||||
sql.NullString{}, // video_resolution
|
||||
sql.NullInt64{}, // video_duration_seconds
|
||||
sql.NullString{Valid: true, String: "flex"},
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
@@ -971,6 +986,9 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{}, // image_output_size
|
||||
sql.NullString{}, // image_size_source
|
||||
sql.NullString{}, // image_size_breakdown
|
||||
0, // video_count
|
||||
sql.NullString{}, // video_resolution
|
||||
sql.NullInt64{}, // video_duration_seconds
|
||||
sql.NullString{Valid: true, String: "priority"},
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
|
||||
@@ -1231,11 +1231,11 @@ type ImagePriceConfig struct {
|
||||
Price4K *float64 // 4K 尺寸价格(nil 表示使用默认值)
|
||||
}
|
||||
|
||||
// VideoPriceConfig 视频生成计费配置。
|
||||
// VideoPriceConfig 视频生成计费配置。所有价格均为**每秒**单价(USD/s),与 xAI 官方计费口径一致。
|
||||
type VideoPriceConfig struct {
|
||||
Price480P *float64 // 480p 视频价格(nil 表示使用默认值)
|
||||
Price720P *float64 // 720p 视频价格(nil 表示使用默认值)
|
||||
Price1080P *float64 // 1080p 视频价格(nil 表示使用默认值)
|
||||
Price480P *float64 // 480p 每秒价格(nil 表示使用默认值)
|
||||
Price720P *float64 // 720p 每秒价格(nil 表示使用默认值)
|
||||
Price1080P *float64 // 1080p 每秒价格(nil 表示使用默认值)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -1246,6 +1246,7 @@ const (
|
||||
defaultGrokImagineImageQualityPrice1K = 0.05
|
||||
defaultGrokImagineImageQualityPrice2K = 0.07
|
||||
|
||||
// 视频默认价为 xAI 官方**每秒**输出价格(USD/s),总价 = 每秒价 × 时长(秒)。
|
||||
defaultGrokImagineVideoPrice480P = 0.05
|
||||
defaultGrokImagineVideoPrice720P = 0.07
|
||||
defaultGrokImagineVideo15Price480P = 0.08
|
||||
@@ -1284,20 +1285,22 @@ func (s *BillingService) CalculateImageCost(model string, imageSize string, imag
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateVideoCost 计算视频生成费用。
|
||||
// CalculateVideoCost 计算视频生成费用(按秒计费,与 xAI 口径一致)。
|
||||
// model: 请求的模型名称(用于获取默认价格)
|
||||
// resolution: 视频分辨率 "480p", "720p", "1080p"
|
||||
// videoCount: 生成的视频数量
|
||||
// groupConfig: 分组配置的价格(可能为 nil,表示使用默认值)
|
||||
// durationSeconds: 单个视频时长(秒),<=0 时按上游默认时长计
|
||||
// groupConfig: 分组配置的每秒价格(可能为 nil,表示使用默认值)
|
||||
// rateMultiplier: 费率倍数
|
||||
func (s *BillingService) CalculateVideoCost(model string, resolution string, videoCount int, groupConfig *VideoPriceConfig, rateMultiplier float64) *CostBreakdown {
|
||||
func (s *BillingService) CalculateVideoCost(model string, resolution string, videoCount int, durationSeconds int, groupConfig *VideoPriceConfig, rateMultiplier float64) *CostBreakdown {
|
||||
if videoCount <= 0 {
|
||||
return &CostBreakdown{}
|
||||
}
|
||||
resolution = NormalizeVideoBillingResolutionOrDefault(resolution)
|
||||
durationSeconds = NormalizeVideoBillingDurationSecondsOrDefault(durationSeconds)
|
||||
|
||||
unitPrice := s.getVideoUnitPrice(model, resolution, groupConfig)
|
||||
totalCost := unitPrice * float64(videoCount)
|
||||
perSecondPrice := s.getVideoUnitPrice(model, resolution, groupConfig)
|
||||
totalCost := perSecondPrice * float64(durationSeconds) * float64(videoCount)
|
||||
|
||||
if rateMultiplier < 0 {
|
||||
rateMultiplier = 0
|
||||
@@ -1394,8 +1397,9 @@ func (s *BillingService) getDefaultVideoPrice(model string, resolution string) f
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Keep the historical model default as the fallback (interpreted as a per-second
|
||||
// rate; today only Grok models reach video billing, so this path is a safety net),
|
||||
// while letting group-level video prices override it independently from image prices.
|
||||
return s.getDefaultImagePrice(model, ImageBillingSize2K)
|
||||
}
|
||||
|
||||
|
||||
@@ -878,14 +878,29 @@ func TestCalculateVideoCostUsesSeparateConfig(t *testing.T) {
|
||||
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)
|
||||
videoCost := svc.CalculateVideoCost("grok-imagine-video", "480p", 1, 10, &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.InDelta(t, 0.8, videoCost.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.4, videoCost.ActualCost, 1e-10)
|
||||
require.Equal(t, string(BillingModeVideo), videoCost.BillingMode)
|
||||
}
|
||||
|
||||
func TestCalculateVideoCostBillsPerSecond(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
oneSecond := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, 1, nil, 1.0)
|
||||
fifteenSeconds := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, 15, nil, 1.0)
|
||||
// duration <=0 时按上游默认 8 秒计费,超出上限按 15 秒收敛。
|
||||
defaultDuration := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, 0, nil, 1.0)
|
||||
clampedDuration := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, 999, nil, 1.0)
|
||||
|
||||
require.InDelta(t, 0.07, oneSecond.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.07*15, fifteenSeconds.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.07*8, defaultDuration.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.07*15, clampedDuration.TotalCost, 1e-10)
|
||||
}
|
||||
|
||||
func TestCalculateGrokImagineImageCostUsesDefaultRateCard(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
@@ -903,11 +918,12 @@ func TestCalculateGrokImagineImageCostUsesDefaultRateCard(t *testing.T) {
|
||||
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)
|
||||
// 默认价目为 xAI 官方每秒价格,按 1 秒时长验证每秒单价。
|
||||
standard480P := svc.CalculateVideoCost("grok-imagine-video", "480p", 1, 1, nil, 1.0)
|
||||
standard720P := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, 1, nil, 1.0)
|
||||
video15_480P := svc.CalculateVideoCost("grok-imagine-video-1.5", "480p", 1, 1, nil, 1.0)
|
||||
video15_720P := svc.CalculateVideoCost("grok-imagine-video-1.5", "720p", 1, 1, nil, 1.0)
|
||||
video15_1080P := svc.CalculateVideoCost("grok-imagine-video-1.5", "1080p", 1, 1, nil, 1.0)
|
||||
|
||||
require.InDelta(t, 0.05, standard480P.TotalCost, 1e-10)
|
||||
require.InDelta(t, 0.07, standard720P.TotalCost, 1e-10)
|
||||
|
||||
@@ -43,16 +43,17 @@ func (e GrokMediaEndpoint) IsGenerationRequest() bool {
|
||||
}
|
||||
|
||||
type GrokMediaRequestInfo struct {
|
||||
Model string
|
||||
Prompt string
|
||||
N int
|
||||
Size string
|
||||
SizeTier string
|
||||
Resolution string
|
||||
InputImageURLs []string
|
||||
MaskImageURL string
|
||||
Uploads []OpenAIImagesUpload
|
||||
MaskUpload *OpenAIImagesUpload
|
||||
Model string
|
||||
Prompt string
|
||||
N int
|
||||
Size string
|
||||
SizeTier string
|
||||
Resolution string
|
||||
DurationSeconds int
|
||||
InputImageURLs []string
|
||||
MaskImageURL string
|
||||
Uploads []OpenAIImagesUpload
|
||||
MaskUpload *OpenAIImagesUpload
|
||||
}
|
||||
|
||||
func (r GrokMediaRequestInfo) ModerationBody() []byte {
|
||||
@@ -116,6 +117,7 @@ func ParseGrokMediaRequest(contentType string, body []byte) GrokMediaRequestInfo
|
||||
info.Size = strings.TrimSpace(info.Size)
|
||||
info.SizeTier = NormalizeImageBillingTierOrDefault(info.Size)
|
||||
info.Resolution = NormalizeVideoBillingResolutionOrDefault(info.Resolution)
|
||||
info.DurationSeconds = NormalizeVideoBillingDurationSecondsOrDefault(info.DurationSeconds)
|
||||
if info.N <= 0 {
|
||||
info.N = 1
|
||||
}
|
||||
@@ -130,6 +132,9 @@ func parseGrokMediaJSONRequest(body []byte, info *GrokMediaRequestInfo) {
|
||||
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 duration := gjson.GetBytes(body, "duration"); duration.Exists() && duration.Type == gjson.Number {
|
||||
info.DurationSeconds = int(duration.Int())
|
||||
}
|
||||
if n := gjson.GetBytes(body, "n"); n.Exists() && n.Type == gjson.Number {
|
||||
info.N = int(n.Int())
|
||||
}
|
||||
@@ -231,6 +236,10 @@ func parseGrokMediaMultipartRequest(contentType string, body []byte, info *GrokM
|
||||
info.Size = value
|
||||
case "resolution":
|
||||
info.Resolution = value
|
||||
case "duration":
|
||||
if duration, err := strconv.Atoi(value); err == nil {
|
||||
info.DurationSeconds = duration
|
||||
}
|
||||
case "n":
|
||||
if n, err := strconv.Atoi(value); err == nil {
|
||||
info.N = n
|
||||
@@ -356,20 +365,21 @@ func (s *OpenAIGatewayService) ForwardGrokMedia(
|
||||
writeGrokMediaResponse(c, resp, respBody, s.responseHeaderFilter)
|
||||
usage := grokMediaUsageFromResponse(endpoint, requestInfo, respBody)
|
||||
return &OpenAIForwardResult{
|
||||
RequestID: requestIDHeader,
|
||||
ResponseID: usage.ResponseID,
|
||||
Usage: usage.Usage,
|
||||
Model: requestModel,
|
||||
BillingModel: requestModel,
|
||||
UpstreamModel: requestModel,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
Duration: time.Since(startTime),
|
||||
ImageCount: usage.ImageCount,
|
||||
ImageSize: usage.ImageSize,
|
||||
ImageInputSize: usage.ImageInputSize,
|
||||
ImageOutputSizes: usage.ImageOutputSizes,
|
||||
VideoCount: usage.VideoCount,
|
||||
VideoResolution: usage.VideoResolution,
|
||||
RequestID: requestIDHeader,
|
||||
ResponseID: usage.ResponseID,
|
||||
Usage: usage.Usage,
|
||||
Model: requestModel,
|
||||
BillingModel: requestModel,
|
||||
UpstreamModel: requestModel,
|
||||
ResponseHeaders: resp.Header.Clone(),
|
||||
Duration: time.Since(startTime),
|
||||
ImageCount: usage.ImageCount,
|
||||
ImageSize: usage.ImageSize,
|
||||
ImageInputSize: usage.ImageInputSize,
|
||||
ImageOutputSizes: usage.ImageOutputSizes,
|
||||
VideoCount: usage.VideoCount,
|
||||
VideoResolution: usage.VideoResolution,
|
||||
VideoDurationSeconds: usage.VideoDurationSeconds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -472,14 +482,15 @@ func normalizeGrokMediaModelForEndpoint(endpoint GrokMediaEndpoint, model string
|
||||
}
|
||||
|
||||
type grokMediaUsageMetadata struct {
|
||||
ResponseID string
|
||||
Usage OpenAIUsage
|
||||
ImageCount int
|
||||
ImageSize string
|
||||
ImageInputSize string
|
||||
ImageOutputSizes []string
|
||||
VideoCount int
|
||||
VideoResolution string
|
||||
ResponseID string
|
||||
Usage OpenAIUsage
|
||||
ImageCount int
|
||||
ImageSize string
|
||||
ImageInputSize string
|
||||
ImageOutputSizes []string
|
||||
VideoCount int
|
||||
VideoResolution string
|
||||
VideoDurationSeconds int
|
||||
}
|
||||
|
||||
func grokMediaUsageFromResponse(endpoint GrokMediaEndpoint, requestInfo GrokMediaRequestInfo, responseBody []byte) grokMediaUsageMetadata {
|
||||
@@ -502,6 +513,7 @@ func grokMediaUsageFromResponse(endpoint GrokMediaEndpoint, requestInfo GrokMedi
|
||||
meta.ResponseID = extractGrokMediaVideoRequestID(responseBody)
|
||||
meta.VideoCount = 1
|
||||
meta.VideoResolution = requestInfo.Resolution
|
||||
meta.VideoDurationSeconds = requestInfo.DurationSeconds
|
||||
// Keep the legacy media-unit counter populated for existing usage displays.
|
||||
meta.ImageCount = 1
|
||||
}
|
||||
|
||||
@@ -337,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","resolution":"720p"}`)
|
||||
body := []byte(`{"model":"grok-imagine-video-1.5","prompt":"waves","resolution":"720p","duration":10}`)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/videos/generations", bytes.NewReader(body))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -365,7 +365,7 @@ 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","resolution":"720p"}`, string(upstream.lastBody))
|
||||
require.JSONEq(t, `{"model":"grok-imagine-video","prompt":"waves","resolution":"720p","duration":10}`, 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)
|
||||
@@ -374,6 +374,7 @@ func TestForwardGrokMediaVideoGenerationReturnsUsageAndResponseID(t *testing.T)
|
||||
require.Empty(t, result.ImageSize)
|
||||
require.Equal(t, 1, result.VideoCount)
|
||||
require.Equal(t, VideoBillingResolution720P, result.VideoResolution)
|
||||
require.Equal(t, 10, result.VideoDurationSeconds)
|
||||
}
|
||||
|
||||
func TestForwardGrokMediaVideoGenerationPreservesImageToVideoModel(t *testing.T) {
|
||||
@@ -412,6 +413,8 @@ func TestForwardGrokMediaVideoGenerationPreservesImageToVideoModel(t *testing.T)
|
||||
require.JSONEq(t, `{"model":"grok-imagine-video-1.5","prompt":"animate","image":{"image_url":"data:image/png;base64,aW1n"}}`, string(upstream.lastBody))
|
||||
require.Equal(t, "video-request-456", result.ResponseID)
|
||||
require.Equal(t, "grok-imagine-video-1.5", result.BillingModel)
|
||||
// 未指定 duration 时按上游默认 8 秒计费。
|
||||
require.Equal(t, VideoBillingDefaultDurationSeconds, result.VideoDurationSeconds)
|
||||
}
|
||||
|
||||
func TestForwardGrokMediaVideoStatusUsesGETWithoutBody(t *testing.T) {
|
||||
|
||||
@@ -1813,14 +1813,15 @@ func TestGrokVideoBillingUsesSeparateVideoRateMultiplier(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",
|
||||
ImageCount: 1,
|
||||
VideoCount: 1,
|
||||
VideoResolution: VideoBillingResolution480P,
|
||||
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,
|
||||
VideoDurationSeconds: 1,
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{
|
||||
ID: 10126,
|
||||
@@ -1851,6 +1852,11 @@ func TestGrokVideoBillingUsesSeparateVideoRateMultiplier(t *testing.T) {
|
||||
require.InDelta(t, 0.25, usageRepo.lastLog.RateMultiplier, 1e-12)
|
||||
require.NotNil(t, usageRepo.lastLog.BillingMode)
|
||||
require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode)
|
||||
require.Equal(t, 1, usageRepo.lastLog.VideoCount)
|
||||
require.NotNil(t, usageRepo.lastLog.VideoResolution)
|
||||
require.Equal(t, VideoBillingResolution480P, *usageRepo.lastLog.VideoResolution)
|
||||
require.NotNil(t, usageRepo.lastLog.VideoDurationSeconds)
|
||||
require.Equal(t, 1, *usageRepo.lastLog.VideoDurationSeconds)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_GrokVideoUsesDefaultRateCard(t *testing.T) {
|
||||
@@ -1885,11 +1891,15 @@ func TestOpenAIGatewayServiceRecordUsage_GrokVideoUsesDefaultRateCard(t *testing
|
||||
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)
|
||||
// 结果未携带 duration 时按上游默认 8 秒计费:0.14 USD/s × 8s。
|
||||
require.InDelta(t, 0.14*8, usageRepo.lastLog.TotalCost, 1e-12)
|
||||
require.InDelta(t, 0.14*8, 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)
|
||||
require.Equal(t, 1, usageRepo.lastLog.VideoCount)
|
||||
require.NotNil(t, usageRepo.lastLog.VideoDurationSeconds)
|
||||
require.Equal(t, VideoBillingDefaultDurationSeconds, *usageRepo.lastLog.VideoDurationSeconds)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_GroupImagePriceOverridesChannelImagePrice(t *testing.T) {
|
||||
@@ -1945,13 +1955,14 @@ func TestOpenAIGatewayServiceRecordUsage_GroupVideoPriceOverridesChannelImagePri
|
||||
|
||||
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,
|
||||
RequestID: "resp_grok_video_group_price",
|
||||
Model: "grok-imagine-video",
|
||||
BillingModel: "grok-imagine-video",
|
||||
ImageCount: 1,
|
||||
VideoCount: 1,
|
||||
VideoResolution: VideoBillingResolution720P,
|
||||
VideoDurationSeconds: 1,
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{
|
||||
ID: 10128,
|
||||
@@ -2046,13 +2057,14 @@ func TestOpenAIGatewayServiceRecordUsage_HydratesGroupVideoPriceWhenAuthSnapshot
|
||||
|
||||
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,
|
||||
RequestID: "resp_grok_video_hydrated_price",
|
||||
Model: "grok-imagine-video",
|
||||
BillingModel: "grok-imagine-video",
|
||||
ImageCount: 1,
|
||||
VideoCount: 1,
|
||||
VideoResolution: VideoBillingResolution720P,
|
||||
VideoDurationSeconds: 1,
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{
|
||||
ID: 10131,
|
||||
@@ -2075,6 +2087,53 @@ func TestOpenAIGatewayServiceRecordUsage_HydratesGroupVideoPriceWhenAuthSnapshot
|
||||
require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode)
|
||||
}
|
||||
|
||||
// 视频请求命中渠道 token 计费时走 token 路径;此时行是 billing_mode='token'、image_count=1、
|
||||
// image_size=NULL,必须携带 video_count>0 才能通过 usage_logs 的 image_size check 约束
|
||||
// (迁移 172),否则整个计费事务会因约束违反而丢失。
|
||||
func TestOpenAIGatewayServiceRecordUsage_GrokVideoWithTokenChannelPricingKeepsVideoMetadata(t *testing.T) {
|
||||
groupID := int64(132)
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil)
|
||||
svc.resolver = newOpenAITokenImageChannelPricingResolverForTest(t, groupID, "grok-imagine-video")
|
||||
|
||||
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
|
||||
Result: &OpenAIForwardResult{
|
||||
RequestID: "resp_grok_video_token_channel",
|
||||
Model: "grok-imagine-video",
|
||||
BillingModel: "grok-imagine-video",
|
||||
ImageCount: 1,
|
||||
VideoCount: 1,
|
||||
VideoResolution: VideoBillingResolution720P,
|
||||
VideoDurationSeconds: 5,
|
||||
Usage: OpenAIUsage{InputTokens: 100, OutputTokens: 200},
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{
|
||||
ID: 10132,
|
||||
GroupID: i64p(groupID),
|
||||
Group: &Group{
|
||||
ID: groupID,
|
||||
Platform: PlatformGrok,
|
||||
RateMultiplier: 1,
|
||||
},
|
||||
},
|
||||
User: &User{ID: 20132},
|
||||
Account: &Account{ID: 30132, Platform: PlatformGrok},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, usageRepo.lastLog)
|
||||
require.NotNil(t, usageRepo.lastLog.BillingMode)
|
||||
require.Equal(t, string(BillingModeToken), *usageRepo.lastLog.BillingMode)
|
||||
require.Nil(t, usageRepo.lastLog.ImageSize)
|
||||
require.Equal(t, 1, usageRepo.lastLog.ImageCount)
|
||||
require.Equal(t, 1, usageRepo.lastLog.VideoCount)
|
||||
require.NotNil(t, usageRepo.lastLog.VideoResolution)
|
||||
require.Equal(t, VideoBillingResolution720P, *usageRepo.lastLog.VideoResolution)
|
||||
require.NotNil(t, usageRepo.lastLog.VideoDurationSeconds)
|
||||
require.Equal(t, 5, *usageRepo.lastLog.VideoDurationSeconds)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_ChannelImageBillingUsesImageCountAndSharedMultiplier(t *testing.T) {
|
||||
groupID := int64(123)
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
|
||||
@@ -244,6 +244,8 @@ type OpenAIForwardResult struct {
|
||||
ImageSizeBreakdown map[string]int
|
||||
VideoCount int
|
||||
VideoResolution string
|
||||
// VideoDurationSeconds 是提交时请求的生成时长(xAI 按输出秒数计费),已归一化到 1-15 秒。
|
||||
VideoDurationSeconds int
|
||||
|
||||
wsReplayInput []json.RawMessage
|
||||
wsReplayInputExists bool
|
||||
|
||||
@@ -243,6 +243,12 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
ImageSizeBreakdown: result.ImageSizeBreakdown,
|
||||
}
|
||||
isVideoUsage := isGrokVideoUsageResult(result, billingModels)
|
||||
if isVideoUsage {
|
||||
usageLog.VideoCount = result.VideoCount
|
||||
usageLog.VideoResolution = optionalTrimmedStringPtr(NormalizeVideoBillingResolutionOrDefault(result.VideoResolution))
|
||||
videoDurationSeconds := NormalizeVideoBillingDurationSecondsOrDefault(result.VideoDurationSeconds)
|
||||
usageLog.VideoDurationSeconds = &videoDurationSeconds
|
||||
}
|
||||
if cost != nil {
|
||||
usageLog.InputCost = cost.InputCost
|
||||
usageLog.OutputCost = cost.OutputCost
|
||||
@@ -499,19 +505,21 @@ func (s *OpenAIGatewayService) calculateOpenAIVideoCost(
|
||||
videoCount = 1
|
||||
}
|
||||
resolution := NormalizeVideoBillingResolutionOrDefault(result.VideoResolution)
|
||||
durationSeconds := NormalizeVideoBillingDurationSecondsOrDefault(result.VideoDurationSeconds)
|
||||
groupConfig := videoPriceConfigFromAPIKey(apiKey)
|
||||
if apiKeyHasConfiguredVideoPrice(apiKey, resolution) {
|
||||
return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, groupConfig, multiplier)
|
||||
return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, durationSeconds, 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.CalculateVideoCost(billingModel, resolution, videoCount, durationSeconds, groupConfig, multiplier)
|
||||
}
|
||||
}
|
||||
if resolved := s.resolveOpenAIChannelPricing(ctx, billingModel, apiKey); resolved != nil &&
|
||||
(resolved.Mode == BillingModePerRequest || resolved.Mode == BillingModeImage) {
|
||||
// 渠道 per_request/image 定价保持"按请求次数"口径(价格由管理员按次配置),不乘视频时长。
|
||||
gid := apiKey.Group.ID
|
||||
cost, err := s.billingService.CalculateCostUnified(CostInput{
|
||||
Ctx: ctx,
|
||||
@@ -530,13 +538,16 @@ func (s *OpenAIGatewayService) calculateOpenAIVideoCost(
|
||||
logger.LegacyPrintf("service.openai_gateway", "Calculate video channel cost failed: %v", err)
|
||||
}
|
||||
|
||||
return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, groupConfig, multiplier)
|
||||
return s.billingService.CalculateVideoCost(billingModel, resolution, videoCount, durationSeconds, groupConfig, multiplier)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) apiKeyWithFreshGroupMediaPricing(ctx context.Context, apiKey *APIKey) *APIKey {
|
||||
if apiKey == nil || apiKey.GroupID == nil || *apiKey.GroupID <= 0 {
|
||||
return apiKey
|
||||
}
|
||||
if !groupMediaPricingLooksIncomplete(apiKey.Group) {
|
||||
return apiKey
|
||||
}
|
||||
if s == nil || s.channelService == nil || s.channelService.groupRepo == nil {
|
||||
return apiKey
|
||||
}
|
||||
@@ -549,6 +560,24 @@ func (s *OpenAIGatewayService) apiKeyWithFreshGroupMediaPricing(ctx context.Cont
|
||||
return &clone
|
||||
}
|
||||
|
||||
// groupMediaPricingLooksIncomplete 判断分组对象是否可能缺失媒体计费字段(例如由不含
|
||||
// 这些字段的旧快照或手工构造的上下文对象生成)。image/video 独立倍率在数据库中的
|
||||
// 默认值均为 1.0,正常加载的分组不可能两个倍率同时为 0 且未开启独立倍率、全部媒体
|
||||
// 价为 nil——只有这种情况才回源查库,避免对未配置覆盖价的分组每条媒体用量都多打一次 DB 查询。
|
||||
func groupMediaPricingLooksIncomplete(group *Group) bool {
|
||||
if group == nil {
|
||||
return true
|
||||
}
|
||||
if group.ImageRateIndependent || group.VideoRateIndependent {
|
||||
return false
|
||||
}
|
||||
if group.ImageRateMultiplier != 0 || group.VideoRateMultiplier != 0 {
|
||||
return false
|
||||
}
|
||||
return group.ImagePrice1K == nil && group.ImagePrice2K == nil && group.ImagePrice4K == nil &&
|
||||
group.VideoPrice480P == nil && group.VideoPrice720P == nil && group.VideoPrice1080P == nil
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) resolveOpenAIChannelPricing(ctx context.Context, billingModel string, apiKey *APIKey) *ResolvedPricing {
|
||||
if s.resolver == nil || apiKey == nil || apiKey.Group == nil {
|
||||
return nil
|
||||
|
||||
@@ -175,6 +175,11 @@ type UsageLog struct {
|
||||
ImageSizeBreakdown map[string]int
|
||||
MediaType *string
|
||||
|
||||
// 视频生成字段(Grok 视频按秒计费;video_count>0 的行不要求 image_size)
|
||||
VideoCount int
|
||||
VideoResolution *string
|
||||
VideoDurationSeconds *int
|
||||
|
||||
CreatedAt time.Time
|
||||
|
||||
User *User
|
||||
|
||||
@@ -8,6 +8,29 @@ const (
|
||||
VideoBillingResolution1080P = "1080p"
|
||||
)
|
||||
|
||||
// xAI 视频生成按秒计费,duration 请求参数允许 1-15 秒;未指定时上游默认生成 8 秒。
|
||||
// 计费时长必须与上游实际消耗对齐,否则用户可通过拉长 duration 套利(提交时长由用户控制)。
|
||||
const (
|
||||
VideoBillingMinDurationSeconds = 1
|
||||
VideoBillingMaxDurationSeconds = 15
|
||||
VideoBillingDefaultDurationSeconds = 8
|
||||
)
|
||||
|
||||
// NormalizeVideoBillingDurationSecondsOrDefault 归一化计费用视频时长:
|
||||
// 未指定(<=0)按上游默认 8 秒计,超出上游允许区间按边界收敛。
|
||||
func NormalizeVideoBillingDurationSecondsOrDefault(durationSeconds int) int {
|
||||
if durationSeconds <= 0 {
|
||||
return VideoBillingDefaultDurationSeconds
|
||||
}
|
||||
if durationSeconds < VideoBillingMinDurationSeconds {
|
||||
return VideoBillingMinDurationSeconds
|
||||
}
|
||||
if durationSeconds > VideoBillingMaxDurationSeconds {
|
||||
return VideoBillingMaxDurationSeconds
|
||||
}
|
||||
return durationSeconds
|
||||
}
|
||||
|
||||
func NormalizeVideoBillingResolutionOrDefault(resolution string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(resolution)) {
|
||||
case "480", "480p", "sd":
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
-- Grok video billing is per second of generated output (xAI rate card), so usage
|
||||
-- rows must record the billed resolution and duration for auditability. The
|
||||
-- image-size check constraint must also exempt any video row by video_count
|
||||
-- instead of billing_mode='video' alone: a video request billed through a
|
||||
-- token-mode channel price produces billing_mode='token' with image_count=1
|
||||
-- (legacy media counter) and no image_size, which the previous constraint
|
||||
-- rejected and silently dropped the whole billing transaction.
|
||||
|
||||
ALTER TABLE usage_logs
|
||||
ADD COLUMN IF NOT EXISTS video_count INTEGER NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS video_resolution VARCHAR(10),
|
||||
ADD COLUMN IF NOT EXISTS video_duration_seconds INTEGER;
|
||||
|
||||
COMMENT ON COLUMN usage_logs.video_count IS '视频生成数量;>0 表示本行是视频生成用量';
|
||||
COMMENT ON COLUMN usage_logs.video_resolution IS '计费用视频分辨率 480p/720p/1080p';
|
||||
COMMENT ON COLUMN usage_logs.video_duration_seconds IS '提交时请求的视频时长(秒),按秒计费的乘数';
|
||||
|
||||
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 COALESCE(video_count, 0) > 0
|
||||
OR (
|
||||
image_size IS NOT NULL
|
||||
AND image_size IN ('1K', '2K', '4K', 'mixed')
|
||||
)
|
||||
) NOT VALID;
|
||||
|
||||
-- Group video prices are per-second rates (USD/s), matching the xAI rate card;
|
||||
-- total cost = per-second price x duration seconds. Clarify the column docs
|
||||
-- introduced by migration 170, which read as per-video prices.
|
||||
COMMENT ON COLUMN groups.video_price_480p IS '480p 视频生成每秒单价 (USD/s),Grok 平台使用';
|
||||
COMMENT ON COLUMN groups.video_price_720p IS '720p 视频生成每秒单价 (USD/s),Grok 平台使用';
|
||||
COMMENT ON COLUMN groups.video_price_1080p IS '1080p 视频生成每秒单价 (USD/s),Grok 平台使用';
|
||||
@@ -841,26 +841,15 @@ 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.',
|
||||
description:
|
||||
'Configure Grok video generation prices in USD per second of output video. Leave empty to use the default per-second rates (grok-imagine-video: $0.05/s 480p, $0.07/s 720p; video-1.5: $0.08/s 480p, $0.14/s 720p, $0.25/s 1080p).',
|
||||
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',
|
||||
'Videos are billed per second: per-second price × duration (1-15s, default 8s). By default the current effective group multiplier applies; independent mode uses the video multiplier instead.',
|
||||
finalPricePreview: 'Final per-second price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
peakRate: {
|
||||
|
||||
@@ -919,25 +919,15 @@ export default {
|
||||
finalPricePreview: '最终单张价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
mediaPricing: {
|
||||
title: '图片/视频生成计费',
|
||||
description: '配置 Grok 图片和视频生成能力及媒体基础单价,留空则使用默认价格',
|
||||
allowImageGeneration: '允许当前分组生图和视频生成',
|
||||
independentMultiplier: '媒体倍率独立',
|
||||
imageMultiplier: '媒体独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,Grok 媒体费用 = 媒体价格 × 当前分组有效倍率;开启独立倍率后,Grok 媒体费用 = 媒体价格 × 媒体独立倍率。一次视频生成按 1 个媒体单位计费。',
|
||||
finalPricePreview: '最终单次媒体价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
videoPricing: {
|
||||
title: '视频生成计费',
|
||||
description: '配置 Grok 视频生成基础单价,留空则使用默认视频价格',
|
||||
description:
|
||||
'配置 Grok 视频生成的每秒单价(USD/秒),留空则使用默认每秒价(grok-imagine-video:480p $0.05/s、720p $0.07/s;video-1.5:480p $0.08/s、720p $0.14/s、1080p $0.25/s)',
|
||||
independentMultiplier: '视频倍率独立',
|
||||
videoMultiplier: '视频独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,视频费用 = 视频价格 × 当前分组有效倍率;开启独立倍率后,视频费用 = 视频价格 × 视频独立倍率。',
|
||||
finalPricePreview: '最终单次视频价格预览',
|
||||
'视频按秒计费:费用 = 每秒价格 × 时长(1-15 秒,未指定默认 8 秒)。默认叠加当前分组有效倍率;开启独立倍率后改用视频独立倍率。',
|
||||
finalPricePreview: '最终每秒价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
peakRate: {
|
||||
|
||||
@@ -1010,7 +1010,7 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<label class="input-label">480p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_480p"
|
||||
type="number"
|
||||
@@ -1021,7 +1021,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<label class="input-label">720p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_720p"
|
||||
type="number"
|
||||
@@ -1032,7 +1032,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<label class="input-label">1080p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_1080p"
|
||||
type="number"
|
||||
@@ -2489,7 +2489,7 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<label class="input-label">480p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_480p"
|
||||
type="number"
|
||||
@@ -2500,7 +2500,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<label class="input-label">720p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_720p"
|
||||
type="number"
|
||||
@@ -2511,7 +2511,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<label class="input-label">1080p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_1080p"
|
||||
type="number"
|
||||
@@ -4718,6 +4718,14 @@ const handleCreateGroup = async () => {
|
||||
requestData.video_rate_multiplier = normalizeRateMultiplier(
|
||||
requestData.video_rate_multiplier,
|
||||
);
|
||||
// 媒体价格输入清空时 v-model.number 产生 "",直接提交会被后端 *float64 反序列化拒绝(400),
|
||||
// 创建时按"未配置"(null)处理。
|
||||
requestData.image_price_1k = emptyToNull(requestData.image_price_1k);
|
||||
requestData.image_price_2k = emptyToNull(requestData.image_price_2k);
|
||||
requestData.image_price_4k = emptyToNull(requestData.image_price_4k);
|
||||
requestData.video_price_480p = emptyToNull(requestData.video_price_480p);
|
||||
requestData.video_price_720p = emptyToNull(requestData.video_price_720p);
|
||||
requestData.video_price_1080p = emptyToNull(requestData.video_price_1080p);
|
||||
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
|
||||
requestData.peak_start = createForm.peak_start;
|
||||
requestData.peak_end = createForm.peak_end;
|
||||
@@ -4896,6 +4904,16 @@ const handleUpdateGroup = async () => {
|
||||
payload.video_rate_multiplier = normalizeRateMultiplier(
|
||||
payload.video_rate_multiplier,
|
||||
);
|
||||
// 媒体价格输入清空时 v-model.number 产生 "",直接提交会被后端 *float64 反序列化拒绝(400)。
|
||||
// 更新语义中 null 表示"不修改",因此清空后的字段发送 -1:后端 normalizePrice 将负价归一为
|
||||
// NULL,从而真正清除已配置的价格。
|
||||
const emptyPriceToClear = (v: any) => (v === "" || v === null ? -1 : v);
|
||||
payload.image_price_1k = emptyPriceToClear(payload.image_price_1k);
|
||||
payload.image_price_2k = emptyPriceToClear(payload.image_price_2k);
|
||||
payload.image_price_4k = emptyPriceToClear(payload.image_price_4k);
|
||||
payload.video_price_480p = emptyPriceToClear(payload.video_price_480p);
|
||||
payload.video_price_720p = emptyPriceToClear(payload.video_price_720p);
|
||||
payload.video_price_1080p = emptyPriceToClear(payload.video_price_1080p);
|
||||
payload.peak_rate_enabled = editForm.peak_rate_enabled;
|
||||
payload.peak_start = editForm.peak_start;
|
||||
payload.peak_end = editForm.peak_end;
|
||||
|
||||
@@ -37,8 +37,10 @@ describe("groups image pricing platform support", () => {
|
||||
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");
|
||||
// 视频 placeholder 为每秒单价:480p/720p 取 grok-imagine-video 官方每秒价,
|
||||
// 1080p 仅 video-1.5 支持、取 1.5 每秒价。
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_480p")).toBe("0.05");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_720p")).toBe("0.07");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_1080p")).toBe("0.25");
|
||||
});
|
||||
|
||||
|
||||
@@ -39,13 +39,15 @@ const defaultImagePricePlaceholders: Record<
|
||||
},
|
||||
};
|
||||
|
||||
// 视频价为每秒单价(USD/s)。480p/720p 取 grok-imagine-video(文生视频实际走该模型)的
|
||||
// 官方每秒价;1080p 仅 grok-imagine-video-1.5 图生视频支持,取 1.5 的每秒价。
|
||||
const defaultVideoPricePlaceholders: Record<
|
||||
string,
|
||||
Record<VideoPricingTierKey, string>
|
||||
> = {
|
||||
grok: {
|
||||
video_price_480p: "0.08",
|
||||
video_price_720p: "0.14",
|
||||
video_price_480p: "0.05",
|
||||
video_price_720p: "0.07",
|
||||
video_price_1080p: "0.25",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user