diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index d0e43bf3..8ef8d3a7 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -371,6 +371,7 @@ var ( {Name: "stream", Type: field.TypeBool, Default: false}, {Name: "duration_ms", Type: field.TypeInt, Nullable: true}, {Name: "first_token_ms", Type: field.TypeInt, Nullable: true}, + {Name: "user_agent", Type: field.TypeString, Nullable: true, Size: 512}, {Name: "image_count", Type: field.TypeInt, Default: 0}, {Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10}, {Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, @@ -388,31 +389,31 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "usage_logs_api_keys_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[23]}, + Columns: []*schema.Column{UsageLogsColumns[24]}, RefColumns: []*schema.Column{APIKeysColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_accounts_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[24]}, + Columns: []*schema.Column{UsageLogsColumns[25]}, RefColumns: []*schema.Column{AccountsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_groups_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[25]}, + Columns: []*schema.Column{UsageLogsColumns[26]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "usage_logs_users_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[26]}, + Columns: []*schema.Column{UsageLogsColumns[27]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_user_subscriptions_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[27]}, + Columns: []*schema.Column{UsageLogsColumns[28]}, RefColumns: []*schema.Column{UserSubscriptionsColumns[0]}, OnDelete: schema.SetNull, }, @@ -421,32 +422,32 @@ var ( { Name: "usagelog_user_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[26]}, + Columns: []*schema.Column{UsageLogsColumns[27]}, }, { Name: "usagelog_api_key_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[23]}, + Columns: []*schema.Column{UsageLogsColumns[24]}, }, { Name: "usagelog_account_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[24]}, + Columns: []*schema.Column{UsageLogsColumns[25]}, }, { Name: "usagelog_group_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[25]}, + Columns: []*schema.Column{UsageLogsColumns[26]}, }, { Name: "usagelog_subscription_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[27]}, + Columns: []*schema.Column{UsageLogsColumns[28]}, }, { Name: "usagelog_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[22]}, + Columns: []*schema.Column{UsageLogsColumns[23]}, }, { Name: "usagelog_model", @@ -461,12 +462,12 @@ var ( { Name: "usagelog_user_id_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[26], UsageLogsColumns[22]}, + Columns: []*schema.Column{UsageLogsColumns[27], UsageLogsColumns[23]}, }, { Name: "usagelog_api_key_id_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[23], UsageLogsColumns[22]}, + Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[23]}, }, }, } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 91883413..d335a064 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -8107,6 +8107,7 @@ type UsageLogMutation struct { addduration_ms *int first_token_ms *int addfirst_token_ms *int + user_agent *string image_count *int addimage_count *int image_size *string @@ -9463,6 +9464,55 @@ func (m *UsageLogMutation) ResetFirstTokenMs() { delete(m.clearedFields, usagelog.FieldFirstTokenMs) } +// SetUserAgent sets the "user_agent" field. +func (m *UsageLogMutation) SetUserAgent(s string) { + m.user_agent = &s +} + +// UserAgent returns the value of the "user_agent" field in the mutation. +func (m *UsageLogMutation) UserAgent() (r string, exists bool) { + v := m.user_agent + if v == nil { + return + } + return *v, true +} + +// OldUserAgent returns the old "user_agent" 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) OldUserAgent(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUserAgent is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUserAgent requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUserAgent: %w", err) + } + return oldValue.UserAgent, nil +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (m *UsageLogMutation) ClearUserAgent() { + m.user_agent = nil + m.clearedFields[usagelog.FieldUserAgent] = struct{}{} +} + +// UserAgentCleared returns if the "user_agent" field was cleared in this mutation. +func (m *UsageLogMutation) UserAgentCleared() bool { + _, ok := m.clearedFields[usagelog.FieldUserAgent] + return ok +} + +// ResetUserAgent resets all changes to the "user_agent" field. +func (m *UsageLogMutation) ResetUserAgent() { + m.user_agent = nil + delete(m.clearedFields, usagelog.FieldUserAgent) +} + // SetImageCount sets the "image_count" field. func (m *UsageLogMutation) SetImageCount(i int) { m.image_count = &i @@ -9773,7 +9823,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, 27) + fields := make([]string, 0, 28) if m.user != nil { fields = append(fields, usagelog.FieldUserID) } @@ -9846,6 +9896,9 @@ func (m *UsageLogMutation) Fields() []string { if m.first_token_ms != nil { fields = append(fields, usagelog.FieldFirstTokenMs) } + if m.user_agent != nil { + fields = append(fields, usagelog.FieldUserAgent) + } if m.image_count != nil { fields = append(fields, usagelog.FieldImageCount) } @@ -9911,6 +9964,8 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.DurationMs() case usagelog.FieldFirstTokenMs: return m.FirstTokenMs() + case usagelog.FieldUserAgent: + return m.UserAgent() case usagelog.FieldImageCount: return m.ImageCount() case usagelog.FieldImageSize: @@ -9974,6 +10029,8 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldDurationMs(ctx) case usagelog.FieldFirstTokenMs: return m.OldFirstTokenMs(ctx) + case usagelog.FieldUserAgent: + return m.OldUserAgent(ctx) case usagelog.FieldImageCount: return m.OldImageCount(ctx) case usagelog.FieldImageSize: @@ -10157,6 +10214,13 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetFirstTokenMs(v) return nil + case usagelog.FieldUserAgent: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUserAgent(v) + return nil case usagelog.FieldImageCount: v, ok := value.(int) if !ok { @@ -10427,6 +10491,9 @@ func (m *UsageLogMutation) ClearedFields() []string { if m.FieldCleared(usagelog.FieldFirstTokenMs) { fields = append(fields, usagelog.FieldFirstTokenMs) } + if m.FieldCleared(usagelog.FieldUserAgent) { + fields = append(fields, usagelog.FieldUserAgent) + } if m.FieldCleared(usagelog.FieldImageSize) { fields = append(fields, usagelog.FieldImageSize) } @@ -10456,6 +10523,9 @@ func (m *UsageLogMutation) ClearField(name string) error { case usagelog.FieldFirstTokenMs: m.ClearFirstTokenMs() return nil + case usagelog.FieldUserAgent: + m.ClearUserAgent() + return nil case usagelog.FieldImageSize: m.ClearImageSize() return nil @@ -10539,6 +10609,9 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldFirstTokenMs: m.ResetFirstTokenMs() return nil + case usagelog.FieldUserAgent: + m.ResetUserAgent() + return nil case usagelog.FieldImageCount: m.ResetImageCount() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index e2cb6a3c..34b811a7 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -521,16 +521,20 @@ func init() { usagelogDescStream := usagelogFields[21].Descriptor() // usagelog.DefaultStream holds the default value on creation for the stream field. usagelog.DefaultStream = usagelogDescStream.Default.(bool) + // usagelogDescUserAgent is the schema descriptor for user_agent field. + usagelogDescUserAgent := usagelogFields[24].Descriptor() + // usagelog.UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save. + usagelog.UserAgentValidator = usagelogDescUserAgent.Validators[0].(func(string) error) // usagelogDescImageCount is the schema descriptor for image_count field. - usagelogDescImageCount := usagelogFields[24].Descriptor() + usagelogDescImageCount := usagelogFields[25].Descriptor() // usagelog.DefaultImageCount holds the default value on creation for the image_count field. usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int) // usagelogDescImageSize is the schema descriptor for image_size field. - usagelogDescImageSize := usagelogFields[25].Descriptor() + usagelogDescImageSize := usagelogFields[26].Descriptor() // usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save. usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error) // usagelogDescCreatedAt is the schema descriptor for created_at field. - usagelogDescCreatedAt := usagelogFields[26].Descriptor() + usagelogDescCreatedAt := usagelogFields[27].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() diff --git a/backend/ent/schema/usage_log.go b/backend/ent/schema/usage_log.go index af99904d..df955181 100644 --- a/backend/ent/schema/usage_log.go +++ b/backend/ent/schema/usage_log.go @@ -96,6 +96,10 @@ func (UsageLog) Fields() []ent.Field { field.Int("first_token_ms"). Optional(). Nillable(), + field.String("user_agent"). + MaxLen(512). + Optional(). + Nillable(), // 图片生成字段(仅 gemini-3-pro-image 等图片模型使用) field.Int("image_count"). diff --git a/backend/ent/usagelog.go b/backend/ent/usagelog.go index 35cd337f..798f3a9f 100644 --- a/backend/ent/usagelog.go +++ b/backend/ent/usagelog.go @@ -70,6 +70,8 @@ type UsageLog struct { DurationMs *int `json:"duration_ms,omitempty"` // FirstTokenMs holds the value of the "first_token_ms" field. FirstTokenMs *int `json:"first_token_ms,omitempty"` + // UserAgent holds the value of the "user_agent" field. + UserAgent *string `json:"user_agent,omitempty"` // ImageCount holds the value of the "image_count" field. ImageCount int `json:"image_count,omitempty"` // ImageSize holds the value of the "image_size" field. @@ -165,7 +167,7 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullFloat64) case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount: values[i] = new(sql.NullInt64) - case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldImageSize: + case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldUserAgent, usagelog.FieldImageSize: values[i] = new(sql.NullString) case usagelog.FieldCreatedAt: values[i] = new(sql.NullTime) @@ -338,6 +340,13 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error { _m.FirstTokenMs = new(int) *_m.FirstTokenMs = int(value.Int64) } + case usagelog.FieldUserAgent: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field user_agent", values[i]) + } else if value.Valid { + _m.UserAgent = new(string) + *_m.UserAgent = value.String + } case usagelog.FieldImageCount: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field image_count", values[i]) @@ -498,6 +507,11 @@ func (_m *UsageLog) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.UserAgent; v != nil { + builder.WriteString("user_agent=") + builder.WriteString(*v) + } + builder.WriteString(", ") builder.WriteString("image_count=") builder.WriteString(fmt.Sprintf("%v", _m.ImageCount)) builder.WriteString(", ") diff --git a/backend/ent/usagelog/usagelog.go b/backend/ent/usagelog/usagelog.go index bc0cedc8..d3edfb4d 100644 --- a/backend/ent/usagelog/usagelog.go +++ b/backend/ent/usagelog/usagelog.go @@ -62,6 +62,8 @@ const ( FieldDurationMs = "duration_ms" // FieldFirstTokenMs holds the string denoting the first_token_ms field in the database. FieldFirstTokenMs = "first_token_ms" + // FieldUserAgent holds the string denoting the user_agent field in the database. + FieldUserAgent = "user_agent" // FieldImageCount holds the string denoting the image_count field in the database. FieldImageCount = "image_count" // FieldImageSize holds the string denoting the image_size field in the database. @@ -144,6 +146,7 @@ var Columns = []string{ FieldStream, FieldDurationMs, FieldFirstTokenMs, + FieldUserAgent, FieldImageCount, FieldImageSize, FieldCreatedAt, @@ -194,6 +197,8 @@ var ( DefaultBillingType int8 // DefaultStream holds the default value on creation for the "stream" field. DefaultStream bool + // UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save. + UserAgentValidator func(string) error // DefaultImageCount holds the default value on creation for the "image_count" field. DefaultImageCount int // ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save. @@ -330,6 +335,11 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldFirstTokenMs, opts...).ToFunc() } +// ByUserAgent orders the results by the user_agent field. +func ByUserAgent(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUserAgent, opts...).ToFunc() +} + // ByImageCount orders the results by the image_count field. func ByImageCount(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldImageCount, opts...).ToFunc() diff --git a/backend/ent/usagelog/where.go b/backend/ent/usagelog/where.go index 7d9edae1..c7acd59d 100644 --- a/backend/ent/usagelog/where.go +++ b/backend/ent/usagelog/where.go @@ -175,6 +175,11 @@ func FirstTokenMs(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldFirstTokenMs, v)) } +// UserAgent applies equality check predicate on the "user_agent" field. It's identical to UserAgentEQ. +func UserAgent(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v)) +} + // ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ. func ImageCount(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v)) @@ -1110,6 +1115,81 @@ func FirstTokenMsNotNil() predicate.UsageLog { return predicate.UsageLog(sql.FieldNotNull(FieldFirstTokenMs)) } +// UserAgentEQ applies the EQ predicate on the "user_agent" field. +func UserAgentEQ(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldUserAgent, v)) +} + +// UserAgentNEQ applies the NEQ predicate on the "user_agent" field. +func UserAgentNEQ(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNEQ(FieldUserAgent, v)) +} + +// UserAgentIn applies the In predicate on the "user_agent" field. +func UserAgentIn(vs ...string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldIn(FieldUserAgent, vs...)) +} + +// UserAgentNotIn applies the NotIn predicate on the "user_agent" field. +func UserAgentNotIn(vs ...string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNotIn(FieldUserAgent, vs...)) +} + +// UserAgentGT applies the GT predicate on the "user_agent" field. +func UserAgentGT(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGT(FieldUserAgent, v)) +} + +// UserAgentGTE applies the GTE predicate on the "user_agent" field. +func UserAgentGTE(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGTE(FieldUserAgent, v)) +} + +// UserAgentLT applies the LT predicate on the "user_agent" field. +func UserAgentLT(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLT(FieldUserAgent, v)) +} + +// UserAgentLTE applies the LTE predicate on the "user_agent" field. +func UserAgentLTE(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLTE(FieldUserAgent, v)) +} + +// UserAgentContains applies the Contains predicate on the "user_agent" field. +func UserAgentContains(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldContains(FieldUserAgent, v)) +} + +// UserAgentHasPrefix applies the HasPrefix predicate on the "user_agent" field. +func UserAgentHasPrefix(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldHasPrefix(FieldUserAgent, v)) +} + +// UserAgentHasSuffix applies the HasSuffix predicate on the "user_agent" field. +func UserAgentHasSuffix(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldHasSuffix(FieldUserAgent, v)) +} + +// UserAgentIsNil applies the IsNil predicate on the "user_agent" field. +func UserAgentIsNil() predicate.UsageLog { + return predicate.UsageLog(sql.FieldIsNull(FieldUserAgent)) +} + +// UserAgentNotNil applies the NotNil predicate on the "user_agent" field. +func UserAgentNotNil() predicate.UsageLog { + return predicate.UsageLog(sql.FieldNotNull(FieldUserAgent)) +} + +// UserAgentEqualFold applies the EqualFold predicate on the "user_agent" field. +func UserAgentEqualFold(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEqualFold(FieldUserAgent, v)) +} + +// UserAgentContainsFold applies the ContainsFold predicate on the "user_agent" field. +func UserAgentContainsFold(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldContainsFold(FieldUserAgent, v)) +} + // ImageCountEQ applies the EQ predicate on the "image_count" field. func ImageCountEQ(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v)) diff --git a/backend/ent/usagelog_create.go b/backend/ent/usagelog_create.go index ef4a9ca2..f77650ab 100644 --- a/backend/ent/usagelog_create.go +++ b/backend/ent/usagelog_create.go @@ -323,6 +323,20 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate { return _c } +// SetUserAgent sets the "user_agent" field. +func (_c *UsageLogCreate) SetUserAgent(v string) *UsageLogCreate { + _c.mutation.SetUserAgent(v) + return _c +} + +// SetNillableUserAgent sets the "user_agent" field if the given value is not nil. +func (_c *UsageLogCreate) SetNillableUserAgent(v *string) *UsageLogCreate { + if v != nil { + _c.SetUserAgent(*v) + } + return _c +} + // SetImageCount sets the "image_count" field. func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate { _c.mutation.SetImageCount(v) @@ -567,6 +581,11 @@ func (_c *UsageLogCreate) check() error { if _, ok := _c.mutation.Stream(); !ok { return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)} } + if v, ok := _c.mutation.UserAgent(); ok { + if err := usagelog.UserAgentValidator(v); err != nil { + return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)} + } + } if _, ok := _c.mutation.ImageCount(); !ok { return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)} } @@ -690,6 +709,10 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, value) _node.FirstTokenMs = &value } + if value, ok := _c.mutation.UserAgent(); ok { + _spec.SetField(usagelog.FieldUserAgent, field.TypeString, value) + _node.UserAgent = &value + } if value, ok := _c.mutation.ImageCount(); ok { _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) _node.ImageCount = value @@ -1247,6 +1270,24 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert { return u } +// SetUserAgent sets the "user_agent" field. +func (u *UsageLogUpsert) SetUserAgent(v string) *UsageLogUpsert { + u.Set(usagelog.FieldUserAgent, v) + return u +} + +// UpdateUserAgent sets the "user_agent" field to the value that was provided on create. +func (u *UsageLogUpsert) UpdateUserAgent() *UsageLogUpsert { + u.SetExcluded(usagelog.FieldUserAgent) + return u +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (u *UsageLogUpsert) ClearUserAgent() *UsageLogUpsert { + u.SetNull(usagelog.FieldUserAgent) + return u +} + // SetImageCount sets the "image_count" field. func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert { u.Set(usagelog.FieldImageCount, v) @@ -1804,6 +1845,27 @@ func (u *UsageLogUpsertOne) ClearFirstTokenMs() *UsageLogUpsertOne { }) } +// SetUserAgent sets the "user_agent" field. +func (u *UsageLogUpsertOne) SetUserAgent(v string) *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.SetUserAgent(v) + }) +} + +// UpdateUserAgent sets the "user_agent" field to the value that was provided on create. +func (u *UsageLogUpsertOne) UpdateUserAgent() *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateUserAgent() + }) +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (u *UsageLogUpsertOne) ClearUserAgent() *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.ClearUserAgent() + }) +} + // SetImageCount sets the "image_count" field. func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne { return u.Update(func(s *UsageLogUpsert) { @@ -2533,6 +2595,27 @@ func (u *UsageLogUpsertBulk) ClearFirstTokenMs() *UsageLogUpsertBulk { }) } +// SetUserAgent sets the "user_agent" field. +func (u *UsageLogUpsertBulk) SetUserAgent(v string) *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.SetUserAgent(v) + }) +} + +// UpdateUserAgent sets the "user_agent" field to the value that was provided on create. +func (u *UsageLogUpsertBulk) UpdateUserAgent() *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateUserAgent() + }) +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (u *UsageLogUpsertBulk) ClearUserAgent() *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.ClearUserAgent() + }) +} + // SetImageCount sets the "image_count" field. func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk { return u.Update(func(s *UsageLogUpsert) { diff --git a/backend/ent/usagelog_update.go b/backend/ent/usagelog_update.go index 7eb2132b..2e77eef7 100644 --- a/backend/ent/usagelog_update.go +++ b/backend/ent/usagelog_update.go @@ -504,6 +504,26 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate { return _u } +// SetUserAgent sets the "user_agent" field. +func (_u *UsageLogUpdate) SetUserAgent(v string) *UsageLogUpdate { + _u.mutation.SetUserAgent(v) + return _u +} + +// SetNillableUserAgent sets the "user_agent" field if the given value is not nil. +func (_u *UsageLogUpdate) SetNillableUserAgent(v *string) *UsageLogUpdate { + if v != nil { + _u.SetUserAgent(*v) + } + return _u +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (_u *UsageLogUpdate) ClearUserAgent() *UsageLogUpdate { + _u.mutation.ClearUserAgent() + return _u +} + // SetImageCount sets the "image_count" field. func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate { _u.mutation.ResetImageCount() @@ -644,6 +664,11 @@ func (_u *UsageLogUpdate) check() error { return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)} } } + if v, ok := _u.mutation.UserAgent(); ok { + if err := usagelog.UserAgentValidator(v); err != nil { + return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)} + } + } if v, ok := _u.mutation.ImageSize(); ok { if err := usagelog.ImageSizeValidator(v); err != nil { return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)} @@ -784,6 +809,12 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.FirstTokenMsCleared() { _spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt) } + if value, ok := _u.mutation.UserAgent(); ok { + _spec.SetField(usagelog.FieldUserAgent, field.TypeString, value) + } + if _u.mutation.UserAgentCleared() { + _spec.ClearField(usagelog.FieldUserAgent, field.TypeString) + } if value, ok := _u.mutation.ImageCount(); ok { _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) } @@ -1433,6 +1464,26 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne { return _u } +// SetUserAgent sets the "user_agent" field. +func (_u *UsageLogUpdateOne) SetUserAgent(v string) *UsageLogUpdateOne { + _u.mutation.SetUserAgent(v) + return _u +} + +// SetNillableUserAgent sets the "user_agent" field if the given value is not nil. +func (_u *UsageLogUpdateOne) SetNillableUserAgent(v *string) *UsageLogUpdateOne { + if v != nil { + _u.SetUserAgent(*v) + } + return _u +} + +// ClearUserAgent clears the value of the "user_agent" field. +func (_u *UsageLogUpdateOne) ClearUserAgent() *UsageLogUpdateOne { + _u.mutation.ClearUserAgent() + return _u +} + // SetImageCount sets the "image_count" field. func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne { _u.mutation.ResetImageCount() @@ -1586,6 +1637,11 @@ func (_u *UsageLogUpdateOne) check() error { return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)} } } + if v, ok := _u.mutation.UserAgent(); ok { + if err := usagelog.UserAgentValidator(v); err != nil { + return &ValidationError{Name: "user_agent", err: fmt.Errorf(`ent: validator failed for field "UsageLog.user_agent": %w`, err)} + } + } if v, ok := _u.mutation.ImageSize(); ok { if err := usagelog.ImageSizeValidator(v); err != nil { return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)} @@ -1743,6 +1799,12 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err if _u.mutation.FirstTokenMsCleared() { _spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt) } + if value, ok := _u.mutation.UserAgent(); ok { + _spec.SetField(usagelog.FieldUserAgent, field.TypeString, value) + } + if _u.mutation.UserAgentCleared() { + _spec.ClearField(usagelog.FieldUserAgent, field.TypeString) + } if value, ok := _u.mutation.ImageCount(); ok { _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) } diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index de3cbad9..2d8ff957 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -108,6 +108,9 @@ func (h *GatewayHandler) Messages(c *gin.Context) { // 获取订阅信息(可能为nil)- 提前获取用于后续检查 subscription, _ := middleware2.GetSubscriptionFromContext(c) + // 获取 User-Agent + userAgent := c.Request.UserAgent() + // 0. 检查wait队列是否已满 maxWait := service.CalculateMaxWait(subject.Concurrency) canWait, err := h.concurrencyHelper.IncrementWaitCount(c.Request.Context(), subject.UserID, maxWait) @@ -267,7 +270,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } // 异步记录使用量(subscription已在函数开头获取) - go func(result *service.ForwardResult, usedAccount *service.Account) { + go func(result *service.ForwardResult, usedAccount *service.Account, ua string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ @@ -276,10 +279,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) { User: apiKey.User, Account: usedAccount, Subscription: subscription, + UserAgent: ua, }); err != nil { log.Printf("Record usage failed: %v", err) } - }(result, account) + }(result, account, userAgent) return } } @@ -394,7 +398,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } // 异步记录使用量(subscription已在函数开头获取) - go func(result *service.ForwardResult, usedAccount *service.Account) { + go func(result *service.ForwardResult, usedAccount *service.Account, ua string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ @@ -403,10 +407,11 @@ func (h *GatewayHandler) Messages(c *gin.Context) { User: apiKey.User, Account: usedAccount, Subscription: subscription, + UserAgent: ua, }); err != nil { log.Printf("Record usage failed: %v", err) } - }(result, account) + }(result, account, userAgent) return } } diff --git a/backend/internal/handler/gemini_v1beta_handler.go b/backend/internal/handler/gemini_v1beta_handler.go index aaf651e9..fc8c7cd6 100644 --- a/backend/internal/handler/gemini_v1beta_handler.go +++ b/backend/internal/handler/gemini_v1beta_handler.go @@ -164,6 +164,9 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { // Get subscription (may be nil) subscription, _ := middleware.GetSubscriptionFromContext(c) + // 获取 User-Agent + userAgent := c.Request.UserAgent() + // For Gemini native API, do not send Claude-style ping frames. geminiConcurrency := NewConcurrencyHelper(h.concurrencyHelper.concurrencyService, SSEPingFormatNone, 0) @@ -300,7 +303,7 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { } // 6) record usage async - go func(result *service.ForwardResult, usedAccount *service.Account) { + go func(result *service.ForwardResult, usedAccount *service.Account, ua string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := h.gatewayService.RecordUsage(ctx, &service.RecordUsageInput{ @@ -309,10 +312,11 @@ func (h *GatewayHandler) GeminiV1BetaModels(c *gin.Context) { User: apiKey.User, Account: usedAccount, Subscription: subscription, + UserAgent: ua, }); err != nil { log.Printf("Record usage failed: %v", err) } - }(result, account) + }(result, account, userAgent) return } } diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index 04d268a5..f76a9851 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -242,7 +242,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { } // Async record usage - go func(result *service.OpenAIForwardResult, usedAccount *service.Account) { + go func(result *service.OpenAIForwardResult, usedAccount *service.Account, ua string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := h.gatewayService.RecordUsage(ctx, &service.OpenAIRecordUsageInput{ @@ -251,10 +251,11 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { User: apiKey.User, Account: usedAccount, Subscription: subscription, + UserAgent: ua, }); err != nil { log.Printf("Record usage failed: %v", err) } - }(result, account) + }(result, account, userAgent) return } } diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 4df10b23..bd5c8b4f 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -22,7 +22,7 @@ import ( "github.com/lib/pq" ) -const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, image_count, image_size, created_at" +const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, image_count, image_size, created_at" type usageLogRepository struct { client *dbent.Client @@ -109,6 +109,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) stream, duration_ms, first_token_ms, + user_agent, image_count, image_size, created_at @@ -118,8 +119,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, - $20, $21, $22, $23, $24, - $25, $26, $27 + $20, $21, $22, $23, $24, $25, $26, $27, $28 ) ON CONFLICT (request_id, api_key_id) DO NOTHING RETURNING id, created_at @@ -129,6 +129,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) subscriptionID := nullInt64(log.SubscriptionID) duration := nullInt(log.DurationMs) firstToken := nullInt(log.FirstTokenMs) + userAgent := nullString(log.UserAgent) imageSize := nullString(log.ImageSize) var requestIDArg any @@ -161,6 +162,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) log.Stream, duration, firstToken, + userAgent, log.ImageCount, imageSize, createdAt, @@ -1870,6 +1872,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e stream bool durationMs sql.NullInt64 firstTokenMs sql.NullInt64 + userAgent sql.NullString imageCount int imageSize sql.NullString createdAt time.Time @@ -1901,6 +1904,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e &stream, &durationMs, &firstTokenMs, + &userAgent, &imageCount, &imageSize, &createdAt, @@ -1952,6 +1956,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e value := int(firstTokenMs.Int64) log.FirstTokenMs = &value } + if userAgent.Valid { + log.UserAgent = &userAgent.String + } if imageSize.Valid { log.ImageSize = &imageSize.String } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 120637d5..3a68b44c 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2152,6 +2152,7 @@ type RecordUsageInput struct { User *User Account *Account Subscription *UserSubscription // 可选:订阅信息 + UserAgent string // 请求的 User-Agent } // RecordUsage 记录使用量并扣费(或更新订阅用量) @@ -2237,6 +2238,11 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu CreatedAt: time.Now(), } + // 添加 UserAgent + if input.UserAgent != "" { + usageLog.UserAgent = &input.UserAgent + } + // 添加分组和订阅关联 if apiKey.GroupID != nil { usageLog.GroupID = apiKey.GroupID diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index 08bd8df5..d744bfab 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -1092,6 +1092,7 @@ type OpenAIRecordUsageInput struct { User *User Account *Account Subscription *UserSubscription + UserAgent string // 请求的 User-Agent } // RecordUsage records usage and deducts balance @@ -1161,6 +1162,11 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec CreatedAt: time.Now(), } + // 添加 UserAgent + if input.UserAgent != "" { + usageLog.UserAgent = &input.UserAgent + } + if apiKey.GroupID != nil { usageLog.GroupID = apiKey.GroupID } diff --git a/backend/internal/service/usage_log.go b/backend/internal/service/usage_log.go index 255f0440..9ecb7098 100644 --- a/backend/internal/service/usage_log.go +++ b/backend/internal/service/usage_log.go @@ -38,6 +38,7 @@ type UsageLog struct { Stream bool DurationMs *int FirstTokenMs *int + UserAgent *string // 图片生成字段 ImageCount int diff --git a/backend/migrations/028_add_usage_logs_user_agent.sql b/backend/migrations/028_add_usage_logs_user_agent.sql new file mode 100644 index 00000000..e7e1a581 --- /dev/null +++ b/backend/migrations/028_add_usage_logs_user_agent.sql @@ -0,0 +1,10 @@ +-- Add user_agent column to usage_logs table +-- Records the User-Agent header from API requests for analytics and debugging + +ALTER TABLE usage_logs + ADD COLUMN IF NOT EXISTS user_agent VARCHAR(512); + +-- Optional: Add index for user_agent queries (uncomment if needed for analytics) +-- CREATE INDEX IF NOT EXISTS idx_usage_logs_user_agent ON usage_logs(user_agent); + +COMMENT ON COLUMN usage_logs.user_agent IS 'User-Agent header from the API request';