mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(billing): 按账号控制 OpenAI 长上下文计费
This commit is contained in:
@@ -1559,6 +1559,7 @@ var (
|
||||
{Name: "total_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,10)"}},
|
||||
{Name: "actual_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,10)"}},
|
||||
{Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
{Name: "long_context_billing_applied", Type: field.TypeBool, Default: false},
|
||||
{Name: "account_rate_multiplier", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
{Name: "billing_type", Type: field.TypeInt8, Default: 0},
|
||||
{Name: "stream", Type: field.TypeBool, Default: false},
|
||||
@@ -1591,31 +1592,31 @@ var (
|
||||
ForeignKeys: []*schema.ForeignKey{
|
||||
{
|
||||
Symbol: "usage_logs_api_keys_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
RefColumns: []*schema.Column{APIKeysColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_accounts_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
RefColumns: []*schema.Column{AccountsColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_groups_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
RefColumns: []*schema.Column{GroupsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_users_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
RefColumns: []*schema.Column{UsersColumns[0]},
|
||||
OnDelete: schema.NoAction,
|
||||
},
|
||||
{
|
||||
Symbol: "usage_logs_user_subscriptions_usage_logs",
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
|
||||
OnDelete: schema.SetNull,
|
||||
},
|
||||
@@ -1624,32 +1625,32 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_account_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[41]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[42]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_subscription_id",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[44]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[45]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[40]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_model",
|
||||
@@ -1669,17 +1670,17 @@ var (
|
||||
{
|
||||
Name: "usagelog_user_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[43], UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[44], UsageLogsColumns[40]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_api_key_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[40], UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[41], UsageLogsColumns[40]},
|
||||
},
|
||||
{
|
||||
Name: "usagelog_group_id_created_at",
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UsageLogsColumns[42], UsageLogsColumns[39]},
|
||||
Columns: []*schema.Column{UsageLogsColumns[43], UsageLogsColumns[40]},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+132
-78
@@ -41656,83 +41656,84 @@ func (m *UsageCleanupTaskMutation) ResetEdge(name string) error {
|
||||
// UsageLogMutation represents an operation that mutates the UsageLog nodes in the graph.
|
||||
type UsageLogMutation struct {
|
||||
config
|
||||
op Op
|
||||
typ string
|
||||
id *int64
|
||||
request_id *string
|
||||
model *string
|
||||
requested_model *string
|
||||
upstream_model *string
|
||||
channel_id *int64
|
||||
addchannel_id *int64
|
||||
model_mapping_chain *string
|
||||
billing_tier *string
|
||||
billing_mode *string
|
||||
input_tokens *int
|
||||
addinput_tokens *int
|
||||
output_tokens *int
|
||||
addoutput_tokens *int
|
||||
cache_creation_tokens *int
|
||||
addcache_creation_tokens *int
|
||||
cache_read_tokens *int
|
||||
addcache_read_tokens *int
|
||||
cache_creation_5m_tokens *int
|
||||
addcache_creation_5m_tokens *int
|
||||
cache_creation_1h_tokens *int
|
||||
addcache_creation_1h_tokens *int
|
||||
input_cost *float64
|
||||
addinput_cost *float64
|
||||
output_cost *float64
|
||||
addoutput_cost *float64
|
||||
cache_creation_cost *float64
|
||||
addcache_creation_cost *float64
|
||||
cache_read_cost *float64
|
||||
addcache_read_cost *float64
|
||||
total_cost *float64
|
||||
addtotal_cost *float64
|
||||
actual_cost *float64
|
||||
addactual_cost *float64
|
||||
rate_multiplier *float64
|
||||
addrate_multiplier *float64
|
||||
account_rate_multiplier *float64
|
||||
addaccount_rate_multiplier *float64
|
||||
billing_type *int8
|
||||
addbilling_type *int8
|
||||
stream *bool
|
||||
duration_ms *int
|
||||
addduration_ms *int
|
||||
first_token_ms *int
|
||||
addfirst_token_ms *int
|
||||
user_agent *string
|
||||
ip_address *string
|
||||
image_count *int
|
||||
addimage_count *int
|
||||
image_size *string
|
||||
image_input_size *string
|
||||
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{}
|
||||
user *int64
|
||||
cleareduser bool
|
||||
api_key *int64
|
||||
clearedapi_key bool
|
||||
account *int64
|
||||
clearedaccount bool
|
||||
group *int64
|
||||
clearedgroup bool
|
||||
subscription *int64
|
||||
clearedsubscription bool
|
||||
done bool
|
||||
oldValue func(context.Context) (*UsageLog, error)
|
||||
predicates []predicate.UsageLog
|
||||
op Op
|
||||
typ string
|
||||
id *int64
|
||||
request_id *string
|
||||
model *string
|
||||
requested_model *string
|
||||
upstream_model *string
|
||||
channel_id *int64
|
||||
addchannel_id *int64
|
||||
model_mapping_chain *string
|
||||
billing_tier *string
|
||||
billing_mode *string
|
||||
input_tokens *int
|
||||
addinput_tokens *int
|
||||
output_tokens *int
|
||||
addoutput_tokens *int
|
||||
cache_creation_tokens *int
|
||||
addcache_creation_tokens *int
|
||||
cache_read_tokens *int
|
||||
addcache_read_tokens *int
|
||||
cache_creation_5m_tokens *int
|
||||
addcache_creation_5m_tokens *int
|
||||
cache_creation_1h_tokens *int
|
||||
addcache_creation_1h_tokens *int
|
||||
input_cost *float64
|
||||
addinput_cost *float64
|
||||
output_cost *float64
|
||||
addoutput_cost *float64
|
||||
cache_creation_cost *float64
|
||||
addcache_creation_cost *float64
|
||||
cache_read_cost *float64
|
||||
addcache_read_cost *float64
|
||||
total_cost *float64
|
||||
addtotal_cost *float64
|
||||
actual_cost *float64
|
||||
addactual_cost *float64
|
||||
rate_multiplier *float64
|
||||
addrate_multiplier *float64
|
||||
long_context_billing_applied *bool
|
||||
account_rate_multiplier *float64
|
||||
addaccount_rate_multiplier *float64
|
||||
billing_type *int8
|
||||
addbilling_type *int8
|
||||
stream *bool
|
||||
duration_ms *int
|
||||
addduration_ms *int
|
||||
first_token_ms *int
|
||||
addfirst_token_ms *int
|
||||
user_agent *string
|
||||
ip_address *string
|
||||
image_count *int
|
||||
addimage_count *int
|
||||
image_size *string
|
||||
image_input_size *string
|
||||
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{}
|
||||
user *int64
|
||||
cleareduser bool
|
||||
api_key *int64
|
||||
clearedapi_key bool
|
||||
account *int64
|
||||
clearedaccount bool
|
||||
group *int64
|
||||
clearedgroup bool
|
||||
subscription *int64
|
||||
clearedsubscription bool
|
||||
done bool
|
||||
oldValue func(context.Context) (*UsageLog, error)
|
||||
predicates []predicate.UsageLog
|
||||
}
|
||||
|
||||
var _ ent.Mutation = (*UsageLogMutation)(nil)
|
||||
@@ -43154,6 +43155,42 @@ func (m *UsageLogMutation) ResetRateMultiplier() {
|
||||
m.addrate_multiplier = nil
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (m *UsageLogMutation) SetLongContextBillingApplied(b bool) {
|
||||
m.long_context_billing_applied = &b
|
||||
}
|
||||
|
||||
// LongContextBillingApplied returns the value of the "long_context_billing_applied" field in the mutation.
|
||||
func (m *UsageLogMutation) LongContextBillingApplied() (r bool, exists bool) {
|
||||
v := m.long_context_billing_applied
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldLongContextBillingApplied returns the old "long_context_billing_applied" 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) OldLongContextBillingApplied(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldLongContextBillingApplied is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldLongContextBillingApplied requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldLongContextBillingApplied: %w", err)
|
||||
}
|
||||
return oldValue.LongContextBillingApplied, nil
|
||||
}
|
||||
|
||||
// ResetLongContextBillingApplied resets all changes to the "long_context_billing_applied" field.
|
||||
func (m *UsageLogMutation) ResetLongContextBillingApplied() {
|
||||
m.long_context_billing_applied = nil
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (m *UsageLogMutation) SetAccountRateMultiplier(f float64) {
|
||||
m.account_rate_multiplier = &f
|
||||
@@ -44271,7 +44308,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, 44)
|
||||
fields := make([]string, 0, 45)
|
||||
if m.user != nil {
|
||||
fields = append(fields, usagelog.FieldUserID)
|
||||
}
|
||||
@@ -44350,6 +44387,9 @@ func (m *UsageLogMutation) Fields() []string {
|
||||
if m.rate_multiplier != nil {
|
||||
fields = append(fields, usagelog.FieldRateMultiplier)
|
||||
}
|
||||
if m.long_context_billing_applied != nil {
|
||||
fields = append(fields, usagelog.FieldLongContextBillingApplied)
|
||||
}
|
||||
if m.account_rate_multiplier != nil {
|
||||
fields = append(fields, usagelog.FieldAccountRateMultiplier)
|
||||
}
|
||||
@@ -44464,6 +44504,8 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.ActualCost()
|
||||
case usagelog.FieldRateMultiplier:
|
||||
return m.RateMultiplier()
|
||||
case usagelog.FieldLongContextBillingApplied:
|
||||
return m.LongContextBillingApplied()
|
||||
case usagelog.FieldAccountRateMultiplier:
|
||||
return m.AccountRateMultiplier()
|
||||
case usagelog.FieldBillingType:
|
||||
@@ -44561,6 +44603,8 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value
|
||||
return m.OldActualCost(ctx)
|
||||
case usagelog.FieldRateMultiplier:
|
||||
return m.OldRateMultiplier(ctx)
|
||||
case usagelog.FieldLongContextBillingApplied:
|
||||
return m.OldLongContextBillingApplied(ctx)
|
||||
case usagelog.FieldAccountRateMultiplier:
|
||||
return m.OldAccountRateMultiplier(ctx)
|
||||
case usagelog.FieldBillingType:
|
||||
@@ -44788,6 +44832,13 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetRateMultiplier(v)
|
||||
return nil
|
||||
case usagelog.FieldLongContextBillingApplied:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetLongContextBillingApplied(v)
|
||||
return nil
|
||||
case usagelog.FieldAccountRateMultiplier:
|
||||
v, ok := value.(float64)
|
||||
if !ok {
|
||||
@@ -45419,6 +45470,9 @@ func (m *UsageLogMutation) ResetField(name string) error {
|
||||
case usagelog.FieldRateMultiplier:
|
||||
m.ResetRateMultiplier()
|
||||
return nil
|
||||
case usagelog.FieldLongContextBillingApplied:
|
||||
m.ResetLongContextBillingApplied()
|
||||
return nil
|
||||
case usagelog.FieldAccountRateMultiplier:
|
||||
m.ResetAccountRateMultiplier()
|
||||
return nil
|
||||
|
||||
@@ -1940,56 +1940,60 @@ func init() {
|
||||
usagelogDescRateMultiplier := usagelogFields[25].Descriptor()
|
||||
// usagelog.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
|
||||
usagelog.DefaultRateMultiplier = usagelogDescRateMultiplier.Default.(float64)
|
||||
// usagelogDescLongContextBillingApplied is the schema descriptor for long_context_billing_applied field.
|
||||
usagelogDescLongContextBillingApplied := usagelogFields[26].Descriptor()
|
||||
// usagelog.DefaultLongContextBillingApplied holds the default value on creation for the long_context_billing_applied field.
|
||||
usagelog.DefaultLongContextBillingApplied = usagelogDescLongContextBillingApplied.Default.(bool)
|
||||
// usagelogDescBillingType is the schema descriptor for billing_type field.
|
||||
usagelogDescBillingType := usagelogFields[27].Descriptor()
|
||||
usagelogDescBillingType := usagelogFields[28].Descriptor()
|
||||
// usagelog.DefaultBillingType holds the default value on creation for the billing_type field.
|
||||
usagelog.DefaultBillingType = usagelogDescBillingType.Default.(int8)
|
||||
// usagelogDescStream is the schema descriptor for stream field.
|
||||
usagelogDescStream := usagelogFields[28].Descriptor()
|
||||
usagelogDescStream := usagelogFields[29].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[31].Descriptor()
|
||||
usagelogDescUserAgent := usagelogFields[32].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)
|
||||
// usagelogDescIPAddress is the schema descriptor for ip_address field.
|
||||
usagelogDescIPAddress := usagelogFields[32].Descriptor()
|
||||
usagelogDescIPAddress := usagelogFields[33].Descriptor()
|
||||
// usagelog.IPAddressValidator is a validator for the "ip_address" field. It is called by the builders before save.
|
||||
usagelog.IPAddressValidator = usagelogDescIPAddress.Validators[0].(func(string) error)
|
||||
// usagelogDescImageCount is the schema descriptor for image_count field.
|
||||
usagelogDescImageCount := usagelogFields[33].Descriptor()
|
||||
usagelogDescImageCount := usagelogFields[34].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[34].Descriptor()
|
||||
usagelogDescImageSize := usagelogFields[35].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)
|
||||
// usagelogDescImageInputSize is the schema descriptor for image_input_size field.
|
||||
usagelogDescImageInputSize := usagelogFields[35].Descriptor()
|
||||
usagelogDescImageInputSize := usagelogFields[36].Descriptor()
|
||||
// usagelog.ImageInputSizeValidator is a validator for the "image_input_size" field. It is called by the builders before save.
|
||||
usagelog.ImageInputSizeValidator = usagelogDescImageInputSize.Validators[0].(func(string) error)
|
||||
// usagelogDescImageOutputSize is the schema descriptor for image_output_size field.
|
||||
usagelogDescImageOutputSize := usagelogFields[36].Descriptor()
|
||||
usagelogDescImageOutputSize := usagelogFields[37].Descriptor()
|
||||
// usagelog.ImageOutputSizeValidator is a validator for the "image_output_size" field. It is called by the builders before save.
|
||||
usagelog.ImageOutputSizeValidator = usagelogDescImageOutputSize.Validators[0].(func(string) error)
|
||||
// usagelogDescImageSizeSource is the schema descriptor for image_size_source field.
|
||||
usagelogDescImageSizeSource := usagelogFields[37].Descriptor()
|
||||
usagelogDescImageSizeSource := usagelogFields[38].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()
|
||||
usagelogDescVideoCount := usagelogFields[40].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()
|
||||
usagelogDescVideoResolution := usagelogFields[41].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[42].Descriptor()
|
||||
usagelogDescCacheTTLOverridden := usagelogFields[43].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[43].Descriptor()
|
||||
usagelogDescCreatedAt := usagelogFields[44].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()
|
||||
|
||||
@@ -100,6 +100,9 @@ func (UsageLog) Fields() []ent.Field {
|
||||
field.Float("rate_multiplier").
|
||||
Default(1).
|
||||
SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}),
|
||||
field.Bool("long_context_billing_applied").
|
||||
Default(false).
|
||||
Comment("Whether long-context pricing changed token prices for this request"),
|
||||
|
||||
// account_rate_multiplier: 账号计费倍率快照(NULL 表示按 1.0 处理)
|
||||
field.Float("account_rate_multiplier").
|
||||
|
||||
+12
-1
@@ -75,6 +75,8 @@ type UsageLog struct {
|
||||
ActualCost float64 `json:"actual_cost,omitempty"`
|
||||
// RateMultiplier holds the value of the "rate_multiplier" field.
|
||||
RateMultiplier float64 `json:"rate_multiplier,omitempty"`
|
||||
// Whether long-context pricing changed token prices for this request
|
||||
LongContextBillingApplied bool `json:"long_context_billing_applied,omitempty"`
|
||||
// AccountRateMultiplier holds the value of the "account_rate_multiplier" field.
|
||||
AccountRateMultiplier *float64 `json:"account_rate_multiplier,omitempty"`
|
||||
// BillingType holds the value of the "billing_type" field.
|
||||
@@ -196,7 +198,7 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
|
||||
switch columns[i] {
|
||||
case usagelog.FieldImageSizeBreakdown:
|
||||
values[i] = new([]byte)
|
||||
case usagelog.FieldStream, usagelog.FieldCacheTTLOverridden:
|
||||
case usagelog.FieldLongContextBillingApplied, usagelog.FieldStream, usagelog.FieldCacheTTLOverridden:
|
||||
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)
|
||||
@@ -391,6 +393,12 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
|
||||
} else if value.Valid {
|
||||
_m.RateMultiplier = value.Float64
|
||||
}
|
||||
case usagelog.FieldLongContextBillingApplied:
|
||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field long_context_billing_applied", values[i])
|
||||
} else if value.Valid {
|
||||
_m.LongContextBillingApplied = value.Bool
|
||||
}
|
||||
case usagelog.FieldAccountRateMultiplier:
|
||||
if value, ok := values[i].(*sql.NullFloat64); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field account_rate_multiplier", values[i])
|
||||
@@ -667,6 +675,9 @@ func (_m *UsageLog) String() string {
|
||||
builder.WriteString("rate_multiplier=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.RateMultiplier))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("long_context_billing_applied=")
|
||||
builder.WriteString(fmt.Sprintf("%v", _m.LongContextBillingApplied))
|
||||
builder.WriteString(", ")
|
||||
if v := _m.AccountRateMultiplier; v != nil {
|
||||
builder.WriteString("account_rate_multiplier=")
|
||||
builder.WriteString(fmt.Sprintf("%v", *v))
|
||||
|
||||
@@ -66,6 +66,8 @@ const (
|
||||
FieldActualCost = "actual_cost"
|
||||
// FieldRateMultiplier holds the string denoting the rate_multiplier field in the database.
|
||||
FieldRateMultiplier = "rate_multiplier"
|
||||
// FieldLongContextBillingApplied holds the string denoting the long_context_billing_applied field in the database.
|
||||
FieldLongContextBillingApplied = "long_context_billing_applied"
|
||||
// FieldAccountRateMultiplier holds the string denoting the account_rate_multiplier field in the database.
|
||||
FieldAccountRateMultiplier = "account_rate_multiplier"
|
||||
// FieldBillingType holds the string denoting the billing_type field in the database.
|
||||
@@ -180,6 +182,7 @@ var Columns = []string{
|
||||
FieldTotalCost,
|
||||
FieldActualCost,
|
||||
FieldRateMultiplier,
|
||||
FieldLongContextBillingApplied,
|
||||
FieldAccountRateMultiplier,
|
||||
FieldBillingType,
|
||||
FieldStream,
|
||||
@@ -251,6 +254,8 @@ var (
|
||||
DefaultActualCost float64
|
||||
// DefaultRateMultiplier holds the default value on creation for the "rate_multiplier" field.
|
||||
DefaultRateMultiplier float64
|
||||
// DefaultLongContextBillingApplied holds the default value on creation for the "long_context_billing_applied" field.
|
||||
DefaultLongContextBillingApplied bool
|
||||
// DefaultBillingType holds the default value on creation for the "billing_type" field.
|
||||
DefaultBillingType int8
|
||||
// DefaultStream holds the default value on creation for the "stream" field.
|
||||
@@ -417,6 +422,11 @@ func ByRateMultiplier(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldRateMultiplier, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByLongContextBillingApplied orders the results by the long_context_billing_applied field.
|
||||
func ByLongContextBillingApplied(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldLongContextBillingApplied, opts...).ToFunc()
|
||||
}
|
||||
|
||||
// ByAccountRateMultiplier orders the results by the account_rate_multiplier field.
|
||||
func ByAccountRateMultiplier(opts ...sql.OrderTermOption) OrderOption {
|
||||
return sql.OrderByField(FieldAccountRateMultiplier, opts...).ToFunc()
|
||||
|
||||
@@ -185,6 +185,11 @@ func RateMultiplier(v float64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldRateMultiplier, v))
|
||||
}
|
||||
|
||||
// LongContextBillingApplied applies equality check predicate on the "long_context_billing_applied" field. It's identical to LongContextBillingAppliedEQ.
|
||||
func LongContextBillingApplied(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldLongContextBillingApplied, v))
|
||||
}
|
||||
|
||||
// AccountRateMultiplier applies equality check predicate on the "account_rate_multiplier" field. It's identical to AccountRateMultiplierEQ.
|
||||
func AccountRateMultiplier(v float64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldAccountRateMultiplier, v))
|
||||
@@ -1465,6 +1470,16 @@ func RateMultiplierLTE(v float64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldLTE(FieldRateMultiplier, v))
|
||||
}
|
||||
|
||||
// LongContextBillingAppliedEQ applies the EQ predicate on the "long_context_billing_applied" field.
|
||||
func LongContextBillingAppliedEQ(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldLongContextBillingApplied, v))
|
||||
}
|
||||
|
||||
// LongContextBillingAppliedNEQ applies the NEQ predicate on the "long_context_billing_applied" field.
|
||||
func LongContextBillingAppliedNEQ(v bool) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldNEQ(FieldLongContextBillingApplied, v))
|
||||
}
|
||||
|
||||
// AccountRateMultiplierEQ applies the EQ predicate on the "account_rate_multiplier" field.
|
||||
func AccountRateMultiplierEQ(v float64) predicate.UsageLog {
|
||||
return predicate.UsageLog(sql.FieldEQ(FieldAccountRateMultiplier, v))
|
||||
|
||||
@@ -351,6 +351,20 @@ func (_c *UsageLogCreate) SetNillableRateMultiplier(v *float64) *UsageLogCreate
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (_c *UsageLogCreate) SetLongContextBillingApplied(v bool) *UsageLogCreate {
|
||||
_c.mutation.SetLongContextBillingApplied(v)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetNillableLongContextBillingApplied sets the "long_context_billing_applied" field if the given value is not nil.
|
||||
func (_c *UsageLogCreate) SetNillableLongContextBillingApplied(v *bool) *UsageLogCreate {
|
||||
if v != nil {
|
||||
_c.SetLongContextBillingApplied(*v)
|
||||
}
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (_c *UsageLogCreate) SetAccountRateMultiplier(v float64) *UsageLogCreate {
|
||||
_c.mutation.SetAccountRateMultiplier(v)
|
||||
@@ -707,6 +721,10 @@ func (_c *UsageLogCreate) defaults() {
|
||||
v := usagelog.DefaultRateMultiplier
|
||||
_c.mutation.SetRateMultiplier(v)
|
||||
}
|
||||
if _, ok := _c.mutation.LongContextBillingApplied(); !ok {
|
||||
v := usagelog.DefaultLongContextBillingApplied
|
||||
_c.mutation.SetLongContextBillingApplied(v)
|
||||
}
|
||||
if _, ok := _c.mutation.BillingType(); !ok {
|
||||
v := usagelog.DefaultBillingType
|
||||
_c.mutation.SetBillingType(v)
|
||||
@@ -824,6 +842,9 @@ func (_c *UsageLogCreate) check() error {
|
||||
if _, ok := _c.mutation.RateMultiplier(); !ok {
|
||||
return &ValidationError{Name: "rate_multiplier", err: errors.New(`ent: missing required field "UsageLog.rate_multiplier"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.LongContextBillingApplied(); !ok {
|
||||
return &ValidationError{Name: "long_context_billing_applied", err: errors.New(`ent: missing required field "UsageLog.long_context_billing_applied"`)}
|
||||
}
|
||||
if _, ok := _c.mutation.BillingType(); !ok {
|
||||
return &ValidationError{Name: "billing_type", err: errors.New(`ent: missing required field "UsageLog.billing_type"`)}
|
||||
}
|
||||
@@ -997,6 +1018,10 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
|
||||
_node.RateMultiplier = value
|
||||
}
|
||||
if value, ok := _c.mutation.LongContextBillingApplied(); ok {
|
||||
_spec.SetField(usagelog.FieldLongContextBillingApplied, field.TypeBool, value)
|
||||
_node.LongContextBillingApplied = value
|
||||
}
|
||||
if value, ok := _c.mutation.AccountRateMultiplier(); ok {
|
||||
_spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
|
||||
_node.AccountRateMultiplier = &value
|
||||
@@ -1650,6 +1675,18 @@ func (u *UsageLogUpsert) AddRateMultiplier(v float64) *UsageLogUpsert {
|
||||
return u
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (u *UsageLogUpsert) SetLongContextBillingApplied(v bool) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldLongContextBillingApplied, v)
|
||||
return u
|
||||
}
|
||||
|
||||
// UpdateLongContextBillingApplied sets the "long_context_billing_applied" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsert) UpdateLongContextBillingApplied() *UsageLogUpsert {
|
||||
u.SetExcluded(usagelog.FieldLongContextBillingApplied)
|
||||
return u
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (u *UsageLogUpsert) SetAccountRateMultiplier(v float64) *UsageLogUpsert {
|
||||
u.Set(usagelog.FieldAccountRateMultiplier, v)
|
||||
@@ -2531,6 +2568,20 @@ func (u *UsageLogUpsertOne) UpdateRateMultiplier() *UsageLogUpsertOne {
|
||||
})
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (u *UsageLogUpsertOne) SetLongContextBillingApplied(v bool) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetLongContextBillingApplied(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLongContextBillingApplied sets the "long_context_billing_applied" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertOne) UpdateLongContextBillingApplied() *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateLongContextBillingApplied()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (u *UsageLogUpsertOne) SetAccountRateMultiplier(v float64) *UsageLogUpsertOne {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
@@ -3631,6 +3682,20 @@ func (u *UsageLogUpsertBulk) UpdateRateMultiplier() *UsageLogUpsertBulk {
|
||||
})
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (u *UsageLogUpsertBulk) SetLongContextBillingApplied(v bool) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.SetLongContextBillingApplied(v)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLongContextBillingApplied sets the "long_context_billing_applied" field to the value that was provided on create.
|
||||
func (u *UsageLogUpsertBulk) UpdateLongContextBillingApplied() *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
s.UpdateLongContextBillingApplied()
|
||||
})
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (u *UsageLogUpsertBulk) SetAccountRateMultiplier(v float64) *UsageLogUpsertBulk {
|
||||
return u.Update(func(s *UsageLogUpsert) {
|
||||
|
||||
@@ -542,6 +542,20 @@ func (_u *UsageLogUpdate) AddRateMultiplier(v float64) *UsageLogUpdate {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (_u *UsageLogUpdate) SetLongContextBillingApplied(v bool) *UsageLogUpdate {
|
||||
_u.mutation.SetLongContextBillingApplied(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLongContextBillingApplied sets the "long_context_billing_applied" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdate) SetNillableLongContextBillingApplied(v *bool) *UsageLogUpdate {
|
||||
if v != nil {
|
||||
_u.SetLongContextBillingApplied(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (_u *UsageLogUpdate) SetAccountRateMultiplier(v float64) *UsageLogUpdate {
|
||||
_u.mutation.ResetAccountRateMultiplier()
|
||||
@@ -1199,6 +1213,9 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
||||
if value, ok := _u.mutation.AddedRateMultiplier(); ok {
|
||||
_spec.AddField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.LongContextBillingApplied(); ok {
|
||||
_spec.SetField(usagelog.FieldLongContextBillingApplied, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AccountRateMultiplier(); ok {
|
||||
_spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
|
||||
}
|
||||
@@ -1982,6 +1999,20 @@ func (_u *UsageLogUpdateOne) AddRateMultiplier(v float64) *UsageLogUpdateOne {
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetLongContextBillingApplied sets the "long_context_billing_applied" field.
|
||||
func (_u *UsageLogUpdateOne) SetLongContextBillingApplied(v bool) *UsageLogUpdateOne {
|
||||
_u.mutation.SetLongContextBillingApplied(v)
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetNillableLongContextBillingApplied sets the "long_context_billing_applied" field if the given value is not nil.
|
||||
func (_u *UsageLogUpdateOne) SetNillableLongContextBillingApplied(v *bool) *UsageLogUpdateOne {
|
||||
if v != nil {
|
||||
_u.SetLongContextBillingApplied(*v)
|
||||
}
|
||||
return _u
|
||||
}
|
||||
|
||||
// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
|
||||
func (_u *UsageLogUpdateOne) SetAccountRateMultiplier(v float64) *UsageLogUpdateOne {
|
||||
_u.mutation.ResetAccountRateMultiplier()
|
||||
@@ -2669,6 +2700,9 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
|
||||
if value, ok := _u.mutation.AddedRateMultiplier(); ok {
|
||||
_spec.AddField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
|
||||
}
|
||||
if value, ok := _u.mutation.LongContextBillingApplied(); ok {
|
||||
_spec.SetField(usagelog.FieldLongContextBillingApplied, field.TypeBool, value)
|
||||
}
|
||||
if value, ok := _u.mutation.AccountRateMultiplier(); ok {
|
||||
_spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
|
||||
}
|
||||
|
||||
@@ -598,54 +598,55 @@ func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
|
||||
requestedModel = l.Model
|
||||
}
|
||||
return UsageLog{
|
||||
ID: l.ID,
|
||||
UserID: l.UserID,
|
||||
APIKeyID: l.APIKeyID,
|
||||
AccountID: l.AccountID,
|
||||
RequestID: l.RequestID,
|
||||
Model: requestedModel,
|
||||
ServiceTier: l.ServiceTier,
|
||||
ReasoningEffort: l.ReasoningEffort,
|
||||
InboundEndpoint: l.InboundEndpoint,
|
||||
GroupID: l.GroupID,
|
||||
SubscriptionID: l.SubscriptionID,
|
||||
InputTokens: l.InputTokens,
|
||||
OutputTokens: l.OutputTokens,
|
||||
CacheCreationTokens: l.CacheCreationTokens,
|
||||
CacheReadTokens: l.CacheReadTokens,
|
||||
CacheCreation5mTokens: l.CacheCreation5mTokens,
|
||||
CacheCreation1hTokens: l.CacheCreation1hTokens,
|
||||
InputCost: l.InputCost,
|
||||
OutputCost: l.OutputCost,
|
||||
CacheCreationCost: l.CacheCreationCost,
|
||||
CacheReadCost: l.CacheReadCost,
|
||||
TotalCost: l.TotalCost,
|
||||
ActualCost: l.ActualCost,
|
||||
RateMultiplier: l.RateMultiplier,
|
||||
BillingType: l.BillingType,
|
||||
RequestType: requestType.String(),
|
||||
Stream: stream,
|
||||
OpenAIWSMode: openAIWSMode,
|
||||
DurationMs: l.DurationMs,
|
||||
FirstTokenMs: l.FirstTokenMs,
|
||||
ImageCount: l.ImageCount,
|
||||
ImageSize: l.ImageSize,
|
||||
ImageInputSize: l.ImageInputSize,
|
||||
ImageOutputSize: l.ImageOutputSize,
|
||||
ImageOutputTokens: l.ImageOutputTokens,
|
||||
ImageOutputCost: l.ImageOutputCost,
|
||||
ImageSizeSource: l.ImageSizeSource,
|
||||
ImageSizeBreakdown: l.ImageSizeBreakdown,
|
||||
MediaType: l.MediaType,
|
||||
UserAgent: l.UserAgent,
|
||||
IPAddress: l.IPAddress,
|
||||
CacheTTLOverridden: l.CacheTTLOverridden,
|
||||
BillingMode: l.BillingMode,
|
||||
CreatedAt: l.CreatedAt,
|
||||
User: UserFromServiceShallow(l.User),
|
||||
APIKey: APIKeyFromService(l.APIKey),
|
||||
Group: GroupFromServiceShallow(l.Group),
|
||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
||||
ID: l.ID,
|
||||
UserID: l.UserID,
|
||||
APIKeyID: l.APIKeyID,
|
||||
AccountID: l.AccountID,
|
||||
RequestID: l.RequestID,
|
||||
Model: requestedModel,
|
||||
ServiceTier: l.ServiceTier,
|
||||
ReasoningEffort: l.ReasoningEffort,
|
||||
InboundEndpoint: l.InboundEndpoint,
|
||||
GroupID: l.GroupID,
|
||||
SubscriptionID: l.SubscriptionID,
|
||||
InputTokens: l.InputTokens,
|
||||
OutputTokens: l.OutputTokens,
|
||||
CacheCreationTokens: l.CacheCreationTokens,
|
||||
CacheReadTokens: l.CacheReadTokens,
|
||||
CacheCreation5mTokens: l.CacheCreation5mTokens,
|
||||
CacheCreation1hTokens: l.CacheCreation1hTokens,
|
||||
InputCost: l.InputCost,
|
||||
OutputCost: l.OutputCost,
|
||||
CacheCreationCost: l.CacheCreationCost,
|
||||
CacheReadCost: l.CacheReadCost,
|
||||
TotalCost: l.TotalCost,
|
||||
ActualCost: l.ActualCost,
|
||||
RateMultiplier: l.RateMultiplier,
|
||||
LongContextBillingApplied: l.LongContextBillingApplied,
|
||||
BillingType: l.BillingType,
|
||||
RequestType: requestType.String(),
|
||||
Stream: stream,
|
||||
OpenAIWSMode: openAIWSMode,
|
||||
DurationMs: l.DurationMs,
|
||||
FirstTokenMs: l.FirstTokenMs,
|
||||
ImageCount: l.ImageCount,
|
||||
ImageSize: l.ImageSize,
|
||||
ImageInputSize: l.ImageInputSize,
|
||||
ImageOutputSize: l.ImageOutputSize,
|
||||
ImageOutputTokens: l.ImageOutputTokens,
|
||||
ImageOutputCost: l.ImageOutputCost,
|
||||
ImageSizeSource: l.ImageSizeSource,
|
||||
ImageSizeBreakdown: l.ImageSizeBreakdown,
|
||||
MediaType: l.MediaType,
|
||||
UserAgent: l.UserAgent,
|
||||
IPAddress: l.IPAddress,
|
||||
CacheTTLOverridden: l.CacheTTLOverridden,
|
||||
BillingMode: l.BillingMode,
|
||||
CreatedAt: l.CreatedAt,
|
||||
User: UserFromServiceShallow(l.User),
|
||||
APIKey: APIKeyFromService(l.APIKey),
|
||||
Group: GroupFromServiceShallow(l.Group),
|
||||
Subscription: UserSubscriptionFromService(l.Subscription),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -480,13 +480,14 @@ type UsageLog struct {
|
||||
CacheCreation5mTokens int `json:"cache_creation_5m_tokens"`
|
||||
CacheCreation1hTokens int `json:"cache_creation_1h_tokens"`
|
||||
|
||||
InputCost float64 `json:"input_cost"`
|
||||
OutputCost float64 `json:"output_cost"`
|
||||
CacheCreationCost float64 `json:"cache_creation_cost"`
|
||||
CacheReadCost float64 `json:"cache_read_cost"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ActualCost float64 `json:"actual_cost"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
InputCost float64 `json:"input_cost"`
|
||||
OutputCost float64 `json:"output_cost"`
|
||||
CacheCreationCost float64 `json:"cache_creation_cost"`
|
||||
CacheReadCost float64 `json:"cache_read_cost"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
ActualCost float64 `json:"actual_cost"`
|
||||
RateMultiplier float64 `json:"rate_multiplier"`
|
||||
LongContextBillingApplied bool `json:"long_context_billing_applied"`
|
||||
|
||||
BillingType int8 `json:"billing_type"`
|
||||
RequestType string `json:"request_type"`
|
||||
|
||||
@@ -71,6 +71,7 @@ var usageLogInsertArgTypes = [...]string{
|
||||
"text", // inbound_endpoint
|
||||
"text", // upstream_endpoint
|
||||
"boolean", // cache_ttl_overridden
|
||||
"boolean", // long_context_billing_applied
|
||||
"bigint", // channel_id
|
||||
"text", // model_mapping_chain
|
||||
"text", // billing_tier
|
||||
@@ -263,6 +264,7 @@ func (r *usageLogRepository) createSingle(ctx context.Context, sqlq sqlExecutor,
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -275,7 +277,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, $51, $52, $53
|
||||
$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, $54
|
||||
)
|
||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||
RETURNING id, created_at
|
||||
@@ -714,6 +716,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -722,7 +725,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
created_at
|
||||
) AS (VALUES `)
|
||||
|
||||
args := make([]any, 0, len(keys)*53)
|
||||
args := make([]any, 0, len(keys)*54)
|
||||
argPos := 1
|
||||
for idx, key := range keys {
|
||||
if idx > 0 {
|
||||
@@ -798,6 +801,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -853,6 +857,7 @@ func buildUsageLogBatchInsertQuery(keys []string, preparedByKey map[string]usage
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -948,6 +953,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -956,7 +962,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
created_at
|
||||
) AS (VALUES `)
|
||||
|
||||
args := make([]any, 0, len(preparedList)*53)
|
||||
args := make([]any, 0, len(preparedList)*54)
|
||||
argPos := 1
|
||||
for idx, prepared := range preparedList {
|
||||
if idx > 0 {
|
||||
@@ -1029,6 +1035,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -1084,6 +1091,7 @@ func buildUsageLogBestEffortInsertQuery(preparedList []usageLogInsertPrepared) (
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -1147,6 +1155,7 @@ func execUsageLogInsertNoResult(ctx context.Context, sqlq sqlExecutor, prepared
|
||||
inbound_endpoint,
|
||||
upstream_endpoint,
|
||||
cache_ttl_overridden,
|
||||
long_context_billing_applied,
|
||||
channel_id,
|
||||
model_mapping_chain,
|
||||
billing_tier,
|
||||
@@ -1159,7 +1168,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, $51, $52, $53
|
||||
$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, $54
|
||||
)
|
||||
ON CONFLICT (request_id, api_key_id) DO NOTHING
|
||||
`, prepared.args...)
|
||||
@@ -1264,6 +1273,7 @@ func prepareUsageLogInsert(log *service.UsageLog) usageLogInsertPrepared {
|
||||
inboundEndpoint,
|
||||
upstreamEndpoint,
|
||||
log.CacheTTLOverridden,
|
||||
log.LongContextBillingApplied,
|
||||
channelID,
|
||||
modelMappingChain,
|
||||
billingTier,
|
||||
|
||||
@@ -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, 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"
|
||||
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, long_context_billing_applied, 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"
|
||||
@@ -425,60 +425,61 @@ func (r *usageLogRepository) loadSubscriptions(ctx context.Context, ids []int64)
|
||||
|
||||
func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, error) {
|
||||
var (
|
||||
id int64
|
||||
userID int64
|
||||
apiKeyID int64
|
||||
accountID int64
|
||||
requestID sql.NullString
|
||||
model string
|
||||
requestedModel sql.NullString
|
||||
upstreamModel sql.NullString
|
||||
groupID sql.NullInt64
|
||||
subscriptionID sql.NullInt64
|
||||
inputTokens int
|
||||
outputTokens int
|
||||
cacheCreationTokens int
|
||||
cacheReadTokens int
|
||||
cacheCreation5m int
|
||||
cacheCreation1h int
|
||||
imageOutputTokens int
|
||||
imageOutputCost float64
|
||||
inputCost float64
|
||||
outputCost float64
|
||||
cacheCreationCost float64
|
||||
cacheReadCost float64
|
||||
totalCost float64
|
||||
actualCost float64
|
||||
rateMultiplier float64
|
||||
accountRateMultiplier sql.NullFloat64
|
||||
billingType int16
|
||||
requestTypeRaw int16
|
||||
stream bool
|
||||
openaiWSMode bool
|
||||
durationMs sql.NullInt64
|
||||
firstTokenMs sql.NullInt64
|
||||
userAgent sql.NullString
|
||||
ipAddress sql.NullString
|
||||
imageCount int
|
||||
imageSize sql.NullString
|
||||
imageInputSize sql.NullString
|
||||
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
|
||||
upstreamEndpoint sql.NullString
|
||||
cacheTTLOverridden bool
|
||||
channelID sql.NullInt64
|
||||
modelMappingChain sql.NullString
|
||||
billingTier sql.NullString
|
||||
billingMode sql.NullString
|
||||
accountStatsCost sql.NullFloat64
|
||||
createdAt time.Time
|
||||
id int64
|
||||
userID int64
|
||||
apiKeyID int64
|
||||
accountID int64
|
||||
requestID sql.NullString
|
||||
model string
|
||||
requestedModel sql.NullString
|
||||
upstreamModel sql.NullString
|
||||
groupID sql.NullInt64
|
||||
subscriptionID sql.NullInt64
|
||||
inputTokens int
|
||||
outputTokens int
|
||||
cacheCreationTokens int
|
||||
cacheReadTokens int
|
||||
cacheCreation5m int
|
||||
cacheCreation1h int
|
||||
imageOutputTokens int
|
||||
imageOutputCost float64
|
||||
inputCost float64
|
||||
outputCost float64
|
||||
cacheCreationCost float64
|
||||
cacheReadCost float64
|
||||
totalCost float64
|
||||
actualCost float64
|
||||
rateMultiplier float64
|
||||
accountRateMultiplier sql.NullFloat64
|
||||
billingType int16
|
||||
requestTypeRaw int16
|
||||
stream bool
|
||||
openaiWSMode bool
|
||||
durationMs sql.NullInt64
|
||||
firstTokenMs sql.NullInt64
|
||||
userAgent sql.NullString
|
||||
ipAddress sql.NullString
|
||||
imageCount int
|
||||
imageSize sql.NullString
|
||||
imageInputSize sql.NullString
|
||||
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
|
||||
upstreamEndpoint sql.NullString
|
||||
cacheTTLOverridden bool
|
||||
longContextBillingApplied bool
|
||||
channelID sql.NullInt64
|
||||
modelMappingChain sql.NullString
|
||||
billingTier sql.NullString
|
||||
billingMode sql.NullString
|
||||
accountStatsCost sql.NullFloat64
|
||||
createdAt time.Time
|
||||
)
|
||||
|
||||
if err := scanner.Scan(
|
||||
@@ -530,6 +531,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
&inboundEndpoint,
|
||||
&upstreamEndpoint,
|
||||
&cacheTTLOverridden,
|
||||
&longContextBillingApplied,
|
||||
&channelID,
|
||||
&modelMappingChain,
|
||||
&billingTier,
|
||||
@@ -541,34 +543,35 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
|
||||
}
|
||||
|
||||
log := &service.UsageLog{
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
Model: model,
|
||||
RequestedModel: coalesceTrimmedString(requestedModel, model),
|
||||
InputTokens: inputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
CacheCreationTokens: cacheCreationTokens,
|
||||
CacheReadTokens: cacheReadTokens,
|
||||
CacheCreation5mTokens: cacheCreation5m,
|
||||
CacheCreation1hTokens: cacheCreation1h,
|
||||
ImageOutputTokens: imageOutputTokens,
|
||||
ImageOutputCost: imageOutputCost,
|
||||
InputCost: inputCost,
|
||||
OutputCost: outputCost,
|
||||
CacheCreationCost: cacheCreationCost,
|
||||
CacheReadCost: cacheReadCost,
|
||||
TotalCost: totalCost,
|
||||
ActualCost: actualCost,
|
||||
RateMultiplier: rateMultiplier,
|
||||
AccountRateMultiplier: nullFloat64Ptr(accountRateMultiplier),
|
||||
BillingType: int8(billingType),
|
||||
RequestType: service.RequestTypeFromInt16(requestTypeRaw),
|
||||
ImageCount: imageCount,
|
||||
VideoCount: videoCount,
|
||||
CacheTTLOverridden: cacheTTLOverridden,
|
||||
CreatedAt: createdAt,
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
APIKeyID: apiKeyID,
|
||||
AccountID: accountID,
|
||||
Model: model,
|
||||
RequestedModel: coalesceTrimmedString(requestedModel, model),
|
||||
InputTokens: inputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
CacheCreationTokens: cacheCreationTokens,
|
||||
CacheReadTokens: cacheReadTokens,
|
||||
CacheCreation5mTokens: cacheCreation5m,
|
||||
CacheCreation1hTokens: cacheCreation1h,
|
||||
ImageOutputTokens: imageOutputTokens,
|
||||
ImageOutputCost: imageOutputCost,
|
||||
InputCost: inputCost,
|
||||
OutputCost: outputCost,
|
||||
CacheCreationCost: cacheCreationCost,
|
||||
CacheReadCost: cacheReadCost,
|
||||
TotalCost: totalCost,
|
||||
ActualCost: actualCost,
|
||||
RateMultiplier: rateMultiplier,
|
||||
AccountRateMultiplier: nullFloat64Ptr(accountRateMultiplier),
|
||||
BillingType: int8(billingType),
|
||||
RequestType: service.RequestTypeFromInt16(requestTypeRaw),
|
||||
ImageCount: imageCount,
|
||||
VideoCount: videoCount,
|
||||
CacheTTLOverridden: cacheTTLOverridden,
|
||||
LongContextBillingApplied: longContextBillingApplied,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
// 先回填 legacy 字段,再基于 legacy + request_type 计算最终请求类型,保证历史数据兼容。
|
||||
log.Stream = stream
|
||||
|
||||
@@ -88,6 +88,7 @@ func TestUsageLogRepositoryCreateSyncRequestTypeAndLegacyFields(t *testing.T) {
|
||||
sqlmock.AnyArg(), // inbound_endpoint
|
||||
sqlmock.AnyArg(), // upstream_endpoint
|
||||
log.CacheTTLOverridden,
|
||||
log.LongContextBillingApplied,
|
||||
sqlmock.AnyArg(), // channel_id
|
||||
sqlmock.AnyArg(), // model_mapping_chain
|
||||
sqlmock.AnyArg(), // billing_tier
|
||||
@@ -174,6 +175,7 @@ func TestUsageLogRepositoryCreate_PersistsServiceTier(t *testing.T) {
|
||||
sqlmock.AnyArg(),
|
||||
sqlmock.AnyArg(),
|
||||
log.CacheTTLOverridden,
|
||||
log.LongContextBillingApplied,
|
||||
sqlmock.AnyArg(), // channel_id
|
||||
sqlmock.AnyArg(), // model_mapping_chain
|
||||
sqlmock.AnyArg(), // billing_tier
|
||||
@@ -813,6 +815,7 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
false,
|
||||
false,
|
||||
sql.NullInt64{},
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
@@ -884,6 +887,7 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
false,
|
||||
false,
|
||||
sql.NullInt64{}, // channel_id
|
||||
sql.NullString{}, // model_mapping_chain
|
||||
sql.NullString{}, // billing_tier
|
||||
@@ -939,6 +943,7 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
false,
|
||||
false,
|
||||
sql.NullInt64{}, // channel_id
|
||||
sql.NullString{}, // model_mapping_chain
|
||||
sql.NullString{}, // billing_tier
|
||||
@@ -994,6 +999,7 @@ func TestScanUsageLogRequestTypeAndLegacyFallback(t *testing.T) {
|
||||
sql.NullString{},
|
||||
sql.NullString{},
|
||||
false,
|
||||
false,
|
||||
sql.NullInt64{}, // channel_id
|
||||
sql.NullString{}, // model_mapping_chain
|
||||
sql.NullString{}, // billing_tier
|
||||
|
||||
@@ -1191,6 +1191,14 @@ func (a *Account) IsOpenAI() bool {
|
||||
return a.Platform == PlatformOpenAI
|
||||
}
|
||||
|
||||
func (a *Account) IsOpenAILongContextBillingEnabled() bool {
|
||||
if a == nil || a.Extra == nil {
|
||||
return false
|
||||
}
|
||||
enabled, ok := a.Extra["openai_long_context_billing_enabled"].(bool)
|
||||
return ok && enabled
|
||||
}
|
||||
|
||||
func (a *Account) IsAnthropic() bool {
|
||||
return a.Platform == PlatformAnthropic
|
||||
}
|
||||
|
||||
@@ -153,14 +153,15 @@ type UsageTokens struct {
|
||||
|
||||
// CostBreakdown 费用明细
|
||||
type CostBreakdown struct {
|
||||
InputCost float64
|
||||
OutputCost float64
|
||||
ImageOutputCost float64
|
||||
CacheCreationCost float64
|
||||
CacheReadCost float64
|
||||
TotalCost float64
|
||||
ActualCost float64 // 应用倍率后的实际费用
|
||||
BillingMode string // 计费模式("token"/"per_request"/"image"),由 CalculateCostUnified 填充
|
||||
InputCost float64
|
||||
OutputCost float64
|
||||
ImageOutputCost float64
|
||||
CacheCreationCost float64
|
||||
CacheReadCost float64
|
||||
TotalCost float64
|
||||
ActualCost float64 // 应用倍率后的实际费用
|
||||
BillingMode string // 计费模式("token"/"per_request"/"image"),由 CalculateCostUnified 填充
|
||||
LongContextBillingApplied bool
|
||||
}
|
||||
|
||||
// ErrModelPricingUnavailable indicates that none of the configured pricing
|
||||
@@ -856,16 +857,17 @@ func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing
|
||||
|
||||
// CostInput 统一计费输入
|
||||
type CostInput struct {
|
||||
Ctx context.Context
|
||||
Model string
|
||||
GroupID *int64 // 用于渠道定价查找
|
||||
Tokens UsageTokens
|
||||
RequestCount int // 按次计费时使用
|
||||
SizeTier string // 按次/图片模式的层级标签("1K","2K","4K","HD" 等)
|
||||
RateMultiplier float64
|
||||
ServiceTier string // "priority","flex","" 等
|
||||
Resolver *ModelPricingResolver // 定价解析器
|
||||
Resolved *ResolvedPricing // 可选:预解析的定价结果(避免重复 Resolve 调用)
|
||||
Ctx context.Context
|
||||
Model string
|
||||
GroupID *int64 // 用于渠道定价查找
|
||||
Tokens UsageTokens
|
||||
RequestCount int // 按次计费时使用
|
||||
SizeTier string // 按次/图片模式的层级标签("1K","2K","4K","HD" 等)
|
||||
RateMultiplier float64
|
||||
ServiceTier string // "priority","flex","" 等
|
||||
Resolver *ModelPricingResolver // 定价解析器
|
||||
Resolved *ResolvedPricing // 可选:预解析的定价结果(避免重复 Resolve 调用)
|
||||
LongContextBillingEnabled *bool
|
||||
}
|
||||
|
||||
// CalculateCostUnified 统一计费入口,支持三种计费模式。
|
||||
@@ -873,7 +875,18 @@ type CostInput struct {
|
||||
func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown, error) {
|
||||
if input.Resolver == nil {
|
||||
// 无 Resolver,回退到旧路径
|
||||
return s.calculateCostInternal(input.Model, input.Tokens, input.RateMultiplier, input.ServiceTier, nil)
|
||||
applyLongContextBilling := true
|
||||
if input.LongContextBillingEnabled != nil {
|
||||
applyLongContextBilling = *input.LongContextBillingEnabled
|
||||
}
|
||||
return s.calculateCostInternalWithPolicy(
|
||||
input.Model,
|
||||
input.Tokens,
|
||||
input.RateMultiplier,
|
||||
input.ServiceTier,
|
||||
nil,
|
||||
applyLongContextBilling,
|
||||
)
|
||||
}
|
||||
|
||||
// 优先使用预解析结果,避免重复 Resolve 调用
|
||||
@@ -920,6 +933,9 @@ func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input Cos
|
||||
|
||||
// 长上下文定价仅在无区间定价时应用(区间定价已包含上下文分层)
|
||||
applyLongCtx := len(resolved.Intervals) == 0
|
||||
if input.LongContextBillingEnabled != nil {
|
||||
applyLongCtx = applyLongCtx && *input.LongContextBillingEnabled
|
||||
}
|
||||
|
||||
return s.computeTokenBreakdown(pricing, input.Tokens, input.RateMultiplier, input.ServiceTier, applyLongCtx), nil
|
||||
}
|
||||
@@ -960,7 +976,10 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
tierMultiplier = serviceTierCostMultiplier(serviceTier)
|
||||
}
|
||||
|
||||
if applyLongCtx && s.shouldApplySessionLongContextPricing(tokens, pricing) {
|
||||
longContextPricingEligible := applyLongCtx && s.shouldApplySessionLongContextPricing(tokens, pricing)
|
||||
var baselineCost *CostBreakdown
|
||||
if longContextPricingEligible {
|
||||
baselineCost = s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, false)
|
||||
inputPrice *= pricing.LongContextInputMultiplier
|
||||
outputPrice *= pricing.LongContextOutputMultiplier
|
||||
// 缓存读取本质上是输入侧的复用,应与 input 一同应用长上下文倍率;
|
||||
@@ -1024,6 +1043,7 @@ func (s *BillingService) computeTokenBreakdown(
|
||||
bd.TotalCost = bd.InputCost + bd.OutputCost + bd.ImageOutputCost +
|
||||
bd.CacheCreationCost + bd.CacheReadCost
|
||||
bd.ActualCost = bd.TotalCost * rateMultiplier
|
||||
bd.LongContextBillingApplied = baselineCost != nil && bd.ActualCost > baselineCost.ActualCost
|
||||
|
||||
return bd
|
||||
}
|
||||
@@ -1083,7 +1103,28 @@ func (s *BillingService) CalculateCostWithServiceTier(model string, tokens Usage
|
||||
return s.calculateCostInternal(model, tokens, rateMultiplier, serviceTier, nil)
|
||||
}
|
||||
|
||||
func (s *BillingService) calculateCostWithServiceTierPolicy(
|
||||
model string,
|
||||
tokens UsageTokens,
|
||||
rateMultiplier float64,
|
||||
serviceTier string,
|
||||
longContextBillingEnabled bool,
|
||||
) (*CostBreakdown, error) {
|
||||
return s.calculateCostInternalWithPolicy(model, tokens, rateMultiplier, serviceTier, nil, longContextBillingEnabled)
|
||||
}
|
||||
|
||||
func (s *BillingService) calculateCostInternal(model string, tokens UsageTokens, rateMultiplier float64, serviceTier string, channelPricing *ChannelModelPricing) (*CostBreakdown, error) {
|
||||
return s.calculateCostInternalWithPolicy(model, tokens, rateMultiplier, serviceTier, channelPricing, true)
|
||||
}
|
||||
|
||||
func (s *BillingService) calculateCostInternalWithPolicy(
|
||||
model string,
|
||||
tokens UsageTokens,
|
||||
rateMultiplier float64,
|
||||
serviceTier string,
|
||||
channelPricing *ChannelModelPricing,
|
||||
longContextBillingEnabled bool,
|
||||
) (*CostBreakdown, error) {
|
||||
var pricing *ModelPricing
|
||||
var err error
|
||||
if channelPricing != nil {
|
||||
@@ -1095,8 +1136,7 @@ func (s *BillingService) calculateCostInternal(model string, tokens UsageTokens,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 旧路径始终检查长上下文定价(无区间定价概念)
|
||||
return s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, true), nil
|
||||
return s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, longContextBillingEnabled), nil
|
||||
}
|
||||
|
||||
func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *ModelPricing) *ModelPricing {
|
||||
@@ -1227,13 +1267,14 @@ func (s *BillingService) CalculateCostWithLongContext(model string, tokens Usage
|
||||
|
||||
// 合并成本
|
||||
return &CostBreakdown{
|
||||
InputCost: inRangeCost.InputCost + outRangeCost.InputCost,
|
||||
OutputCost: inRangeCost.OutputCost,
|
||||
ImageOutputCost: inRangeCost.ImageOutputCost,
|
||||
CacheCreationCost: inRangeCost.CacheCreationCost,
|
||||
CacheReadCost: inRangeCost.CacheReadCost + outRangeCost.CacheReadCost,
|
||||
TotalCost: inRangeCost.TotalCost + outRangeCost.TotalCost,
|
||||
ActualCost: inRangeCost.ActualCost + outRangeCost.ActualCost,
|
||||
InputCost: inRangeCost.InputCost + outRangeCost.InputCost,
|
||||
OutputCost: inRangeCost.OutputCost,
|
||||
ImageOutputCost: inRangeCost.ImageOutputCost,
|
||||
CacheCreationCost: inRangeCost.CacheCreationCost,
|
||||
CacheReadCost: inRangeCost.CacheReadCost + outRangeCost.CacheReadCost,
|
||||
TotalCost: inRangeCost.TotalCost + outRangeCost.TotalCost,
|
||||
ActualCost: inRangeCost.ActualCost + outRangeCost.ActualCost,
|
||||
LongContextBillingApplied: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -261,6 +261,23 @@ func TestCalculateCost_OpenAIGPT54LongContextAppliesWholeSessionMultipliers(t *t
|
||||
require.InDelta(t, expectedOutput, cost.OutputCost, 1e-10)
|
||||
require.InDelta(t, expectedInput+expectedOutput, cost.TotalCost, 1e-10)
|
||||
require.InDelta(t, expectedInput+expectedOutput, cost.ActualCost, 1e-10)
|
||||
require.True(t, cost.LongContextBillingApplied)
|
||||
}
|
||||
|
||||
func TestCalculateCost_OpenAIGPT54LongContextMarkerRequiresActualCostIncrease(t *testing.T) {
|
||||
svc := newTestBillingService()
|
||||
|
||||
cost, err := svc.calculateCostWithServiceTierPolicy(
|
||||
"gpt-5.4-2026-03-05",
|
||||
UsageTokens{InputTokens: 300000},
|
||||
0,
|
||||
"",
|
||||
true,
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, cost.ActualCost)
|
||||
require.False(t, cost.LongContextBillingApplied)
|
||||
}
|
||||
|
||||
func TestCalculateCost_OpenAIGPT55ProUsesGPT55PricingPolicy(t *testing.T) {
|
||||
|
||||
@@ -947,6 +947,7 @@ func (s *GatewayService) buildRecordUsageLog(
|
||||
usageLog.CacheReadCost = cost.CacheReadCost
|
||||
usageLog.TotalCost = cost.TotalCost
|
||||
usageLog.ActualCost = cost.ActualCost
|
||||
usageLog.LongContextBillingApplied = cost.LongContextBillingApplied
|
||||
}
|
||||
|
||||
return usageLog
|
||||
|
||||
@@ -1045,7 +1045,7 @@ func TestOpenAIGatewayServiceRecordUsage_GPT56SeparatesCacheWriteForBillingAndSt
|
||||
require.InDelta(t, usageRepo.lastLog.TotalCost*1.1, usageRepo.lastLog.ActualCost, 1e-12)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_Gpt54LongContextBillsWholeSession(t *testing.T) {
|
||||
func TestOpenAIGatewayServiceRecordUsage_Gpt54LongContextBillingDisabledByDefault(t *testing.T) {
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
userRepo := &openAIRecordUsageUserRepoStub{}
|
||||
subRepo := &openAIRecordUsageSubRepoStub{}
|
||||
@@ -1069,13 +1069,50 @@ func TestOpenAIGatewayServiceRecordUsage_Gpt54LongContextBillsWholeSession(t *te
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, usageRepo.lastLog)
|
||||
|
||||
expectedInput := 300000 * 2.5e-6
|
||||
expectedOutput := 2000 * 15e-6
|
||||
require.InDelta(t, expectedInput, usageRepo.lastLog.InputCost, 1e-10)
|
||||
require.InDelta(t, expectedOutput, usageRepo.lastLog.OutputCost, 1e-10)
|
||||
require.InDelta(t, expectedInput+expectedOutput, usageRepo.lastLog.TotalCost, 1e-10)
|
||||
require.InDelta(t, (expectedInput+expectedOutput)*1.1, usageRepo.lastLog.ActualCost, 1e-10)
|
||||
require.False(t, usageRepo.lastLog.LongContextBillingApplied)
|
||||
require.Equal(t, 1, userRepo.deductCalls)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_Gpt54LongContextBillingEnabledPerAccount(t *testing.T) {
|
||||
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
|
||||
userRepo := &openAIRecordUsageUserRepoStub{}
|
||||
subRepo := &openAIRecordUsageSubRepoStub{}
|
||||
svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil)
|
||||
|
||||
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
|
||||
Result: &OpenAIForwardResult{
|
||||
RequestID: "resp_gpt54_long_context_enabled",
|
||||
Usage: OpenAIUsage{
|
||||
InputTokens: 300000,
|
||||
OutputTokens: 2000,
|
||||
},
|
||||
Model: "gpt-5.4-2026-03-05",
|
||||
Duration: time.Second,
|
||||
},
|
||||
APIKey: &APIKey{ID: 1015},
|
||||
User: &User{ID: 2015},
|
||||
Account: &Account{
|
||||
ID: 3015,
|
||||
Extra: map[string]any{"openai_long_context_billing_enabled": true},
|
||||
},
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, usageRepo.lastLog)
|
||||
|
||||
expectedInput := 300000 * 2.5e-6 * 2.0
|
||||
expectedOutput := 2000 * 15e-6 * 1.5
|
||||
require.InDelta(t, expectedInput, usageRepo.lastLog.InputCost, 1e-10)
|
||||
require.InDelta(t, expectedOutput, usageRepo.lastLog.OutputCost, 1e-10)
|
||||
require.InDelta(t, expectedInput+expectedOutput, usageRepo.lastLog.TotalCost, 1e-10)
|
||||
require.InDelta(t, (expectedInput+expectedOutput)*1.1, usageRepo.lastLog.ActualCost, 1e-10)
|
||||
require.Equal(t, 1, userRepo.deductCalls)
|
||||
require.True(t, usageRepo.lastLog.LongContextBillingApplied)
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayServiceRecordUsage_ServiceTierPriorityUsesFastPricing(t *testing.T) {
|
||||
|
||||
@@ -178,7 +178,19 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
if result.ServiceTier != nil {
|
||||
serviceTier = strings.TrimSpace(*result.ServiceTier)
|
||||
}
|
||||
cost, err = s.calculateOpenAIRecordUsageCost(ctx, result, apiKey, billingModels, multiplier, imageMultiplier, videoMultiplier, tokens, serviceTier)
|
||||
longContextBillingEnabled := account.IsOpenAILongContextBillingEnabled()
|
||||
cost, err = s.calculateOpenAIRecordUsageCost(
|
||||
ctx,
|
||||
result,
|
||||
apiKey,
|
||||
billingModels,
|
||||
multiplier,
|
||||
imageMultiplier,
|
||||
videoMultiplier,
|
||||
tokens,
|
||||
serviceTier,
|
||||
longContextBillingEnabled,
|
||||
)
|
||||
if err != nil {
|
||||
if !isUsagePricingUnavailableError(err) {
|
||||
return err
|
||||
@@ -257,6 +269,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
|
||||
usageLog.CacheReadCost = cost.CacheReadCost
|
||||
usageLog.TotalCost = cost.TotalCost
|
||||
usageLog.ActualCost = cost.ActualCost
|
||||
usageLog.LongContextBillingApplied = cost.LongContextBillingApplied
|
||||
}
|
||||
if isVideoUsage && (cost == nil || cost.BillingMode != string(BillingModeToken)) {
|
||||
usageLog.RateMultiplier = videoMultiplier
|
||||
@@ -365,6 +378,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost(
|
||||
videoMultiplier float64,
|
||||
tokens UsageTokens,
|
||||
serviceTier string,
|
||||
longContextBillingEnabled bool,
|
||||
) (*CostBreakdown, error) {
|
||||
billingModel := firstUsageBillingModel(billingModels)
|
||||
if isGrokVideoUsageResult(result, billingModels) {
|
||||
@@ -387,7 +401,15 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost(
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
cost, err := s.calculateOpenAIRecordUsageTokenCost(ctx, apiKey, candidate, multiplier, tokens, serviceTier)
|
||||
cost, err := s.calculateOpenAIRecordUsageTokenCost(
|
||||
ctx,
|
||||
apiKey,
|
||||
candidate,
|
||||
multiplier,
|
||||
tokens,
|
||||
serviceTier,
|
||||
longContextBillingEnabled,
|
||||
)
|
||||
if err == nil {
|
||||
return cost, nil
|
||||
}
|
||||
@@ -435,21 +457,29 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost(
|
||||
multiplier float64,
|
||||
tokens UsageTokens,
|
||||
serviceTier string,
|
||||
longContextBillingEnabled bool,
|
||||
) (*CostBreakdown, error) {
|
||||
if s.resolver != nil && apiKey.Group != nil {
|
||||
gid := apiKey.Group.ID
|
||||
return s.billingService.CalculateCostUnified(CostInput{
|
||||
Ctx: ctx,
|
||||
Model: billingModel,
|
||||
GroupID: &gid,
|
||||
Tokens: tokens,
|
||||
RequestCount: 1,
|
||||
RateMultiplier: multiplier,
|
||||
ServiceTier: serviceTier,
|
||||
Resolver: s.resolver,
|
||||
Ctx: ctx,
|
||||
Model: billingModel,
|
||||
GroupID: &gid,
|
||||
Tokens: tokens,
|
||||
RequestCount: 1,
|
||||
RateMultiplier: multiplier,
|
||||
ServiceTier: serviceTier,
|
||||
Resolver: s.resolver,
|
||||
LongContextBillingEnabled: &longContextBillingEnabled,
|
||||
})
|
||||
}
|
||||
return s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, serviceTier)
|
||||
return s.billingService.calculateCostWithServiceTierPolicy(
|
||||
billingModel,
|
||||
tokens,
|
||||
multiplier,
|
||||
serviceTier,
|
||||
longContextBillingEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) calculateOpenAIImageCost(
|
||||
|
||||
@@ -142,13 +142,14 @@ type UsageLog struct {
|
||||
ImageOutputTokens int
|
||||
ImageOutputCost float64
|
||||
|
||||
InputCost float64
|
||||
OutputCost float64
|
||||
CacheCreationCost float64
|
||||
CacheReadCost float64
|
||||
TotalCost float64
|
||||
ActualCost float64
|
||||
RateMultiplier float64
|
||||
InputCost float64
|
||||
OutputCost float64
|
||||
CacheCreationCost float64
|
||||
CacheReadCost float64
|
||||
TotalCost float64
|
||||
ActualCost float64
|
||||
RateMultiplier float64
|
||||
LongContextBillingApplied bool
|
||||
// AccountRateMultiplier 账号计费倍率快照(nil 表示历史数据,按 1.0 处理)
|
||||
AccountRateMultiplier *float64
|
||||
// AccountStatsCost 账号统计定价预计算费用(nil = 使用默认公式 total_cost × account_rate_multiplier)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Snapshot whether long-context pricing changed token prices for a request so
|
||||
-- usage history can explain the applied charge without inferring from totals.
|
||||
ALTER TABLE usage_logs
|
||||
ADD COLUMN IF NOT EXISTS long_context_billing_applied BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -2796,6 +2796,38 @@
|
||||
</div>
|
||||
|
||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||
<div
|
||||
v-if="form.platform === 'openai' && (accountCategory === 'oauth-based' || accountCategory === 'apikey')"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.longContextBilling') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.openai.longContextBillingDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="openai-long-context-billing-toggle"
|
||||
role="switch"
|
||||
:aria-checked="openAILongContextBillingEnabled"
|
||||
@click="openAILongContextBillingEnabled = !openAILongContextBillingEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
openAILongContextBillingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
openAILongContextBillingEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="form.platform === 'openai' && accountCategory === 'oauth-based'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
@@ -3648,6 +3680,7 @@ const fillHeaderOverrideTemplate = () => {
|
||||
const interceptWarmupRequests = ref(false)
|
||||
const autoPauseOnExpired = ref(true)
|
||||
const openaiPassthroughEnabled = ref(false)
|
||||
const openAILongContextBillingEnabled = ref(false)
|
||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
|
||||
const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_completions', 'embeddings'])
|
||||
@@ -4502,6 +4535,7 @@ const resetForm = () => {
|
||||
interceptWarmupRequests.value = false
|
||||
autoPauseOnExpired.value = true
|
||||
openaiPassthroughEnabled.value = false
|
||||
openAILongContextBillingEnabled.value = false
|
||||
openAICompactMode.value = 'auto'
|
||||
openAIResponsesMode.value = 'auto'
|
||||
openAIEndpointCapabilities.value = ['chat_completions', 'embeddings']
|
||||
@@ -4584,6 +4618,11 @@ const buildOpenAIExtra = (base?: Record<string, unknown>): Record<string, unknow
|
||||
delete extra.openai_passthrough
|
||||
delete extra.openai_oauth_passthrough
|
||||
}
|
||||
if (openAILongContextBillingEnabled.value) {
|
||||
extra.openai_long_context_billing_enabled = true
|
||||
} else {
|
||||
delete extra.openai_long_context_billing_enabled
|
||||
}
|
||||
|
||||
if (accountCategory.value === 'oauth-based' && codexCLIOnlyEnabled.value) {
|
||||
extra.codex_cli_only = true
|
||||
|
||||
@@ -1783,6 +1783,38 @@
|
||||
</div>
|
||||
|
||||
<!-- OpenAI OAuth Codex 官方客户端限制开关 -->
|
||||
<div
|
||||
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token' || account?.type === 'apikey')"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.openai.longContextBilling') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.openai.longContextBillingDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="openai-long-context-billing-toggle"
|
||||
role="switch"
|
||||
:aria-checked="openAILongContextBillingEnabled"
|
||||
@click="openAILongContextBillingEnabled = !openAILongContextBillingEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
openAILongContextBillingEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
openAILongContextBillingEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="account?.platform === 'openai' && (account?.type === 'oauth' || account?.type === 'setup-token')"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
@@ -2757,6 +2789,7 @@ const customBaseUrl = ref('')
|
||||
|
||||
// OpenAI 自动透传开关(OAuth/API Key)
|
||||
const openaiPassthroughEnabled = ref(false)
|
||||
const openAILongContextBillingEnabled = ref(false)
|
||||
const openAICompactMode = ref<OpenAICompactMode>('auto')
|
||||
const openAIResponsesMode = ref<OpenAIResponsesMode>('auto')
|
||||
const openAIEndpointCapabilities = ref<OpenAIEndpointCapability[]>(['chat_completions', 'embeddings'])
|
||||
@@ -3183,6 +3216,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
|
||||
// Load OpenAI passthrough toggle (OpenAI OAuth/SetupToken/API Key)
|
||||
openaiPassthroughEnabled.value = false
|
||||
openAILongContextBillingEnabled.value = false
|
||||
openAICompactMode.value = 'auto'
|
||||
openAIResponsesMode.value = 'auto'
|
||||
openAIEndpointCapabilities.value = ['chat_completions', 'embeddings']
|
||||
@@ -3197,6 +3231,7 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
webSearchEmulationMode.value = 'default'
|
||||
if (newAccount.platform === 'openai' && (newAccount.type === 'oauth' || newAccount.type === 'setup-token' || newAccount.type === 'apikey')) {
|
||||
openaiPassthroughEnabled.value = extra?.openai_passthrough === true || extra?.openai_oauth_passthrough === true
|
||||
openAILongContextBillingEnabled.value = extra?.openai_long_context_billing_enabled === true
|
||||
openAICompactMode.value = (extra?.openai_compact_mode as OpenAICompactMode) || 'auto'
|
||||
if (newAccount.type === 'apikey') {
|
||||
openAIResponsesMode.value = normalizeOpenAIResponsesMode(extra?.openai_responses_mode)
|
||||
@@ -4351,6 +4386,7 @@ const handleSubmit = async () => {
|
||||
delete newExtra.openai_passthrough
|
||||
delete newExtra.openai_oauth_passthrough
|
||||
}
|
||||
newExtra.openai_long_context_billing_enabled = openAILongContextBillingEnabled.value
|
||||
if (openAICompactMode.value === 'auto') {
|
||||
delete newExtra.openai_compact_mode
|
||||
} else {
|
||||
|
||||
@@ -383,6 +383,27 @@ describe('EditAccountModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads and submits the per-account OpenAI long-context billing toggle', async () => {
|
||||
const account = buildAccount()
|
||||
account.extra = {
|
||||
openai_long_context_billing_enabled: true
|
||||
}
|
||||
updateAccountMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockReset()
|
||||
checkMixedChannelRiskMock.mockResolvedValue({ has_risk: false })
|
||||
updateAccountMock.mockResolvedValue(account)
|
||||
|
||||
const wrapper = mountModal(account)
|
||||
const toggle = wrapper.get('[data-testid="openai-long-context-billing-toggle"]')
|
||||
expect(toggle.attributes('aria-checked')).toBe('true')
|
||||
|
||||
await toggle.trigger('click')
|
||||
await wrapper.get('form#edit-account-form').trigger('submit.prevent')
|
||||
|
||||
expect(updateAccountMock).toHaveBeenCalledTimes(1)
|
||||
expect(updateAccountMock.mock.calls[0]?.[1]?.extra?.openai_long_context_billing_enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('loads and submits Grok OAuth model mapping edits', async () => {
|
||||
const account = buildGrokOAuthAccount()
|
||||
updateAccountMock.mockReset()
|
||||
|
||||
@@ -168,6 +168,11 @@
|
||||
<div class="text-sm">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="font-medium text-green-600 dark:text-green-400">${{ row.actual_cost?.toFixed(6) || '0.000000' }}</span>
|
||||
<span
|
||||
v-if="row.long_context_billing_applied"
|
||||
data-testid="long-context-billing-marker"
|
||||
class="inline-flex items-center rounded px-1 py-px text-[10px] font-semibold leading-tight bg-amber-100 text-amber-700 ring-1 ring-inset ring-amber-200 dark:bg-amber-500/20 dark:text-amber-300 dark:ring-amber-500/30"
|
||||
>x2</span>
|
||||
<!-- Cost Detail Tooltip -->
|
||||
<div
|
||||
class="group relative"
|
||||
|
||||
@@ -120,6 +120,38 @@ describe('admin UsageTable tooltip', () => {
|
||||
} as DOMRect)
|
||||
})
|
||||
|
||||
it('marks only usage rows that actually applied long-context billing', () => {
|
||||
const wrapper = mount(UsageTable, {
|
||||
props: {
|
||||
data: [
|
||||
{
|
||||
...baseImageRow,
|
||||
request_id: 'req-long-context-enabled',
|
||||
long_context_billing_applied: true,
|
||||
},
|
||||
{
|
||||
...baseImageRow,
|
||||
request_id: 'req-long-context-disabled',
|
||||
long_context_billing_applied: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
columns: [],
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
DataTable: DataTableStub,
|
||||
EmptyState: true,
|
||||
Icon: true,
|
||||
Teleport: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('[data-testid="long-context-billing-marker"]')).toHaveLength(1)
|
||||
expect(wrapper.get('[data-testid="long-context-billing-marker"]').text()).toBe('x2')
|
||||
})
|
||||
|
||||
it('shows service tier and billing breakdown in cost tooltip', async () => {
|
||||
const row = {
|
||||
request_id: 'req-admin-1',
|
||||
|
||||
@@ -402,6 +402,9 @@ export default {
|
||||
oauthPassthrough: 'Auto passthrough (auth only)',
|
||||
oauthPassthroughDesc:
|
||||
'When enabled, this OpenAI account uses automatic passthrough: the gateway forwards request/response as-is and only swaps auth, while keeping billing/concurrency/audit and necessary safety filtering.',
|
||||
longContextBilling: 'API long-context pricing',
|
||||
longContextBillingDesc:
|
||||
'Disabled by default. Enable only when this account\'s upstream charges OpenAI API long-context rates above the model threshold.',
|
||||
responsesWebsocketsV2: 'Responses WebSocket v2',
|
||||
responsesWebsocketsV2Desc:
|
||||
'Disabled by default. Enable to allow responses_websockets_v2 capability (still gated by global and account-type switches).',
|
||||
|
||||
@@ -505,6 +505,8 @@ export default {
|
||||
oauthPassthrough: '自动透传(仅替换认证)',
|
||||
oauthPassthroughDesc:
|
||||
'开启后,该 OpenAI 账号将自动透传请求与响应,仅替换认证并保留计费/并发/审计及必要安全过滤;如遇兼容性问题可随时关闭回滚。',
|
||||
longContextBilling: 'API 长上下文计费',
|
||||
longContextBillingDesc: '默认关闭。仅当该账号的上游会按模型阈值收取 OpenAI API 长上下文费率时启用。',
|
||||
responsesWebsocketsV2: 'Responses WebSocket v2',
|
||||
responsesWebsocketsV2Desc:
|
||||
'默认关闭。开启后可启用 responses_websockets_v2 协议能力(受网关全局开关与账号类型开关约束)。',
|
||||
|
||||
@@ -1344,6 +1344,7 @@ export interface UsageLog {
|
||||
total_cost: number
|
||||
actual_cost: number
|
||||
rate_multiplier: number
|
||||
long_context_billing_applied: boolean
|
||||
billing_type: number
|
||||
|
||||
request_type?: UsageRequestType
|
||||
|
||||
Reference in New Issue
Block a user