mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-20 00:18:55 +00:00
feat(grok): 网关 Voice TTS/STT/Realtime 与分组音频定价
- 中继 xAI Voice HTTP(/tts /stt /custom-voices)与 Realtime WS(/realtime) 仅 Grok 分组可用;账号选调度沿用 chat 能力,不依赖创作中心 - Voice URL 固定走 api.x.ai(CLI proxy base 自动回落) - 分组级 audio_realtime / TTS / STT 单价:schema + migration 218 + admin API + GroupsView 表单与 i18n(无创作中心 UI)
This commit is contained in:
+43
-1
@@ -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(", ")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+322
-1
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 平台使用)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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)',
|
||||
|
||||
@@ -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/次)',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1483,6 +1483,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Grok Voice 显式定价(仅 grok 平台) -->
|
||||
<div
|
||||
v-if="createForm.platform === 'grok'"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ t("admin.groups.voicePricing.title") }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.voicePricing.description") }}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioRealtimePerMin") }}</label>
|
||||
<input
|
||||
v-model.number="createForm.audio_realtime_price_per_min"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="create-audio-realtime-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioTtsPerMillionChars") }}</label>
|
||||
<input
|
||||
v-model.number="createForm.audio_tts_price_per_million_chars"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="create-audio-tts-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioSttPerHour") }}</label>
|
||||
<input
|
||||
v-model.number="createForm.audio_stt_price_per_hour"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="create-audio-stt-price"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OpenAI Live 开关(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="createForm.platform === 'openai'"
|
||||
@@ -3123,6 +3174,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Grok Voice 显式定价(仅 grok 平台) -->
|
||||
<div
|
||||
v-if="editForm.platform === 'grok'"
|
||||
class="border-t border-gray-200 dark:border-dark-400 pt-4 mt-4"
|
||||
>
|
||||
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{{ t("admin.groups.voicePricing.title") }}
|
||||
</h4>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
{{ t("admin.groups.voicePricing.description") }}
|
||||
</p>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioRealtimePerMin") }}</label>
|
||||
<input
|
||||
v-model.number="editForm.audio_realtime_price_per_min"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="edit-audio-realtime-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioTtsPerMillionChars") }}</label>
|
||||
<input
|
||||
v-model.number="editForm.audio_tts_price_per_million_chars"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="edit-audio-tts-price"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t("admin.groups.voicePricing.audioSttPerHour") }}</label>
|
||||
<input
|
||||
v-model.number="editForm.audio_stt_price_per_hour"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
min="0"
|
||||
class="input"
|
||||
:placeholder="t('admin.groups.voicePricing.pricePlaceholder')"
|
||||
data-testid="edit-audio-stt-price"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OpenAI Live 开关(仅 openai 平台) -->
|
||||
<div
|
||||
v-if="editForm.platform === 'openai'"
|
||||
@@ -4806,6 +4908,9 @@ const createForm = reactive({
|
||||
video_model_prices: createVideoModelPricesForm(),
|
||||
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
|
||||
web_search_price_per_call: null as number | null,
|
||||
audio_realtime_price_per_min: null as number | null,
|
||||
audio_tts_price_per_million_chars: null as number | null,
|
||||
audio_stt_price_per_hour: null as number | null,
|
||||
// 高峰时段倍率配置
|
||||
peak_rate_enabled: false,
|
||||
peak_start: "",
|
||||
@@ -5161,6 +5266,9 @@ const editForm = reactive({
|
||||
video_model_prices: createVideoModelPricesForm(),
|
||||
// Codex 网页搜索按次计费(仅 openai 平台使用);null = 使用默认价 0.01
|
||||
web_search_price_per_call: null as number | null,
|
||||
audio_realtime_price_per_min: null as number | null,
|
||||
audio_tts_price_per_million_chars: null as number | null,
|
||||
audio_stt_price_per_hour: null as number | null,
|
||||
// 高峰时段倍率配置
|
||||
peak_rate_enabled: false,
|
||||
peak_start: "",
|
||||
@@ -5611,6 +5719,9 @@ const closeCreateModal = () => {
|
||||
createForm.video_price_1080p = null;
|
||||
createForm.video_model_prices = createVideoModelPricesForm();
|
||||
createForm.web_search_price_per_call = null;
|
||||
createForm.audio_realtime_price_per_min = null;
|
||||
createForm.audio_tts_price_per_million_chars = null;
|
||||
createForm.audio_stt_price_per_hour = null;
|
||||
createForm.peak_rate_enabled = false;
|
||||
createForm.peak_start = "";
|
||||
createForm.peak_end = "";
|
||||
@@ -5775,6 +5886,15 @@ const handleCreateGroup = async () => {
|
||||
requestData.video_price_480p = emptyToNull(requestData.video_price_480p);
|
||||
requestData.video_price_720p = emptyToNull(requestData.video_price_720p);
|
||||
requestData.video_price_1080p = emptyToNull(requestData.video_price_1080p);
|
||||
requestData.audio_realtime_price_per_min = emptyToNull(
|
||||
requestData.audio_realtime_price_per_min,
|
||||
);
|
||||
requestData.audio_tts_price_per_million_chars = emptyToNull(
|
||||
requestData.audio_tts_price_per_million_chars,
|
||||
);
|
||||
requestData.audio_stt_price_per_hour = emptyToNull(
|
||||
requestData.audio_stt_price_per_hour,
|
||||
);
|
||||
requestData.web_search_price_per_call = emptyToNull(
|
||||
requestData.web_search_price_per_call,
|
||||
);
|
||||
@@ -5835,6 +5955,9 @@ const handleEdit = async (group: AdminGroup) => {
|
||||
group.video_model_prices,
|
||||
);
|
||||
editForm.web_search_price_per_call = group.web_search_price_per_call ?? null;
|
||||
editForm.audio_realtime_price_per_min = group.audio_realtime_price_per_min ?? null;
|
||||
editForm.audio_tts_price_per_million_chars = group.audio_tts_price_per_million_chars ?? null;
|
||||
editForm.audio_stt_price_per_hour = group.audio_stt_price_per_hour ?? null;
|
||||
editForm.peak_rate_enabled = group.peak_rate_enabled ?? false;
|
||||
editForm.peak_start = group.peak_start ?? "";
|
||||
editForm.peak_end = group.peak_end ?? "";
|
||||
@@ -5916,6 +6039,9 @@ const closeEditModal = () => {
|
||||
editForm.video_price_1080p = null;
|
||||
editForm.video_model_prices = createVideoModelPricesForm();
|
||||
editForm.web_search_price_per_call = null;
|
||||
editForm.audio_realtime_price_per_min = null;
|
||||
editForm.audio_tts_price_per_million_chars = null;
|
||||
editForm.audio_stt_price_per_hour = null;
|
||||
resetMessagesDispatchFormState(editForm);
|
||||
editForm.allow_live = false;
|
||||
resetModelsListState(editModelsListState);
|
||||
@@ -6021,6 +6147,15 @@ const handleUpdateGroup = async () => {
|
||||
payload.video_price_480p = emptyPriceToClear(payload.video_price_480p);
|
||||
payload.video_price_720p = emptyPriceToClear(payload.video_price_720p);
|
||||
payload.video_price_1080p = emptyPriceToClear(payload.video_price_1080p);
|
||||
payload.audio_realtime_price_per_min = emptyPriceToClear(
|
||||
payload.audio_realtime_price_per_min,
|
||||
);
|
||||
payload.audio_tts_price_per_million_chars = emptyPriceToClear(
|
||||
payload.audio_tts_price_per_million_chars,
|
||||
);
|
||||
payload.audio_stt_price_per_hour = emptyPriceToClear(
|
||||
payload.audio_stt_price_per_hour,
|
||||
);
|
||||
payload.web_search_price_per_call = emptyPriceToClear(
|
||||
payload.web_search_price_per_call,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user