diff --git a/backend/ent/group.go b/backend/ent/group.go index 315541636d..e1a0d3a670 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -89,6 +89,12 @@ type Group struct { VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"` // Codex alpha/search 网页搜索单次价格(USD/次);nil 表示使用默认价 0.01(官方 $10/1000 次) WebSearchPricePerCall *float64 `json:"web_search_price_per_call,omitempty"` + // Voice realtime 每分钟价格(USD) + AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min,omitempty"` + // TTS 每百万字符价格(USD) + AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"` + // STT 每小时价格(USD) + AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"` // 是否仅允许 Claude Code 客户端 ClaudeCodeOnly bool `json:"claude_code_only,omitempty"` // 非 Claude Code 请求降级使用的分组 ID @@ -241,7 +247,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { values[i] = new([]byte) case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled: values[i] = new(sql.NullBool) - case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldBatchImageDiscountMultiplier, group.FieldBatchImageHoldMultiplier, group.FieldVideoRateMultiplier, group.FieldVideoPrice480p, group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, group.FieldProfitMinMargin, group.FieldProfitSafetyBuffer: + case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldBatchImageDiscountMultiplier, group.FieldBatchImageHoldMultiplier, group.FieldVideoRateMultiplier, group.FieldVideoPrice480p, group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, group.FieldAudioRealtimePricePerMin, group.FieldAudioTtsPricePerMillionChars, group.FieldAudioSttPricePerHour, group.FieldProfitMinMargin, group.FieldProfitSafetyBuffer: values[i] = new(sql.NullFloat64) case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit: values[i] = new(sql.NullInt64) @@ -495,6 +501,27 @@ func (_m *Group) assignValues(columns []string, values []any) error { _m.WebSearchPricePerCall = new(float64) *_m.WebSearchPricePerCall = value.Float64 } + case group.FieldAudioRealtimePricePerMin: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field audio_realtime_price_per_min", values[i]) + } else if value.Valid { + _m.AudioRealtimePricePerMin = new(float64) + *_m.AudioRealtimePricePerMin = value.Float64 + } + case group.FieldAudioTtsPricePerMillionChars: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field audio_tts_price_per_million_chars", values[i]) + } else if value.Valid { + _m.AudioTtsPricePerMillionChars = new(float64) + *_m.AudioTtsPricePerMillionChars = value.Float64 + } + case group.FieldAudioSttPricePerHour: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field audio_stt_price_per_hour", values[i]) + } else if value.Valid { + _m.AudioSttPricePerHour = new(float64) + *_m.AudioSttPricePerHour = value.Float64 + } case group.FieldClaudeCodeOnly: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field claude_code_only", values[i]) @@ -840,6 +867,21 @@ func (_m *Group) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.AudioRealtimePricePerMin; v != nil { + builder.WriteString("audio_realtime_price_per_min=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.AudioTtsPricePerMillionChars; v != nil { + builder.WriteString("audio_tts_price_per_million_chars=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.AudioSttPricePerHour; v != nil { + builder.WriteString("audio_stt_price_per_hour=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") builder.WriteString("claude_code_only=") builder.WriteString(fmt.Sprintf("%v", _m.ClaudeCodeOnly)) builder.WriteString(", ") diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 44aa596a75..4c7b002647 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -86,6 +86,12 @@ const ( FieldVideoModelPrices = "video_model_prices" // FieldWebSearchPricePerCall holds the string denoting the web_search_price_per_call field in the database. FieldWebSearchPricePerCall = "web_search_price_per_call" + // FieldAudioRealtimePricePerMin holds the string denoting the audio_realtime_price_per_min field in the database. + FieldAudioRealtimePricePerMin = "audio_realtime_price_per_min" + // FieldAudioTtsPricePerMillionChars holds the string denoting the audio_tts_price_per_million_chars field in the database. + FieldAudioTtsPricePerMillionChars = "audio_tts_price_per_million_chars" + // FieldAudioSttPricePerHour holds the string denoting the audio_stt_price_per_hour field in the database. + FieldAudioSttPricePerHour = "audio_stt_price_per_hour" // FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database. FieldClaudeCodeOnly = "claude_code_only" // FieldFallbackGroupID holds the string denoting the fallback_group_id field in the database. @@ -238,6 +244,9 @@ var Columns = []string{ FieldVideoPrice1080p, FieldVideoModelPrices, FieldWebSearchPricePerCall, + FieldAudioRealtimePricePerMin, + FieldAudioTtsPricePerMillionChars, + FieldAudioSttPricePerHour, FieldClaudeCodeOnly, FieldFallbackGroupID, FieldFallbackGroupIDOnInvalidRequest, @@ -344,6 +353,12 @@ var ( DefaultVideoRateIndependent bool // DefaultVideoRateMultiplier holds the default value on creation for the "video_rate_multiplier" field. DefaultVideoRateMultiplier float64 + // AudioRealtimePricePerMinValidator is a validator for the "audio_realtime_price_per_min" field. It is called by the builders before save. + AudioRealtimePricePerMinValidator func(float64) error + // AudioTtsPricePerMillionCharsValidator is a validator for the "audio_tts_price_per_million_chars" field. It is called by the builders before save. + AudioTtsPricePerMillionCharsValidator func(float64) error + // AudioSttPricePerHourValidator is a validator for the "audio_stt_price_per_hour" field. It is called by the builders before save. + AudioSttPricePerHourValidator func(float64) error // DefaultClaudeCodeOnly holds the default value on creation for the "claude_code_only" field. DefaultClaudeCodeOnly bool // DefaultModelRoutingEnabled holds the default value on creation for the "model_routing_enabled" field. @@ -564,6 +579,21 @@ func ByWebSearchPricePerCall(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldWebSearchPricePerCall, opts...).ToFunc() } +// ByAudioRealtimePricePerMin orders the results by the audio_realtime_price_per_min field. +func ByAudioRealtimePricePerMin(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAudioRealtimePricePerMin, opts...).ToFunc() +} + +// ByAudioTtsPricePerMillionChars orders the results by the audio_tts_price_per_million_chars field. +func ByAudioTtsPricePerMillionChars(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAudioTtsPricePerMillionChars, opts...).ToFunc() +} + +// ByAudioSttPricePerHour orders the results by the audio_stt_price_per_hour field. +func ByAudioSttPricePerHour(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAudioSttPricePerHour, opts...).ToFunc() +} + // ByClaudeCodeOnly orders the results by the claude_code_only field. func ByClaudeCodeOnly(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldClaudeCodeOnly, opts...).ToFunc() diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index 91df1b27ba..2ff0359e3b 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -225,6 +225,21 @@ func WebSearchPricePerCall(v float64) predicate.Group { return predicate.Group(sql.FieldEQ(FieldWebSearchPricePerCall, v)) } +// AudioRealtimePricePerMin applies equality check predicate on the "audio_realtime_price_per_min" field. It's identical to AudioRealtimePricePerMinEQ. +func AudioRealtimePricePerMin(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioRealtimePricePerMin, v)) +} + +// AudioTtsPricePerMillionChars applies equality check predicate on the "audio_tts_price_per_million_chars" field. It's identical to AudioTtsPricePerMillionCharsEQ. +func AudioTtsPricePerMillionChars(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioSttPricePerHour applies equality check predicate on the "audio_stt_price_per_hour" field. It's identical to AudioSttPricePerHourEQ. +func AudioSttPricePerHour(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioSttPricePerHour, v)) +} + // ClaudeCodeOnly applies equality check predicate on the "claude_code_only" field. It's identical to ClaudeCodeOnlyEQ. func ClaudeCodeOnly(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v)) @@ -1825,6 +1840,156 @@ func WebSearchPricePerCallNotNil() predicate.Group { return predicate.Group(sql.FieldNotNull(FieldWebSearchPricePerCall)) } +// AudioRealtimePricePerMinEQ applies the EQ predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinNEQ applies the NEQ predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinIn applies the In predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldAudioRealtimePricePerMin, vs...)) +} + +// AudioRealtimePricePerMinNotIn applies the NotIn predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldAudioRealtimePricePerMin, vs...)) +} + +// AudioRealtimePricePerMinGT applies the GT predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinGTE applies the GTE predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinLT applies the LT predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinLTE applies the LTE predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldAudioRealtimePricePerMin, v)) +} + +// AudioRealtimePricePerMinIsNil applies the IsNil predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldAudioRealtimePricePerMin)) +} + +// AudioRealtimePricePerMinNotNil applies the NotNil predicate on the "audio_realtime_price_per_min" field. +func AudioRealtimePricePerMinNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldAudioRealtimePricePerMin)) +} + +// AudioTtsPricePerMillionCharsEQ applies the EQ predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsNEQ applies the NEQ predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsIn applies the In predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldAudioTtsPricePerMillionChars, vs...)) +} + +// AudioTtsPricePerMillionCharsNotIn applies the NotIn predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldAudioTtsPricePerMillionChars, vs...)) +} + +// AudioTtsPricePerMillionCharsGT applies the GT predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsGTE applies the GTE predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsLT applies the LT predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsLTE applies the LTE predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldAudioTtsPricePerMillionChars, v)) +} + +// AudioTtsPricePerMillionCharsIsNil applies the IsNil predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldAudioTtsPricePerMillionChars)) +} + +// AudioTtsPricePerMillionCharsNotNil applies the NotNil predicate on the "audio_tts_price_per_million_chars" field. +func AudioTtsPricePerMillionCharsNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldAudioTtsPricePerMillionChars)) +} + +// AudioSttPricePerHourEQ applies the EQ predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourNEQ applies the NEQ predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourIn applies the In predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldAudioSttPricePerHour, vs...)) +} + +// AudioSttPricePerHourNotIn applies the NotIn predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldAudioSttPricePerHour, vs...)) +} + +// AudioSttPricePerHourGT applies the GT predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourGTE applies the GTE predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourLT applies the LT predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourLTE applies the LTE predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldAudioSttPricePerHour, v)) +} + +// AudioSttPricePerHourIsNil applies the IsNil predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldAudioSttPricePerHour)) +} + +// AudioSttPricePerHourNotNil applies the NotNil predicate on the "audio_stt_price_per_hour" field. +func AudioSttPricePerHourNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldAudioSttPricePerHour)) +} + // ClaudeCodeOnlyEQ applies the EQ predicate on the "claude_code_only" field. func ClaudeCodeOnlyEQ(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldClaudeCodeOnly, v)) diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 6478c14a68..2753bc827b 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -503,6 +503,48 @@ func (_c *GroupCreate) SetNillableWebSearchPricePerCall(v *float64) *GroupCreate return _c } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (_c *GroupCreate) SetAudioRealtimePricePerMin(v float64) *GroupCreate { + _c.mutation.SetAudioRealtimePricePerMin(v) + return _c +} + +// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil. +func (_c *GroupCreate) SetNillableAudioRealtimePricePerMin(v *float64) *GroupCreate { + if v != nil { + _c.SetAudioRealtimePricePerMin(*v) + } + return _c +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (_c *GroupCreate) SetAudioTtsPricePerMillionChars(v float64) *GroupCreate { + _c.mutation.SetAudioTtsPricePerMillionChars(v) + return _c +} + +// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil. +func (_c *GroupCreate) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupCreate { + if v != nil { + _c.SetAudioTtsPricePerMillionChars(*v) + } + return _c +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (_c *GroupCreate) SetAudioSttPricePerHour(v float64) *GroupCreate { + _c.mutation.SetAudioSttPricePerHour(v) + return _c +} + +// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil. +func (_c *GroupCreate) SetNillableAudioSttPricePerHour(v *float64) *GroupCreate { + if v != nil { + _c.SetAudioSttPricePerHour(*v) + } + return _c +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_c *GroupCreate) SetClaudeCodeOnly(v bool) *GroupCreate { _c.mutation.SetClaudeCodeOnly(v) @@ -1161,6 +1203,21 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.VideoRateMultiplier(); !ok { return &ValidationError{Name: "video_rate_multiplier", err: errors.New(`ent: missing required field "Group.video_rate_multiplier"`)} } + if v, ok := _c.mutation.AudioRealtimePricePerMin(); ok { + if err := group.AudioRealtimePricePerMinValidator(v); err != nil { + return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)} + } + } + if v, ok := _c.mutation.AudioTtsPricePerMillionChars(); ok { + if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil { + return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)} + } + } + if v, ok := _c.mutation.AudioSttPricePerHour(); ok { + if err := group.AudioSttPricePerHourValidator(v); err != nil { + return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)} + } + } if _, ok := _c.mutation.ClaudeCodeOnly(); !ok { return &ValidationError{Name: "claude_code_only", err: errors.New(`ent: missing required field "Group.claude_code_only"`)} } @@ -1392,6 +1449,18 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value) _node.WebSearchPricePerCall = &value } + if value, ok := _c.mutation.AudioRealtimePricePerMin(); ok { + _spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value) + _node.AudioRealtimePricePerMin = &value + } + if value, ok := _c.mutation.AudioTtsPricePerMillionChars(); ok { + _spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value) + _node.AudioTtsPricePerMillionChars = &value + } + if value, ok := _c.mutation.AudioSttPricePerHour(); ok { + _spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value) + _node.AudioSttPricePerHour = &value + } if value, ok := _c.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) _node.ClaudeCodeOnly = value @@ -2208,6 +2277,78 @@ func (u *GroupUpsert) ClearWebSearchPricePerCall() *GroupUpsert { return u } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (u *GroupUpsert) SetAudioRealtimePricePerMin(v float64) *GroupUpsert { + u.Set(group.FieldAudioRealtimePricePerMin, v) + return u +} + +// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create. +func (u *GroupUpsert) UpdateAudioRealtimePricePerMin() *GroupUpsert { + u.SetExcluded(group.FieldAudioRealtimePricePerMin) + return u +} + +// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field. +func (u *GroupUpsert) AddAudioRealtimePricePerMin(v float64) *GroupUpsert { + u.Add(group.FieldAudioRealtimePricePerMin, v) + return u +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (u *GroupUpsert) ClearAudioRealtimePricePerMin() *GroupUpsert { + u.SetNull(group.FieldAudioRealtimePricePerMin) + return u +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsert) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsert { + u.Set(group.FieldAudioTtsPricePerMillionChars, v) + return u +} + +// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create. +func (u *GroupUpsert) UpdateAudioTtsPricePerMillionChars() *GroupUpsert { + u.SetExcluded(group.FieldAudioTtsPricePerMillionChars) + return u +} + +// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsert) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsert { + u.Add(group.FieldAudioTtsPricePerMillionChars, v) + return u +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsert) ClearAudioTtsPricePerMillionChars() *GroupUpsert { + u.SetNull(group.FieldAudioTtsPricePerMillionChars) + return u +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (u *GroupUpsert) SetAudioSttPricePerHour(v float64) *GroupUpsert { + u.Set(group.FieldAudioSttPricePerHour, v) + return u +} + +// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create. +func (u *GroupUpsert) UpdateAudioSttPricePerHour() *GroupUpsert { + u.SetExcluded(group.FieldAudioSttPricePerHour) + return u +} + +// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field. +func (u *GroupUpsert) AddAudioSttPricePerHour(v float64) *GroupUpsert { + u.Add(group.FieldAudioSttPricePerHour, v) + return u +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (u *GroupUpsert) ClearAudioSttPricePerHour() *GroupUpsert { + u.SetNull(group.FieldAudioSttPricePerHour) + return u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsert) SetClaudeCodeOnly(v bool) *GroupUpsert { u.Set(group.FieldClaudeCodeOnly, v) @@ -3234,6 +3375,90 @@ func (u *GroupUpsertOne) ClearWebSearchPricePerCall() *GroupUpsertOne { }) } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (u *GroupUpsertOne) SetAudioRealtimePricePerMin(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetAudioRealtimePricePerMin(v) + }) +} + +// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field. +func (u *GroupUpsertOne) AddAudioRealtimePricePerMin(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddAudioRealtimePricePerMin(v) + }) +} + +// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateAudioRealtimePricePerMin() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioRealtimePricePerMin() + }) +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (u *GroupUpsertOne) ClearAudioRealtimePricePerMin() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioRealtimePricePerMin() + }) +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertOne) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetAudioTtsPricePerMillionChars(v) + }) +} + +// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertOne) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddAudioTtsPricePerMillionChars(v) + }) +} + +// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateAudioTtsPricePerMillionChars() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioTtsPricePerMillionChars() + }) +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertOne) ClearAudioTtsPricePerMillionChars() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioTtsPricePerMillionChars() + }) +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (u *GroupUpsertOne) SetAudioSttPricePerHour(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetAudioSttPricePerHour(v) + }) +} + +// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field. +func (u *GroupUpsertOne) AddAudioSttPricePerHour(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddAudioSttPricePerHour(v) + }) +} + +// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateAudioSttPricePerHour() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioSttPricePerHour() + }) +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (u *GroupUpsertOne) ClearAudioSttPricePerHour() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioSttPricePerHour() + }) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsertOne) SetClaudeCodeOnly(v bool) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { @@ -4477,6 +4702,90 @@ func (u *GroupUpsertBulk) ClearWebSearchPricePerCall() *GroupUpsertBulk { }) } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (u *GroupUpsertBulk) SetAudioRealtimePricePerMin(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetAudioRealtimePricePerMin(v) + }) +} + +// AddAudioRealtimePricePerMin adds v to the "audio_realtime_price_per_min" field. +func (u *GroupUpsertBulk) AddAudioRealtimePricePerMin(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddAudioRealtimePricePerMin(v) + }) +} + +// UpdateAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateAudioRealtimePricePerMin() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioRealtimePricePerMin() + }) +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (u *GroupUpsertBulk) ClearAudioRealtimePricePerMin() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioRealtimePricePerMin() + }) +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertBulk) SetAudioTtsPricePerMillionChars(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetAudioTtsPricePerMillionChars(v) + }) +} + +// AddAudioTtsPricePerMillionChars adds v to the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertBulk) AddAudioTtsPricePerMillionChars(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddAudioTtsPricePerMillionChars(v) + }) +} + +// UpdateAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateAudioTtsPricePerMillionChars() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioTtsPricePerMillionChars() + }) +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (u *GroupUpsertBulk) ClearAudioTtsPricePerMillionChars() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioTtsPricePerMillionChars() + }) +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (u *GroupUpsertBulk) SetAudioSttPricePerHour(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetAudioSttPricePerHour(v) + }) +} + +// AddAudioSttPricePerHour adds v to the "audio_stt_price_per_hour" field. +func (u *GroupUpsertBulk) AddAudioSttPricePerHour(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddAudioSttPricePerHour(v) + }) +} + +// UpdateAudioSttPricePerHour sets the "audio_stt_price_per_hour" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateAudioSttPricePerHour() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateAudioSttPricePerHour() + }) +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (u *GroupUpsertBulk) ClearAudioSttPricePerHour() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearAudioSttPricePerHour() + }) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (u *GroupUpsertBulk) SetClaudeCodeOnly(v bool) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index aa308102ff..126fdef40f 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -679,6 +679,87 @@ func (_u *GroupUpdate) ClearWebSearchPricePerCall() *GroupUpdate { return _u } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (_u *GroupUpdate) SetAudioRealtimePricePerMin(v float64) *GroupUpdate { + _u.mutation.ResetAudioRealtimePricePerMin() + _u.mutation.SetAudioRealtimePricePerMin(v) + return _u +} + +// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableAudioRealtimePricePerMin(v *float64) *GroupUpdate { + if v != nil { + _u.SetAudioRealtimePricePerMin(*v) + } + return _u +} + +// AddAudioRealtimePricePerMin adds value to the "audio_realtime_price_per_min" field. +func (_u *GroupUpdate) AddAudioRealtimePricePerMin(v float64) *GroupUpdate { + _u.mutation.AddAudioRealtimePricePerMin(v) + return _u +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (_u *GroupUpdate) ClearAudioRealtimePricePerMin() *GroupUpdate { + _u.mutation.ClearAudioRealtimePricePerMin() + return _u +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdate) SetAudioTtsPricePerMillionChars(v float64) *GroupUpdate { + _u.mutation.ResetAudioTtsPricePerMillionChars() + _u.mutation.SetAudioTtsPricePerMillionChars(v) + return _u +} + +// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupUpdate { + if v != nil { + _u.SetAudioTtsPricePerMillionChars(*v) + } + return _u +} + +// AddAudioTtsPricePerMillionChars adds value to the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdate) AddAudioTtsPricePerMillionChars(v float64) *GroupUpdate { + _u.mutation.AddAudioTtsPricePerMillionChars(v) + return _u +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdate) ClearAudioTtsPricePerMillionChars() *GroupUpdate { + _u.mutation.ClearAudioTtsPricePerMillionChars() + return _u +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (_u *GroupUpdate) SetAudioSttPricePerHour(v float64) *GroupUpdate { + _u.mutation.ResetAudioSttPricePerHour() + _u.mutation.SetAudioSttPricePerHour(v) + return _u +} + +// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableAudioSttPricePerHour(v *float64) *GroupUpdate { + if v != nil { + _u.SetAudioSttPricePerHour(*v) + } + return _u +} + +// AddAudioSttPricePerHour adds value to the "audio_stt_price_per_hour" field. +func (_u *GroupUpdate) AddAudioSttPricePerHour(v float64) *GroupUpdate { + _u.mutation.AddAudioSttPricePerHour(v) + return _u +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (_u *GroupUpdate) ClearAudioSttPricePerHour() *GroupUpdate { + _u.mutation.ClearAudioSttPricePerHour() + return _u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_u *GroupUpdate) SetClaudeCodeOnly(v bool) *GroupUpdate { _u.mutation.SetClaudeCodeOnly(v) @@ -1316,6 +1397,21 @@ func (_u *GroupUpdate) check() error { return &ValidationError{Name: "subscription_type", err: fmt.Errorf(`ent: validator failed for field "Group.subscription_type": %w`, err)} } } + if v, ok := _u.mutation.AudioRealtimePricePerMin(); ok { + if err := group.AudioRealtimePricePerMinValidator(v); err != nil { + return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)} + } + } + if v, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok { + if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil { + return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)} + } + } + if v, ok := _u.mutation.AudioSttPricePerHour(); ok { + if err := group.AudioSttPricePerHourValidator(v); err != nil { + return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)} + } + } if v, ok := _u.mutation.DefaultMappedModel(); ok { if err := group.DefaultMappedModelValidator(v); err != nil { return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)} @@ -1533,6 +1629,33 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.WebSearchPricePerCallCleared() { _spec.ClearField(group.FieldWebSearchPricePerCall, field.TypeFloat64) } + if value, ok := _u.mutation.AudioRealtimePricePerMin(); ok { + _spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioRealtimePricePerMin(); ok { + _spec.AddField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value) + } + if _u.mutation.AudioRealtimePricePerMinCleared() { + _spec.ClearField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64) + } + if value, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok { + _spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioTtsPricePerMillionChars(); ok { + _spec.AddField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value) + } + if _u.mutation.AudioTtsPricePerMillionCharsCleared() { + _spec.ClearField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64) + } + if value, ok := _u.mutation.AudioSttPricePerHour(); ok { + _spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioSttPricePerHour(); ok { + _spec.AddField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value) + } + if _u.mutation.AudioSttPricePerHourCleared() { + _spec.ClearField(group.FieldAudioSttPricePerHour, field.TypeFloat64) + } if value, ok := _u.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) } @@ -2590,6 +2713,87 @@ func (_u *GroupUpdateOne) ClearWebSearchPricePerCall() *GroupUpdateOne { return _u } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (_u *GroupUpdateOne) SetAudioRealtimePricePerMin(v float64) *GroupUpdateOne { + _u.mutation.ResetAudioRealtimePricePerMin() + _u.mutation.SetAudioRealtimePricePerMin(v) + return _u +} + +// SetNillableAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableAudioRealtimePricePerMin(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetAudioRealtimePricePerMin(*v) + } + return _u +} + +// AddAudioRealtimePricePerMin adds value to the "audio_realtime_price_per_min" field. +func (_u *GroupUpdateOne) AddAudioRealtimePricePerMin(v float64) *GroupUpdateOne { + _u.mutation.AddAudioRealtimePricePerMin(v) + return _u +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (_u *GroupUpdateOne) ClearAudioRealtimePricePerMin() *GroupUpdateOne { + _u.mutation.ClearAudioRealtimePricePerMin() + return _u +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdateOne) SetAudioTtsPricePerMillionChars(v float64) *GroupUpdateOne { + _u.mutation.ResetAudioTtsPricePerMillionChars() + _u.mutation.SetAudioTtsPricePerMillionChars(v) + return _u +} + +// SetNillableAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableAudioTtsPricePerMillionChars(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetAudioTtsPricePerMillionChars(*v) + } + return _u +} + +// AddAudioTtsPricePerMillionChars adds value to the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdateOne) AddAudioTtsPricePerMillionChars(v float64) *GroupUpdateOne { + _u.mutation.AddAudioTtsPricePerMillionChars(v) + return _u +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (_u *GroupUpdateOne) ClearAudioTtsPricePerMillionChars() *GroupUpdateOne { + _u.mutation.ClearAudioTtsPricePerMillionChars() + return _u +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (_u *GroupUpdateOne) SetAudioSttPricePerHour(v float64) *GroupUpdateOne { + _u.mutation.ResetAudioSttPricePerHour() + _u.mutation.SetAudioSttPricePerHour(v) + return _u +} + +// SetNillableAudioSttPricePerHour sets the "audio_stt_price_per_hour" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableAudioSttPricePerHour(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetAudioSttPricePerHour(*v) + } + return _u +} + +// AddAudioSttPricePerHour adds value to the "audio_stt_price_per_hour" field. +func (_u *GroupUpdateOne) AddAudioSttPricePerHour(v float64) *GroupUpdateOne { + _u.mutation.AddAudioSttPricePerHour(v) + return _u +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (_u *GroupUpdateOne) ClearAudioSttPricePerHour() *GroupUpdateOne { + _u.mutation.ClearAudioSttPricePerHour() + return _u +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (_u *GroupUpdateOne) SetClaudeCodeOnly(v bool) *GroupUpdateOne { _u.mutation.SetClaudeCodeOnly(v) @@ -3240,6 +3444,21 @@ func (_u *GroupUpdateOne) check() error { return &ValidationError{Name: "subscription_type", err: fmt.Errorf(`ent: validator failed for field "Group.subscription_type": %w`, err)} } } + if v, ok := _u.mutation.AudioRealtimePricePerMin(); ok { + if err := group.AudioRealtimePricePerMinValidator(v); err != nil { + return &ValidationError{Name: "audio_realtime_price_per_min", err: fmt.Errorf(`ent: validator failed for field "Group.audio_realtime_price_per_min": %w`, err)} + } + } + if v, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok { + if err := group.AudioTtsPricePerMillionCharsValidator(v); err != nil { + return &ValidationError{Name: "audio_tts_price_per_million_chars", err: fmt.Errorf(`ent: validator failed for field "Group.audio_tts_price_per_million_chars": %w`, err)} + } + } + if v, ok := _u.mutation.AudioSttPricePerHour(); ok { + if err := group.AudioSttPricePerHourValidator(v); err != nil { + return &ValidationError{Name: "audio_stt_price_per_hour", err: fmt.Errorf(`ent: validator failed for field "Group.audio_stt_price_per_hour": %w`, err)} + } + } if v, ok := _u.mutation.DefaultMappedModel(); ok { if err := group.DefaultMappedModelValidator(v); err != nil { return &ValidationError{Name: "default_mapped_model", err: fmt.Errorf(`ent: validator failed for field "Group.default_mapped_model": %w`, err)} @@ -3474,6 +3693,33 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if _u.mutation.WebSearchPricePerCallCleared() { _spec.ClearField(group.FieldWebSearchPricePerCall, field.TypeFloat64) } + if value, ok := _u.mutation.AudioRealtimePricePerMin(); ok { + _spec.SetField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioRealtimePricePerMin(); ok { + _spec.AddField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64, value) + } + if _u.mutation.AudioRealtimePricePerMinCleared() { + _spec.ClearField(group.FieldAudioRealtimePricePerMin, field.TypeFloat64) + } + if value, ok := _u.mutation.AudioTtsPricePerMillionChars(); ok { + _spec.SetField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioTtsPricePerMillionChars(); ok { + _spec.AddField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64, value) + } + if _u.mutation.AudioTtsPricePerMillionCharsCleared() { + _spec.ClearField(group.FieldAudioTtsPricePerMillionChars, field.TypeFloat64) + } + if value, ok := _u.mutation.AudioSttPricePerHour(); ok { + _spec.SetField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedAudioSttPricePerHour(); ok { + _spec.AddField(group.FieldAudioSttPricePerHour, field.TypeFloat64, value) + } + if _u.mutation.AudioSttPricePerHourCleared() { + _spec.ClearField(group.FieldAudioSttPricePerHour, field.TypeFloat64) + } if value, ok := _u.mutation.ClaudeCodeOnly(); ok { _spec.SetField(group.FieldClaudeCodeOnly, field.TypeBool, value) } diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index 74b37383ca..185d3ab15c 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -930,6 +930,9 @@ var ( {Name: "video_price_1080p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "video_model_prices", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "web_search_price_per_call", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "audio_realtime_price_per_min", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "audio_tts_price_per_million_chars", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "audio_stt_price_per_hour", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "claude_code_only", Type: field.TypeBool, Default: false}, {Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true}, {Name: "fallback_group_id_on_invalid_request", Type: field.TypeInt64, Nullable: true}, @@ -986,7 +989,7 @@ var ( { Name: "group_sort_order", Unique: false, - Columns: []*schema.Column{GroupsColumns[43]}, + Columns: []*schema.Column{GroupsColumns[46]}, }, { Name: "idx_groups_duplicate_operation_id_active", diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 3dc565846b..86938046e3 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -21899,6 +21899,12 @@ type GroupMutation struct { video_model_prices *map[string]map[string]float64 web_search_price_per_call *float64 addweb_search_price_per_call *float64 + audio_realtime_price_per_min *float64 + addaudio_realtime_price_per_min *float64 + audio_tts_price_per_million_chars *float64 + addaudio_tts_price_per_million_chars *float64 + audio_stt_price_per_hour *float64 + addaudio_stt_price_per_hour *float64 claude_code_only *bool fallback_group_id *int64 addfallback_group_id *int64 @@ -23842,6 +23848,216 @@ func (m *GroupMutation) ResetWebSearchPricePerCall() { delete(m.clearedFields, group.FieldWebSearchPricePerCall) } +// SetAudioRealtimePricePerMin sets the "audio_realtime_price_per_min" field. +func (m *GroupMutation) SetAudioRealtimePricePerMin(f float64) { + m.audio_realtime_price_per_min = &f + m.addaudio_realtime_price_per_min = nil +} + +// AudioRealtimePricePerMin returns the value of the "audio_realtime_price_per_min" field in the mutation. +func (m *GroupMutation) AudioRealtimePricePerMin() (r float64, exists bool) { + v := m.audio_realtime_price_per_min + if v == nil { + return + } + return *v, true +} + +// OldAudioRealtimePricePerMin returns the old "audio_realtime_price_per_min" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldAudioRealtimePricePerMin(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAudioRealtimePricePerMin is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAudioRealtimePricePerMin requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAudioRealtimePricePerMin: %w", err) + } + return oldValue.AudioRealtimePricePerMin, nil +} + +// AddAudioRealtimePricePerMin adds f to the "audio_realtime_price_per_min" field. +func (m *GroupMutation) AddAudioRealtimePricePerMin(f float64) { + if m.addaudio_realtime_price_per_min != nil { + *m.addaudio_realtime_price_per_min += f + } else { + m.addaudio_realtime_price_per_min = &f + } +} + +// AddedAudioRealtimePricePerMin returns the value that was added to the "audio_realtime_price_per_min" field in this mutation. +func (m *GroupMutation) AddedAudioRealtimePricePerMin() (r float64, exists bool) { + v := m.addaudio_realtime_price_per_min + if v == nil { + return + } + return *v, true +} + +// ClearAudioRealtimePricePerMin clears the value of the "audio_realtime_price_per_min" field. +func (m *GroupMutation) ClearAudioRealtimePricePerMin() { + m.audio_realtime_price_per_min = nil + m.addaudio_realtime_price_per_min = nil + m.clearedFields[group.FieldAudioRealtimePricePerMin] = struct{}{} +} + +// AudioRealtimePricePerMinCleared returns if the "audio_realtime_price_per_min" field was cleared in this mutation. +func (m *GroupMutation) AudioRealtimePricePerMinCleared() bool { + _, ok := m.clearedFields[group.FieldAudioRealtimePricePerMin] + return ok +} + +// ResetAudioRealtimePricePerMin resets all changes to the "audio_realtime_price_per_min" field. +func (m *GroupMutation) ResetAudioRealtimePricePerMin() { + m.audio_realtime_price_per_min = nil + m.addaudio_realtime_price_per_min = nil + delete(m.clearedFields, group.FieldAudioRealtimePricePerMin) +} + +// SetAudioTtsPricePerMillionChars sets the "audio_tts_price_per_million_chars" field. +func (m *GroupMutation) SetAudioTtsPricePerMillionChars(f float64) { + m.audio_tts_price_per_million_chars = &f + m.addaudio_tts_price_per_million_chars = nil +} + +// AudioTtsPricePerMillionChars returns the value of the "audio_tts_price_per_million_chars" field in the mutation. +func (m *GroupMutation) AudioTtsPricePerMillionChars() (r float64, exists bool) { + v := m.audio_tts_price_per_million_chars + if v == nil { + return + } + return *v, true +} + +// OldAudioTtsPricePerMillionChars returns the old "audio_tts_price_per_million_chars" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldAudioTtsPricePerMillionChars(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAudioTtsPricePerMillionChars is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAudioTtsPricePerMillionChars requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAudioTtsPricePerMillionChars: %w", err) + } + return oldValue.AudioTtsPricePerMillionChars, nil +} + +// AddAudioTtsPricePerMillionChars adds f to the "audio_tts_price_per_million_chars" field. +func (m *GroupMutation) AddAudioTtsPricePerMillionChars(f float64) { + if m.addaudio_tts_price_per_million_chars != nil { + *m.addaudio_tts_price_per_million_chars += f + } else { + m.addaudio_tts_price_per_million_chars = &f + } +} + +// AddedAudioTtsPricePerMillionChars returns the value that was added to the "audio_tts_price_per_million_chars" field in this mutation. +func (m *GroupMutation) AddedAudioTtsPricePerMillionChars() (r float64, exists bool) { + v := m.addaudio_tts_price_per_million_chars + if v == nil { + return + } + return *v, true +} + +// ClearAudioTtsPricePerMillionChars clears the value of the "audio_tts_price_per_million_chars" field. +func (m *GroupMutation) ClearAudioTtsPricePerMillionChars() { + m.audio_tts_price_per_million_chars = nil + m.addaudio_tts_price_per_million_chars = nil + m.clearedFields[group.FieldAudioTtsPricePerMillionChars] = struct{}{} +} + +// AudioTtsPricePerMillionCharsCleared returns if the "audio_tts_price_per_million_chars" field was cleared in this mutation. +func (m *GroupMutation) AudioTtsPricePerMillionCharsCleared() bool { + _, ok := m.clearedFields[group.FieldAudioTtsPricePerMillionChars] + return ok +} + +// ResetAudioTtsPricePerMillionChars resets all changes to the "audio_tts_price_per_million_chars" field. +func (m *GroupMutation) ResetAudioTtsPricePerMillionChars() { + m.audio_tts_price_per_million_chars = nil + m.addaudio_tts_price_per_million_chars = nil + delete(m.clearedFields, group.FieldAudioTtsPricePerMillionChars) +} + +// SetAudioSttPricePerHour sets the "audio_stt_price_per_hour" field. +func (m *GroupMutation) SetAudioSttPricePerHour(f float64) { + m.audio_stt_price_per_hour = &f + m.addaudio_stt_price_per_hour = nil +} + +// AudioSttPricePerHour returns the value of the "audio_stt_price_per_hour" field in the mutation. +func (m *GroupMutation) AudioSttPricePerHour() (r float64, exists bool) { + v := m.audio_stt_price_per_hour + if v == nil { + return + } + return *v, true +} + +// OldAudioSttPricePerHour returns the old "audio_stt_price_per_hour" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldAudioSttPricePerHour(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAudioSttPricePerHour is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAudioSttPricePerHour requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAudioSttPricePerHour: %w", err) + } + return oldValue.AudioSttPricePerHour, nil +} + +// AddAudioSttPricePerHour adds f to the "audio_stt_price_per_hour" field. +func (m *GroupMutation) AddAudioSttPricePerHour(f float64) { + if m.addaudio_stt_price_per_hour != nil { + *m.addaudio_stt_price_per_hour += f + } else { + m.addaudio_stt_price_per_hour = &f + } +} + +// AddedAudioSttPricePerHour returns the value that was added to the "audio_stt_price_per_hour" field in this mutation. +func (m *GroupMutation) AddedAudioSttPricePerHour() (r float64, exists bool) { + v := m.addaudio_stt_price_per_hour + if v == nil { + return + } + return *v, true +} + +// ClearAudioSttPricePerHour clears the value of the "audio_stt_price_per_hour" field. +func (m *GroupMutation) ClearAudioSttPricePerHour() { + m.audio_stt_price_per_hour = nil + m.addaudio_stt_price_per_hour = nil + m.clearedFields[group.FieldAudioSttPricePerHour] = struct{}{} +} + +// AudioSttPricePerHourCleared returns if the "audio_stt_price_per_hour" field was cleared in this mutation. +func (m *GroupMutation) AudioSttPricePerHourCleared() bool { + _, ok := m.clearedFields[group.FieldAudioSttPricePerHour] + return ok +} + +// ResetAudioSttPricePerHour resets all changes to the "audio_stt_price_per_hour" field. +func (m *GroupMutation) ResetAudioSttPricePerHour() { + m.audio_stt_price_per_hour = nil + m.addaudio_stt_price_per_hour = nil + delete(m.clearedFields, group.FieldAudioSttPricePerHour) +} + // SetClaudeCodeOnly sets the "claude_code_only" field. func (m *GroupMutation) SetClaudeCodeOnly(b bool) { m.claude_code_only = &b @@ -25147,7 +25363,7 @@ func (m *GroupMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *GroupMutation) Fields() []string { - fields := make([]string, 0, 56) + fields := make([]string, 0, 59) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -25253,6 +25469,15 @@ func (m *GroupMutation) Fields() []string { if m.web_search_price_per_call != nil { fields = append(fields, group.FieldWebSearchPricePerCall) } + if m.audio_realtime_price_per_min != nil { + fields = append(fields, group.FieldAudioRealtimePricePerMin) + } + if m.audio_tts_price_per_million_chars != nil { + fields = append(fields, group.FieldAudioTtsPricePerMillionChars) + } + if m.audio_stt_price_per_hour != nil { + fields = append(fields, group.FieldAudioSttPricePerHour) + } if m.claude_code_only != nil { fields = append(fields, group.FieldClaudeCodeOnly) } @@ -25394,6 +25619,12 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.VideoModelPrices() case group.FieldWebSearchPricePerCall: return m.WebSearchPricePerCall() + case group.FieldAudioRealtimePricePerMin: + return m.AudioRealtimePricePerMin() + case group.FieldAudioTtsPricePerMillionChars: + return m.AudioTtsPricePerMillionChars() + case group.FieldAudioSttPricePerHour: + return m.AudioSttPricePerHour() case group.FieldClaudeCodeOnly: return m.ClaudeCodeOnly() case group.FieldFallbackGroupID: @@ -25515,6 +25746,12 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldVideoModelPrices(ctx) case group.FieldWebSearchPricePerCall: return m.OldWebSearchPricePerCall(ctx) + case group.FieldAudioRealtimePricePerMin: + return m.OldAudioRealtimePricePerMin(ctx) + case group.FieldAudioTtsPricePerMillionChars: + return m.OldAudioTtsPricePerMillionChars(ctx) + case group.FieldAudioSttPricePerHour: + return m.OldAudioSttPricePerHour(ctx) case group.FieldClaudeCodeOnly: return m.OldClaudeCodeOnly(ctx) case group.FieldFallbackGroupID: @@ -25811,6 +26048,27 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetWebSearchPricePerCall(v) return nil + case group.FieldAudioRealtimePricePerMin: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAudioRealtimePricePerMin(v) + return nil + case group.FieldAudioTtsPricePerMillionChars: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAudioTtsPricePerMillionChars(v) + return nil + case group.FieldAudioSttPricePerHour: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAudioSttPricePerHour(v) + return nil case group.FieldClaudeCodeOnly: v, ok := value.(bool) if !ok { @@ -26017,6 +26275,15 @@ func (m *GroupMutation) AddedFields() []string { if m.addweb_search_price_per_call != nil { fields = append(fields, group.FieldWebSearchPricePerCall) } + if m.addaudio_realtime_price_per_min != nil { + fields = append(fields, group.FieldAudioRealtimePricePerMin) + } + if m.addaudio_tts_price_per_million_chars != nil { + fields = append(fields, group.FieldAudioTtsPricePerMillionChars) + } + if m.addaudio_stt_price_per_hour != nil { + fields = append(fields, group.FieldAudioSttPricePerHour) + } if m.addfallback_group_id != nil { fields = append(fields, group.FieldFallbackGroupID) } @@ -26077,6 +26344,12 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { return m.AddedVideoPrice1080p() case group.FieldWebSearchPricePerCall: return m.AddedWebSearchPricePerCall() + case group.FieldAudioRealtimePricePerMin: + return m.AddedAudioRealtimePricePerMin() + case group.FieldAudioTtsPricePerMillionChars: + return m.AddedAudioTtsPricePerMillionChars() + case group.FieldAudioSttPricePerHour: + return m.AddedAudioSttPricePerHour() case group.FieldFallbackGroupID: return m.AddedFallbackGroupID() case group.FieldFallbackGroupIDOnInvalidRequest: @@ -26217,6 +26490,27 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddWebSearchPricePerCall(v) return nil + case group.FieldAudioRealtimePricePerMin: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAudioRealtimePricePerMin(v) + return nil + case group.FieldAudioTtsPricePerMillionChars: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAudioTtsPricePerMillionChars(v) + return nil + case group.FieldAudioSttPricePerHour: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddAudioSttPricePerHour(v) + return nil case group.FieldFallbackGroupID: v, ok := value.(int64) if !ok { @@ -26309,6 +26603,15 @@ func (m *GroupMutation) ClearedFields() []string { if m.FieldCleared(group.FieldWebSearchPricePerCall) { fields = append(fields, group.FieldWebSearchPricePerCall) } + if m.FieldCleared(group.FieldAudioRealtimePricePerMin) { + fields = append(fields, group.FieldAudioRealtimePricePerMin) + } + if m.FieldCleared(group.FieldAudioTtsPricePerMillionChars) { + fields = append(fields, group.FieldAudioTtsPricePerMillionChars) + } + if m.FieldCleared(group.FieldAudioSttPricePerHour) { + fields = append(fields, group.FieldAudioSttPricePerHour) + } if m.FieldCleared(group.FieldFallbackGroupID) { fields = append(fields, group.FieldFallbackGroupID) } @@ -26374,6 +26677,15 @@ func (m *GroupMutation) ClearField(name string) error { case group.FieldWebSearchPricePerCall: m.ClearWebSearchPricePerCall() return nil + case group.FieldAudioRealtimePricePerMin: + m.ClearAudioRealtimePricePerMin() + return nil + case group.FieldAudioTtsPricePerMillionChars: + m.ClearAudioTtsPricePerMillionChars() + return nil + case group.FieldAudioSttPricePerHour: + m.ClearAudioSttPricePerHour() + return nil case group.FieldFallbackGroupID: m.ClearFallbackGroupID() return nil @@ -26496,6 +26808,15 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldWebSearchPricePerCall: m.ResetWebSearchPricePerCall() return nil + case group.FieldAudioRealtimePricePerMin: + m.ResetAudioRealtimePricePerMin() + return nil + case group.FieldAudioTtsPricePerMillionChars: + m.ResetAudioTtsPricePerMillionChars() + return nil + case group.FieldAudioSttPricePerHour: + m.ResetAudioSttPricePerHour() + return nil case group.FieldClaudeCodeOnly: m.ResetClaudeCodeOnly() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index f57032757d..6a58af2562 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -1117,80 +1117,92 @@ func init() { groupDescVideoRateMultiplier := groupFields[26].Descriptor() // group.DefaultVideoRateMultiplier holds the default value on creation for the video_rate_multiplier field. group.DefaultVideoRateMultiplier = groupDescVideoRateMultiplier.Default.(float64) + // groupDescAudioRealtimePricePerMin is the schema descriptor for audio_realtime_price_per_min field. + groupDescAudioRealtimePricePerMin := groupFields[32].Descriptor() + // group.AudioRealtimePricePerMinValidator is a validator for the "audio_realtime_price_per_min" field. It is called by the builders before save. + group.AudioRealtimePricePerMinValidator = groupDescAudioRealtimePricePerMin.Validators[0].(func(float64) error) + // groupDescAudioTtsPricePerMillionChars is the schema descriptor for audio_tts_price_per_million_chars field. + groupDescAudioTtsPricePerMillionChars := groupFields[33].Descriptor() + // group.AudioTtsPricePerMillionCharsValidator is a validator for the "audio_tts_price_per_million_chars" field. It is called by the builders before save. + group.AudioTtsPricePerMillionCharsValidator = groupDescAudioTtsPricePerMillionChars.Validators[0].(func(float64) error) + // groupDescAudioSttPricePerHour is the schema descriptor for audio_stt_price_per_hour field. + groupDescAudioSttPricePerHour := groupFields[34].Descriptor() + // group.AudioSttPricePerHourValidator is a validator for the "audio_stt_price_per_hour" field. It is called by the builders before save. + group.AudioSttPricePerHourValidator = groupDescAudioSttPricePerHour.Validators[0].(func(float64) error) // groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field. - groupDescClaudeCodeOnly := groupFields[32].Descriptor() + groupDescClaudeCodeOnly := groupFields[35].Descriptor() // group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field. group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool) // groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field. - groupDescModelRoutingEnabled := groupFields[36].Descriptor() + groupDescModelRoutingEnabled := groupFields[39].Descriptor() // group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field. group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool) // groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field. - groupDescMcpXMLInject := groupFields[37].Descriptor() + groupDescMcpXMLInject := groupFields[40].Descriptor() // group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field. group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool) // groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field. - groupDescSupportedModelScopes := groupFields[38].Descriptor() + groupDescSupportedModelScopes := groupFields[41].Descriptor() // group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field. group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string) // groupDescSortOrder is the schema descriptor for sort_order field. - groupDescSortOrder := groupFields[39].Descriptor() + groupDescSortOrder := groupFields[42].Descriptor() // group.DefaultSortOrder holds the default value on creation for the sort_order field. group.DefaultSortOrder = groupDescSortOrder.Default.(int) // groupDescAllowMessagesDispatch is the schema descriptor for allow_messages_dispatch field. - groupDescAllowMessagesDispatch := groupFields[40].Descriptor() + groupDescAllowMessagesDispatch := groupFields[43].Descriptor() // group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field. group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool) // groupDescAllowLive is the schema descriptor for allow_live field. - groupDescAllowLive := groupFields[41].Descriptor() + groupDescAllowLive := groupFields[44].Descriptor() // group.DefaultAllowLive holds the default value on creation for the allow_live field. group.DefaultAllowLive = groupDescAllowLive.Default.(bool) // groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field. - groupDescRequireOauthOnly := groupFields[42].Descriptor() + groupDescRequireOauthOnly := groupFields[45].Descriptor() // group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field. group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool) // groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field. - groupDescRequirePrivacySet := groupFields[43].Descriptor() + groupDescRequirePrivacySet := groupFields[46].Descriptor() // group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field. group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool) // groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field. - groupDescDefaultMappedModel := groupFields[44].Descriptor() + groupDescDefaultMappedModel := groupFields[47].Descriptor() // group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field. group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string) // group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save. group.DefaultMappedModelValidator = groupDescDefaultMappedModel.Validators[0].(func(string) error) // groupDescMessagesDispatchModelConfig is the schema descriptor for messages_dispatch_model_config field. - groupDescMessagesDispatchModelConfig := groupFields[45].Descriptor() + groupDescMessagesDispatchModelConfig := groupFields[48].Descriptor() // group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field. group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig) // groupDescModelsListConfig is the schema descriptor for models_list_config field. - groupDescModelsListConfig := groupFields[46].Descriptor() + groupDescModelsListConfig := groupFields[49].Descriptor() // group.DefaultModelsListConfig holds the default value on creation for the models_list_config field. group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig) // groupDescRpmLimit is the schema descriptor for rpm_limit field. - groupDescRpmLimit := groupFields[47].Descriptor() + groupDescRpmLimit := groupFields[50].Descriptor() // group.DefaultRpmLimit holds the default value on creation for the rpm_limit field. group.DefaultRpmLimit = groupDescRpmLimit.Default.(int) // groupDescMaxReasoningEffort is the schema descriptor for max_reasoning_effort field. - groupDescMaxReasoningEffort := groupFields[48].Descriptor() + groupDescMaxReasoningEffort := groupFields[51].Descriptor() // group.DefaultMaxReasoningEffort holds the default value on creation for the max_reasoning_effort field. group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string) // group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save. group.MaxReasoningEffortValidator = groupDescMaxReasoningEffort.Validators[0].(func(string) error) // groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field. - groupDescReasoningEffortMappings := groupFields[49].Descriptor() + groupDescReasoningEffortMappings := groupFields[52].Descriptor() // group.DefaultReasoningEffortMappings holds the default value on creation for the reasoning_effort_mappings field. group.DefaultReasoningEffortMappings = groupDescReasoningEffortMappings.Default.([]domain.ReasoningEffortMapping) // groupDescProfitControlEnabled is the schema descriptor for profit_control_enabled field. - groupDescProfitControlEnabled := groupFields[50].Descriptor() + groupDescProfitControlEnabled := groupFields[53].Descriptor() // group.DefaultProfitControlEnabled holds the default value on creation for the profit_control_enabled field. group.DefaultProfitControlEnabled = groupDescProfitControlEnabled.Default.(bool) // groupDescProfitMinMargin is the schema descriptor for profit_min_margin field. - groupDescProfitMinMargin := groupFields[51].Descriptor() + groupDescProfitMinMargin := groupFields[54].Descriptor() // group.DefaultProfitMinMargin holds the default value on creation for the profit_min_margin field. group.DefaultProfitMinMargin = groupDescProfitMinMargin.Default.(float64) // groupDescProfitSafetyBuffer is the schema descriptor for profit_safety_buffer field. - groupDescProfitSafetyBuffer := groupFields[52].Descriptor() + groupDescProfitSafetyBuffer := groupFields[55].Descriptor() // group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field. group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64) idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin() diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 1055a52eb0..e41711d024 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -158,6 +158,26 @@ func (Group) Fields() []ent.Field { SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). Comment("Codex alpha/search 网页搜索单次价格(USD/次);nil 表示使用默认价 0.01(官方 $10/1000 次)"), + // Grok Voice 显式定价(realtime / TTS / STT),不按文本 RateMultiplier。 + field.Float("audio_realtime_price_per_min"). + Optional(). + Nillable(). + Min(0). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). + Comment("Voice realtime 每分钟价格(USD)"), + field.Float("audio_tts_price_per_million_chars"). + Optional(). + Nillable(). + Min(0). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). + Comment("TTS 每百万字符价格(USD)"), + field.Float("audio_stt_price_per_hour"). + Optional(). + Nillable(). + Min(0). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). + Comment("STT 每小时价格(USD)"), + // Claude Code 客户端限制 (added by migration 029) field.Bool("claude_code_only"). Default(false). diff --git a/backend/go.sum b/backend/go.sum index e9a4c1829e..98c57415c7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -310,6 +310,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= @@ -346,6 +348,8 @@ github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7P github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -380,6 +384,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -415,6 +421,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 62960fdd83..c49baeadd3 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -129,6 +129,9 @@ type CreateGroupRequest struct { VideoPrice1080P *float64 `json:"video_price_1080p"` VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"` WebSearchPricePerCall *float64 `json:"web_search_price_per_call"` + AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"` + AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"` + AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"` ClaudeCodeOnly bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"` @@ -192,6 +195,9 @@ type UpdateGroupRequest struct { VideoPrice1080P *float64 `json:"video_price_1080p"` VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"` WebSearchPricePerCall *float64 `json:"web_search_price_per_call"` + AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"` + AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"` + AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"` ClaudeCodeOnly *bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request"` @@ -523,6 +529,9 @@ func (h *GroupHandler) Create(c *gin.Context) { VideoPrice1080P: req.VideoPrice1080P, VideoModelPrices: req.VideoModelPrices, WebSearchPricePerCall: req.WebSearchPricePerCall, + AudioRealtimePricePerMin: req.AudioRealtimePricePerMin, + AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars, + AudioSTTPricePerHour: req.AudioSttPricePerHour, ClaudeCodeOnly: req.ClaudeCodeOnly, FallbackGroupID: req.FallbackGroupID, FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest, @@ -646,6 +655,9 @@ func (h *GroupHandler) Update(c *gin.Context) { VideoPrice1080P: req.VideoPrice1080P, VideoModelPrices: req.VideoModelPrices, WebSearchPricePerCall: req.WebSearchPricePerCall, + AudioRealtimePricePerMin: req.AudioRealtimePricePerMin, + AudioTTSPricePerMillionChars: req.AudioTtsPricePerMillionChars, + AudioSTTPricePerHour: req.AudioSttPricePerHour, ClaudeCodeOnly: req.ClaudeCodeOnly, FallbackGroupID: req.FallbackGroupID, FallbackGroupIDOnInvalidRequest: req.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 0ac92284a7..2a5d3ec472 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -204,6 +204,9 @@ func groupFromServiceBase(g *service.Group) Group { VideoPrice1080P: g.VideoPrice1080P, VideoModelPrices: g.VideoModelPrices, WebSearchPricePerCall: g.WebSearchPricePerCall, + AudioRealtimePricePerMin: g.AudioRealtimePricePerMin, + AudioTtsPricePerMillionChars: g.AudioTTSPricePerMillionChars, + AudioSttPricePerHour: g.AudioSTTPricePerHour, ClaudeCodeOnly: g.ClaudeCodeOnly, FallbackGroupID: g.FallbackGroupID, FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 871f0ab13b..d209cbf836 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -124,7 +124,10 @@ type Group struct { // VideoModelPrices 可选按模型族×分辨率覆盖视频每秒单价 (USD/s)。 VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"` // Codex alpha/search 网页搜索单次价格(USD/次);null 表示使用默认价 0.01 - WebSearchPricePerCall *float64 `json:"web_search_price_per_call"` + WebSearchPricePerCall *float64 `json:"web_search_price_per_call"` + AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min"` + AudioTtsPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars"` + AudioSttPricePerHour *float64 `json:"audio_stt_price_per_hour"` // Claude Code 客户端限制 ClaudeCodeOnly bool `json:"claude_code_only"` diff --git a/backend/internal/handler/grok_audio.go b/backend/internal/handler/grok_audio.go new file mode 100644 index 0000000000..3047874779 --- /dev/null +++ b/backend/internal/handler/grok_audio.go @@ -0,0 +1,171 @@ +package handler + +import ( + "errors" + "io" + "net/http" + "strings" + + middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware" + "github.com/Wei-Shaw/sub2api/internal/service" + coderws "github.com/coder/websocket" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// GrokRealtime exposes xAI's native Voice Realtime WebSocket. +// Only Grok-platform API keys may use this endpoint. +func (h *OpenAIGatewayHandler) GrokRealtime(c *gin.Context) { + if c == nil || c.Request == nil || !isOpenAIWSUpgradeRequest(c.Request) { + h.errorResponse(c, http.StatusUpgradeRequired, "invalid_request_error", "WebSocket upgrade required (Upgrade: websocket)") + return + } + apiKey, ok := middleware2.GetAPIKeyFromContext(c) + if !ok || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformGrok { + h.errorResponse(c, http.StatusNotFound, "not_found_error", "Realtime API is not supported for this platform") + return + } + if !h.ensureResponsesDependencies(c, nil) { + return + } + + selection, _, err := h.gatewayService.SelectAccountWithSchedulerForCapability( + c.Request.Context(), + apiKey.GroupID, + "", + "", + "grok-4.5", + nil, + service.OpenAIUpstreamTransportHTTPSSE, + // Grok only advertises chat_completions + media capabilities on HEAD. + service.OpenAIEndpointCapabilityChatCompletions, + false, + false, + false, + service.PlatformGrok, + ) + if err != nil || selection == nil || selection.Account == nil { + h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "No available Grok accounts") + return + } + + var streamStarted bool + reqLog := requestLogger(c, "handler.openai_gateway.grok_realtime") + release, slotStatus := h.acquireResponsesAccountSlot(c, apiKey.GroupID, "", selection, true, &streamStarted, reqLog) + if slotStatus != openAISlotAcquireOK { + return + } + defer release() + + token, _, err := h.gatewayService.GetRequestCredential(c.Request.Context(), c, selection.Account) + if err != nil { + h.errorResponse(c, http.StatusBadGateway, "upstream_error", "Grok credential unavailable") + return + } + + conn, err := coderws.Accept(c.Writer, c.Request, &coderws.AcceptOptions{CompressionMode: coderws.CompressionContextTakeover}) + if err != nil { + return + } + defer conn.CloseNow() + + model := c.Query("model") + if err := h.gatewayService.ProxyGrokRealtime(c.Request.Context(), c, conn, selection.Account, token, model); err != nil { + reqLog.Info("grok_realtime.proxy_failed", zap.Error(err)) + _ = conn.Close(coderws.StatusInternalError, "upstream realtime websocket failed") + } +} + +// GrokVoice handles xAI Voice HTTP endpoints. endpoint is "tts", "stt", or "custom-voices". +func (h *OpenAIGatewayHandler) GrokVoice(c *gin.Context, endpoint string) { + apiKey, ok := middleware2.GetAPIKeyFromContext(c) + if !ok || apiKey.Group == nil || apiKey.Group.Platform != service.PlatformGrok { + h.errorResponse(c, http.StatusNotFound, "not_found_error", "Voice API is not supported for this platform") + return + } + if !h.ensureResponsesDependencies(c, nil) { + return + } + + body, err := readGrokVoiceGatewayBody(c) + if err != nil { + h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", err.Error()) + return + } + contentType := c.GetHeader("Content-Type") + if strings.TrimSpace(contentType) == "" { + contentType = "application/json" + } + + failed := map[int64]struct{}{} + var last *service.UpstreamFailoverError + reqLog := requestLogger(c, "handler.openai_gateway.grok_voice", zap.String("endpoint", endpoint)) + selectionModel := "grok-4.5" + + for attempts := 0; attempts < 4; attempts++ { + selection, _, selectErr := h.gatewayService.SelectAccountWithSchedulerForCapability( + c.Request.Context(), + apiKey.GroupID, + "", + "", + selectionModel, + failed, + service.OpenAIUpstreamTransportHTTPSSE, + service.OpenAIEndpointCapabilityChatCompletions, + false, + false, + false, + service.PlatformGrok, + ) + if selectErr != nil || selection == nil || selection.Account == nil { + if last != nil { + h.handleFailoverExhausted(c, last, false) + } else { + h.errorResponse(c, http.StatusServiceUnavailable, "api_error", "No available Grok accounts") + } + return + } + account := selection.Account + var started bool + release, status := h.acquireResponsesAccountSlot(c, apiKey.GroupID, "", selection, false, &started, reqLog) + if status == openAISlotAcquireProfitVetoed { + failed[account.ID] = struct{}{} + continue + } + if status != openAISlotAcquireOK { + // Failed already wrote error response (or transient reject). + if status == openAISlotAcquireFailed && len(failed) == 0 { + // Slot path wrote the response; stop. + return + } + failed[account.ID] = struct{}{} + continue + } + result, forwardErr := func() (*service.OpenAIForwardResult, error) { + defer release() + return h.gatewayService.ForwardGrokVoice(c.Request.Context(), c, account, endpoint, body, contentType) + }() + if forwardErr == nil { + _ = result + return + } + var failoverErr *service.UpstreamFailoverError + if errors.As(forwardErr, &failoverErr) && failoverErr.ShouldRetryNextAccount() { + failed[account.ID] = struct{}{} + last = failoverErr + continue + } + // Non-failover errors: handleGrokMediaErrorResponse / transport already wrote response. + return + } + if last != nil { + h.handleFailoverExhausted(c, last, false) + } +} + +func readGrokVoiceGatewayBody(c *gin.Context) ([]byte, error) { + if c == nil || c.Request == nil || c.Request.Body == nil { + return nil, errors.New("request body is required") + } + return io.ReadAll(c.Request.Body) +} diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 06ef485b70..2f1f851ed4 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -191,6 +191,9 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se group.FieldVideoPrice720p, group.FieldVideoPrice1080p, group.FieldWebSearchPricePerCall, + group.FieldAudioRealtimePricePerMin, + group.FieldAudioTtsPricePerMillionChars, + group.FieldAudioSttPricePerHour, group.FieldClaudeCodeOnly, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, @@ -971,6 +974,9 @@ func groupEntityToService(g *dbent.Group) *service.Group { VideoPrice1080P: g.VideoPrice1080p, VideoModelPrices: service.NormalizeVideoModelPrices(g.VideoModelPrices), WebSearchPricePerCall: g.WebSearchPricePerCall, + AudioRealtimePricePerMin: g.AudioRealtimePricePerMin, + AudioTTSPricePerMillionChars: g.AudioTtsPricePerMillionChars, + AudioSTTPricePerHour: g.AudioSttPricePerHour, DefaultValidityDays: g.DefaultValidityDays, ClaudeCodeOnly: g.ClaudeCodeOnly, FallbackGroupID: g.FallbackGroupID, diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 7dff754fab..3232e51c74 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -84,6 +84,9 @@ func createGroupRecord(ctx context.Context, client *dbent.Client, groupIn *servi SetNillableVideoPrice1080p(groupIn.VideoPrice1080P). SetVideoModelPrices(service.NormalizeVideoModelPrices(groupIn.VideoModelPrices)). SetNillableWebSearchPricePerCall(groupIn.WebSearchPricePerCall). + SetNillableAudioRealtimePricePerMin(groupIn.AudioRealtimePricePerMin). + SetNillableAudioTtsPricePerMillionChars(groupIn.AudioTTSPricePerMillionChars). + SetNillableAudioSttPricePerHour(groupIn.AudioSTTPricePerHour). SetDefaultValidityDays(groupIn.DefaultValidityDays). SetClaudeCodeOnly(groupIn.ClaudeCodeOnly). SetNillableFallbackGroupID(groupIn.FallbackGroupID). @@ -329,6 +332,21 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er } else { builder = builder.ClearWebSearchPricePerCall() } + if groupIn.AudioRealtimePricePerMin != nil { + builder = builder.SetAudioRealtimePricePerMin(*groupIn.AudioRealtimePricePerMin) + } else { + builder = builder.ClearAudioRealtimePricePerMin() + } + if groupIn.AudioTTSPricePerMillionChars != nil { + builder = builder.SetAudioTtsPricePerMillionChars(*groupIn.AudioTTSPricePerMillionChars) + } else { + builder = builder.ClearAudioTtsPricePerMillionChars() + } + if groupIn.AudioSTTPricePerHour != nil { + builder = builder.SetAudioSttPricePerHour(*groupIn.AudioSTTPricePerHour) + } else { + builder = builder.ClearAudioSttPricePerHour() + } // 处理 FallbackGroupID:nil 时清除,否则设置 if groupIn.FallbackGroupID != nil { diff --git a/backend/internal/server/routes/gateway.go b/backend/internal/server/routes/gateway.go index 1ce1fd9788..e1522f9eed 100644 --- a/backend/internal/server/routes/gateway.go +++ b/backend/internal/server/routes/gateway.go @@ -261,6 +261,30 @@ func RegisterGatewayRoutes( gateway.POST("/videos/extensions", videoExtensionHandler) gateway.GET("/videos/:request_id", videoStatusHandler) gateway.GET("/videos/:request_id/content", videoContentHandler) + + // xAI Voice APIs (Grok platform only): HTTP TTS/STT + Realtime WS. + // Not part of the creation-center product surface — gateway relay only. + voiceHandler := func(endpoint string) gin.HandlerFunc { + return func(c *gin.Context) { + if getGroupPlatform(c) != service.PlatformGrok { + service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate) + c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}}) + return + } + h.OpenAIGateway.GrokVoice(c, endpoint) + } + } + gateway.POST("/tts", voiceHandler("tts")) + gateway.POST("/stt", voiceHandler("stt")) + gateway.POST("/custom-voices", voiceHandler("custom-voices")) + gateway.GET("/realtime", func(c *gin.Context) { + if getGroupPlatform(c) != service.PlatformGrok { + service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate) + c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Realtime API is not supported for this platform"}}) + return + } + h.OpenAIGateway.GrokRealtime(c) + }) } // Gemini 原生 API 兼容层(Gemini SDK/CLI 直连) @@ -340,6 +364,28 @@ func RegisterGatewayRoutes( r.GET("/videos/:request_id", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, videoStatusHandler) r.GET("/videos/:request_id/content", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, videoContentHandler) + rootVoiceHandler := func(endpoint string) gin.HandlerFunc { + return func(c *gin.Context) { + if getGroupPlatform(c) != service.PlatformGrok { + service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate) + c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Voice API is not supported for this platform"}}) + return + } + h.OpenAIGateway.GrokVoice(c, endpoint) + } + } + r.POST("/tts", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, rootVoiceHandler("tts")) + r.POST("/stt", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, rootVoiceHandler("stt")) + r.POST("/custom-voices", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, rootVoiceHandler("custom-voices")) + r.GET("/realtime", bodyLimit, clientRequestID, opsErrorLogger, endpointNorm, gin.HandlerFunc(apiKeyAuth), compositeTarget, requireGroupAnthropic, func(c *gin.Context) { + if getGroupPlatform(c) != service.PlatformGrok { + service.MarkOpsClientBusinessLimited(c, service.OpsClientBusinessLimitedReasonLocalFeatureGate) + c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"type": "not_found_error", "message": "Realtime API is not supported for this platform"}}) + return + } + h.OpenAIGateway.GrokRealtime(c) + }) + // Antigravity 模型列表 r.GET("/antigravity/models", gin.HandlerFunc(apiKeyAuth), requireGroupAnthropic, h.Gateway.AntigravityModels) diff --git a/backend/internal/service/admin_group.go b/backend/internal/service/admin_group.go index c591c25750..e424bbf2a4 100644 --- a/backend/internal/service/admin_group.go +++ b/backend/internal/service/admin_group.go @@ -328,6 +328,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn videoPrice720P := normalizePrice(input.VideoPrice720P) videoPrice1080P := normalizePrice(input.VideoPrice1080P) webSearchPricePerCall := normalizePrice(input.WebSearchPricePerCall) + audioRealtimePricePerMin := normalizePrice(input.AudioRealtimePricePerMin) + audioTTSPricePerMillionChars := normalizePrice(input.AudioTTSPricePerMillionChars) + audioSTTPricePerHour := normalizePrice(input.AudioSTTPricePerHour) imageRateMultiplier := 1.0 if input.ImageRateMultiplier != nil { if *input.ImageRateMultiplier < 0 { @@ -478,6 +481,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn VideoPrice1080P: videoPrice1080P, VideoModelPrices: NormalizeVideoModelPrices(input.VideoModelPrices), WebSearchPricePerCall: webSearchPricePerCall, + AudioRealtimePricePerMin: audioRealtimePricePerMin, + AudioTTSPricePerMillionChars: audioTTSPricePerMillionChars, + AudioSTTPricePerHour: audioSTTPricePerHour, ClaudeCodeOnly: input.ClaudeCodeOnly, FallbackGroupID: input.FallbackGroupID, FallbackGroupIDOnInvalidRequest: fallbackOnInvalidRequest, @@ -763,6 +769,15 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if input.WebSearchPricePerCall != nil { group.WebSearchPricePerCall = normalizePrice(input.WebSearchPricePerCall) } + if input.AudioRealtimePricePerMin != nil { + group.AudioRealtimePricePerMin = normalizePrice(input.AudioRealtimePricePerMin) + } + if input.AudioTTSPricePerMillionChars != nil { + group.AudioTTSPricePerMillionChars = normalizePrice(input.AudioTTSPricePerMillionChars) + } + if input.AudioSTTPricePerHour != nil { + group.AudioSTTPricePerHour = normalizePrice(input.AudioSTTPricePerHour) + } // Claude Code 客户端限制 if input.ClaudeCodeOnly != nil { diff --git a/backend/internal/service/admin_group_duplicate.go b/backend/internal/service/admin_group_duplicate.go index d1fcdd1475..4a4757ee0a 100644 --- a/backend/internal/service/admin_group_duplicate.go +++ b/backend/internal/service/admin_group_duplicate.go @@ -130,6 +130,9 @@ func cloneGroupForDuplicate(source *Group, operationID string) *Group { VideoPrice1080P: cloneGroupValuePointer(source.VideoPrice1080P), VideoModelPrices: cloneGroupVideoModelPrices(source.VideoModelPrices), WebSearchPricePerCall: cloneGroupValuePointer(source.WebSearchPricePerCall), + AudioRealtimePricePerMin: cloneGroupValuePointer(source.AudioRealtimePricePerMin), + AudioTTSPricePerMillionChars: cloneGroupValuePointer(source.AudioTTSPricePerMillionChars), + AudioSTTPricePerHour: cloneGroupValuePointer(source.AudioSTTPricePerHour), ClaudeCodeOnly: source.ClaudeCodeOnly, FallbackGroupID: cloneGroupValuePointer(source.FallbackGroupID), FallbackGroupIDOnInvalidRequest: cloneGroupValuePointer(source.FallbackGroupIDOnInvalidRequest), diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index a338956b68..d075eb4280 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -242,8 +242,12 @@ type CreateGroupInput struct { VideoModelPrices map[string]map[string]float64 // Codex alpha/search 网页搜索单次价格(USD/次,仅 openai 平台使用);nil/负数按默认价 0.01 处理 WebSearchPricePerCall *float64 - ClaudeCodeOnly bool // 仅允许 Claude Code 客户端 - FallbackGroupID *int64 // 降级分组 ID + // Grok Voice 显式定价(分组级) + AudioRealtimePricePerMin *float64 + AudioTTSPricePerMillionChars *float64 + AudioSTTPricePerHour *float64 + ClaudeCodeOnly bool // 仅允许 Claude Code 客户端 + FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) FallbackGroupIDOnInvalidRequest *int64 // 模型路由配置(仅 anthropic 平台使用) @@ -309,8 +313,12 @@ type UpdateGroupInput struct { VideoModelPrices map[string]map[string]float64 // Codex alpha/search 网页搜索单次价格(USD/次);nil 表示不修改,负数表示清除回默认价 0.01 WebSearchPricePerCall *float64 - ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端 - FallbackGroupID *int64 // 降级分组 ID + // Grok Voice 显式定价;nil 表示不修改,负数表示清除 + AudioRealtimePricePerMin *float64 + AudioTTSPricePerMillionChars *float64 + AudioSTTPricePerHour *float64 + ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端 + FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) FallbackGroupIDOnInvalidRequest *int64 // 模型路由配置(仅 anthropic 平台使用) diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index a665cdad3c..b04820750b 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -79,6 +79,9 @@ type APIKeyAuthGroupSnapshot struct { VideoPrice720P *float64 `json:"video_price_720p,omitempty"` VideoPrice1080P *float64 `json:"video_price_1080p,omitempty"` WebSearchPricePerCall *float64 `json:"web_search_price_per_call,omitempty"` + AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min,omitempty"` + AudioTTSPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"` + AudioSTTPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"` ClaudeCodeOnly bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id,omitempty"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request,omitempty"` diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index e834ce62de..54908b573b 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -401,6 +401,9 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey) VideoPrice720P: apiKey.Group.VideoPrice720P, VideoPrice1080P: apiKey.Group.VideoPrice1080P, WebSearchPricePerCall: apiKey.Group.WebSearchPricePerCall, + AudioRealtimePricePerMin: apiKey.Group.AudioRealtimePricePerMin, + AudioTTSPricePerMillionChars: apiKey.Group.AudioTTSPricePerMillionChars, + AudioSTTPricePerHour: apiKey.Group.AudioSTTPricePerHour, ClaudeCodeOnly: apiKey.Group.ClaudeCodeOnly, FallbackGroupID: apiKey.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: apiKey.Group.FallbackGroupIDOnInvalidRequest, @@ -491,6 +494,9 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho VideoPrice720P: snapshot.Group.VideoPrice720P, VideoPrice1080P: snapshot.Group.VideoPrice1080P, WebSearchPricePerCall: snapshot.Group.WebSearchPricePerCall, + AudioRealtimePricePerMin: snapshot.Group.AudioRealtimePricePerMin, + AudioTTSPricePerMillionChars: snapshot.Group.AudioTTSPricePerMillionChars, + AudioSTTPricePerHour: snapshot.Group.AudioSTTPricePerHour, ClaudeCodeOnly: snapshot.Group.ClaudeCodeOnly, FallbackGroupID: snapshot.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: snapshot.Group.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/service/grok_audio.go b/backend/internal/service/grok_audio.go new file mode 100644 index 0000000000..683b4fee61 --- /dev/null +++ b/backend/internal/service/grok_audio.go @@ -0,0 +1,172 @@ +package service + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + coderws "github.com/coder/websocket" + "github.com/gin-gonic/gin" +) + +// supportedGrokVoiceHTTPEndpoints are xAI Voice HTTP paths we forward as-is. +var supportedGrokVoiceHTTPEndpoints = map[string]struct{}{ + "tts": {}, + "stt": {}, + "custom-voices": {}, +} + +// ForwardGrokVoice forwards the official xAI Voice HTTP APIs (/tts, /stt, /custom-voices). +// The response is intentionally passed through because TTS returns audio bytes +// while STT returns JSON and xAI may add format-specific headers. +func (s *OpenAIGatewayService) ForwardGrokVoice(ctx context.Context, c *gin.Context, account *Account, endpoint string, body []byte, contentType string) (*OpenAIForwardResult, error) { + if s == nil || account == nil { + return nil, fmt.Errorf("grok voice service/account is required") + } + if account.Platform != PlatformGrok { + return nil, fmt.Errorf("account platform %s is not supported for grok voice", account.Platform) + } + endpoint = strings.Trim(strings.TrimSpace(endpoint), "/") + if _, ok := supportedGrokVoiceHTTPEndpoints[endpoint]; !ok { + return nil, fmt.Errorf("unsupported grok voice endpoint: %s", endpoint) + } + token, _, err := s.getRequestCredential(ctx, c, account) + if err != nil { + return nil, err + } + targetURL, err := buildGrokVoiceURL(account, s.cfg, endpoint) + if err != nil { + return nil, err + } + upstreamCtx, release := detachUpstreamContext(ctx) + defer release() + req, err := http.NewRequestWithContext(upstreamCtx, http.MethodPost, targetURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json, audio/*") + if strings.TrimSpace(contentType) == "" { + contentType = "application/json" + } + req.Header.Set("Content-Type", contentType) + // Voice hits api.x.ai (not CLI proxy). Still stamp CLI identity headers for + // consistency with other Grok outbound calls; transport will not rewrite + // non-CLI-proxy hosts. + applyGrokCLIHeaders(req.Header) + account.ApplyHeaderOverrides(req.Header) + + proxyURL := "" + if account.ProxyID != nil && account.Proxy != nil { + proxyURL = account.Proxy.URL() + } + started := time.Now() + resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency) + SetOpsLatencyMs(c, OpsUpstreamLatencyMsKey, time.Since(started).Milliseconds()) + if err != nil { + return nil, s.handleOpenAIUpstreamTransportError(ctx, c, account, err, false) + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode >= 400 { + return s.handleGrokMediaErrorResponse(ctx, resp, c, account, resp.Header.Get("x-request-id"), endpoint) + } + data, err := ReadUpstreamResponseBody(resp.Body, s.cfg, c, openAITooLargeError) + if err != nil { + return nil, err + } + writeGrokMediaResponse(c, resp, data, s.responseHeaderFilter) + return &OpenAIForwardResult{ + RequestID: firstNonEmpty(resp.Header.Get("x-request-id"), resp.Header.Get("xai-request-id")), + Model: endpoint, + UpstreamModel: endpoint, + Duration: time.Since(started), + }, nil +} + +// ProxyGrokRealtime relays JSON Realtime events to xAI's native Voice WS. +// Audio is carried as base64 inside JSON events, so preserving the JSON bytes +// is sufficient and avoids translating protocol event types. +func (s *OpenAIGatewayService) ProxyGrokRealtime(ctx context.Context, c *gin.Context, client *coderws.Conn, account *Account, token, model string) error { + if s == nil || client == nil || account == nil { + return fmt.Errorf("realtime service, client, and account are required") + } + if account.Platform != PlatformGrok { + return fmt.Errorf("account platform %s is not supported for grok realtime", account.Platform) + } + base, err := buildGrokVoiceURL(account, s.cfg, "realtime") + if err != nil { + return err + } + u, err := url.Parse(base) + if err != nil { + return err + } + u.Scheme = "wss" + u.RawQuery = "model=" + url.QueryEscape(firstNonEmpty(model, "grok-voice-latest")) + headers := http.Header{"Authorization": []string{"Bearer " + token}} + // Stamp CLI identity for consistency (host is api.x.ai; no CLI-proxy rewrite). + applyGrokCLIHeaders(headers) + if account != nil { + account.ApplyHeaderOverrides(headers) + } + + dialer := s.getOpenAIWSPassthroughDialer() + proxyURL := "" + if account.ProxyID != nil && account.Proxy != nil { + proxyURL = account.Proxy.URL() + } + upstream, _, _, err := dialer.Dial(ctx, u.String(), headers, proxyURL) + if err != nil { + return err + } + defer func() { _ = upstream.Close() }() + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + errCh := make(chan error, 2) + + // Upstream → client + go func() { + for { + msg, readErr := upstream.ReadMessage(ctx) + if readErr != nil { + errCh <- readErr + return + } + if writeErr := client.Write(ctx, coderws.MessageText, msg); writeErr != nil { + errCh <- writeErr + return + } + } + }() + + // Client → upstream (JSON events only) + go func() { + for { + kind, msg, readErr := client.Read(ctx) + if readErr != nil { + errCh <- readErr + return + } + if kind != coderws.MessageText && kind != coderws.MessageBinary { + continue + } + var raw json.RawMessage + if unmarshalErr := json.Unmarshal(msg, &raw); unmarshalErr != nil { + errCh <- fmt.Errorf("invalid realtime event: %w", unmarshalErr) + return + } + if writeErr := upstream.WriteJSON(ctx, raw); writeErr != nil { + errCh <- writeErr + return + } + } + }() + + return <-errCh +} diff --git a/backend/internal/service/grok_audio_test.go b/backend/internal/service/grok_audio_test.go new file mode 100644 index 0000000000..6647ccdff3 --- /dev/null +++ b/backend/internal/service/grok_audio_test.go @@ -0,0 +1,57 @@ +package service + +import ( + "context" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/pkg/xai" + "github.com/stretchr/testify/require" +) + +func TestBuildGrokVoiceURL_UsesAPIDefaultForCLIProxyBase(t *testing.T) { + account := &Account{ + Platform: PlatformGrok, + Type: AccountTypeOAuth, + Credentials: map[string]any{ + "base_url": xai.DefaultCLIBaseURL, + }, + } + url, err := buildGrokVoiceURL(account, nil, "tts") + require.NoError(t, err) + require.Equal(t, xai.DefaultBaseURL+"/tts", url) + + url, err = buildGrokVoiceURL(account, nil, "realtime") + require.NoError(t, err) + require.Equal(t, xai.DefaultBaseURL+"/realtime", url) +} + +func TestBuildGrokVoiceURL_EmptyBaseFallsBackToAPI(t *testing.T) { + account := &Account{ + Platform: PlatformGrok, + Type: AccountTypeOAuth, + Credentials: map[string]any{}, + } + url, err := buildGrokVoiceURL(account, nil, "stt") + require.NoError(t, err) + require.Equal(t, xai.DefaultBaseURL+"/stt", url) +} + +func TestBuildGrokVoiceURL_RequiresEndpoint(t *testing.T) { + account := &Account{Platform: PlatformGrok, Type: AccountTypeOAuth} + _, err := buildGrokVoiceURL(account, nil, " ") + require.Error(t, err) +} + +func TestForwardGrokVoice_RejectsNonGrok(t *testing.T) { + svc := &OpenAIGatewayService{} + _, err := svc.ForwardGrokVoice(context.Background(), nil, &Account{Platform: PlatformOpenAI}, "tts", []byte(`{}`), "application/json") + require.Error(t, err) + require.Contains(t, err.Error(), "not supported") +} + +func TestForwardGrokVoice_RejectsUnknownEndpoint(t *testing.T) { + svc := &OpenAIGatewayService{} + _, err := svc.ForwardGrokVoice(context.Background(), nil, &Account{Platform: PlatformGrok}, "unknown", []byte(`{}`), "application/json") + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported") +} diff --git a/backend/internal/service/grok_upstream_url.go b/backend/internal/service/grok_upstream_url.go index 362e746e6f..6b757ba444 100644 --- a/backend/internal/service/grok_upstream_url.go +++ b/backend/internal/service/grok_upstream_url.go @@ -3,6 +3,7 @@ package service import ( "errors" "fmt" + "strings" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/xai" @@ -122,3 +123,34 @@ func buildGrokMediaURL(account *Account, cfg *config.Config, endpoint GrokMediaE return "", fmt.Errorf("unsupported grok media endpoint: %s", endpoint) } } + +// buildGrokVoiceURL returns the official xAI Voice API endpoint. +// Voice HTTP (/tts, /stt, /custom-voices) and WS (/realtime) are only exposed +// by api.x.ai — the CLI chat proxy does not implement them. When the account +// base_url points at the CLI proxy (or is empty), fall back to DefaultBaseURL. +func buildGrokVoiceURL(account *Account, cfg *config.Config, endpoint string) (string, error) { + validator, err := grokBaseURLValidator(account, cfg) + if err != nil { + return "", err + } + base := "" + if account != nil { + base = account.GetGrokMediaBaseURL() + } + if strings.TrimSpace(base) == "" || isGrokCLIProxyBaseURL(base) { + base = xai.DefaultBaseURL + } + validated, err := validator(base) + if err != nil { + return "", err + } + ep := strings.Trim(strings.TrimSpace(endpoint), "/") + if ep == "" { + return "", fmt.Errorf("voice endpoint is required") + } + return strings.TrimRight(validated, "/") + "/" + ep, nil +} + +func isGrokCLIProxyBaseURL(raw string) bool { + return isGrokCLIProxyTarget(raw) +} diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 7ae43c3f37..31f6fb334e 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -63,6 +63,11 @@ type Group struct { // nil 表示使用默认价 defaultWebSearchPricePerCall(官方 $10/1000 次)。 WebSearchPricePerCall *float64 + // Grok Voice 显式定价(分组级,不按文本 RateMultiplier)。 + AudioRealtimePricePerMin *float64 + AudioTTSPricePerMillionChars *float64 + AudioSTTPricePerHour *float64 + // Claude Code 客户端限制 ClaudeCodeOnly bool FallbackGroupID *int64 diff --git a/backend/migrations/218_group_audio_voice_pricing.sql b/backend/migrations/218_group_audio_voice_pricing.sql new file mode 100644 index 0000000000..f96b1f168e --- /dev/null +++ b/backend/migrations/218_group_audio_voice_pricing.sql @@ -0,0 +1,5 @@ +-- Grok Voice 显式定价:realtime / TTS / STT。 +-- NULL 表示未配置(当前中继不强制计费;字段预留给分组配置与后续计费接线)。 +ALTER TABLE groups ADD COLUMN IF NOT EXISTS audio_realtime_price_per_min DECIMAL(20,8); +ALTER TABLE groups ADD COLUMN IF NOT EXISTS audio_tts_price_per_million_chars DECIMAL(20,8); +ALTER TABLE groups ADD COLUMN IF NOT EXISTS audio_stt_price_per_hour DECIMAL(20,8); diff --git a/frontend/src/i18n/locales/en/admin/overview.ts b/frontend/src/i18n/locales/en/admin/overview.ts index 4429a3adc1..502e8bd31f 100644 --- a/frontend/src/i18n/locales/en/admin/overview.ts +++ b/frontend/src/i18n/locales/en/admin/overview.ts @@ -994,6 +994,14 @@ export default { finalPricePreview: 'Final per-second price preview', notConfigured: 'Not configured' }, + voicePricing: { + title: 'Grok Voice Pricing', + description: 'Optional per-group prices for Voice realtime / TTS / STT (USD). Leave empty to leave unpriced.', + audioRealtimePerMin: 'Realtime price per minute (USD)', + audioTtsPerMillionChars: 'TTS price per million chars (USD)', + audioSttPerHour: 'STT price per hour (USD)', + pricePlaceholder: 'optional' + }, webSearchPricing: { title: 'Codex Web Search Pricing', pricePerCall: 'Price per search call (USD)', diff --git a/frontend/src/i18n/locales/zh/admin/overview.ts b/frontend/src/i18n/locales/zh/admin/overview.ts index 1ff5b3555e..87a5095e50 100644 --- a/frontend/src/i18n/locales/zh/admin/overview.ts +++ b/frontend/src/i18n/locales/zh/admin/overview.ts @@ -991,6 +991,14 @@ export default { finalPricePreview: '最终每秒价格预览', notConfigured: '未配置' }, + voicePricing: { + title: 'Grok Voice 定价', + description: '分组级 Voice realtime / TTS / STT 单价(USD)。留空表示未配置。', + audioRealtimePerMin: 'Realtime 每分钟价格(USD)', + audioTtsPerMillionChars: 'TTS 每百万字符价格(USD)', + audioSttPerHour: 'STT 每小时价格(USD)', + pricePlaceholder: '可选' + }, webSearchPricing: { title: 'Codex 网页搜索计费', pricePerCall: '搜索单次价格(USD/次)', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 28d94c6dad..a52c6d48e0 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -572,6 +572,10 @@ export interface Group { video_model_prices?: VideoModelPrices // Codex 网页搜索单次价格(USD/次);null 表示使用默认价 0.01 web_search_price_per_call: number | null + // Grok Voice 显式定价(分组级) + audio_realtime_price_per_min: number | null + audio_tts_price_per_million_chars: number | null + audio_stt_price_per_hour: number | null // 高峰时段倍率配置 peak_rate_enabled: boolean peak_start: string @@ -772,6 +776,9 @@ export interface CreateGroupRequest { video_price_1080p?: number | null video_model_prices?: VideoModelPrices web_search_price_per_call?: number | null + audio_realtime_price_per_min?: number | null + audio_tts_price_per_million_chars?: number | null + audio_stt_price_per_hour?: number | null peak_rate_enabled?: boolean peak_start?: string peak_end?: string @@ -828,6 +835,9 @@ export interface UpdateGroupRequest { video_price_1080p?: number | null video_model_prices?: VideoModelPrices web_search_price_per_call?: number | null + audio_realtime_price_per_min?: number | null + audio_tts_price_per_million_chars?: number | null + audio_stt_price_per_hour?: number | null peak_rate_enabled?: boolean peak_start?: string peak_end?: string diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index f710e4b96e..90777188c3 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -1483,6 +1483,57 @@ + + +
+ {{ t("admin.groups.voicePricing.description") }} +
++ {{ t("admin.groups.voicePricing.description") }} +
+