diff --git a/backend/ent/channelmonitor.go b/backend/ent/channelmonitor.go index dbb73362..defd06c6 100644 --- a/backend/ent/channelmonitor.go +++ b/backend/ent/channelmonitor.go @@ -27,6 +27,8 @@ type ChannelMonitor struct { Name string `json:"name,omitempty"` // Provider holds the value of the "provider" field. Provider channelmonitor.Provider `json:"provider,omitempty"` + // OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions + APIMode string `json:"api_mode,omitempty"` // Provider base origin, e.g. https://api.openai.com Endpoint string `json:"endpoint,omitempty"` // AES-256-GCM encrypted API key @@ -112,7 +114,7 @@ func (*ChannelMonitor) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case channelmonitor.FieldID, channelmonitor.FieldIntervalSeconds, channelmonitor.FieldCreatedBy, channelmonitor.FieldTemplateID: values[i] = new(sql.NullInt64) - case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode: + case channelmonitor.FieldName, channelmonitor.FieldProvider, channelmonitor.FieldAPIMode, channelmonitor.FieldEndpoint, channelmonitor.FieldAPIKeyEncrypted, channelmonitor.FieldPrimaryModel, channelmonitor.FieldGroupName, channelmonitor.FieldBodyOverrideMode: values[i] = new(sql.NullString) case channelmonitor.FieldCreatedAt, channelmonitor.FieldUpdatedAt, channelmonitor.FieldLastCheckedAt: values[i] = new(sql.NullTime) @@ -161,6 +163,12 @@ func (_m *ChannelMonitor) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Provider = channelmonitor.Provider(value.String) } + case channelmonitor.FieldAPIMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field api_mode", values[i]) + } else if value.Valid { + _m.APIMode = value.String + } case channelmonitor.FieldEndpoint: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field endpoint", values[i]) @@ -310,6 +318,9 @@ func (_m *ChannelMonitor) String() string { builder.WriteString("provider=") builder.WriteString(fmt.Sprintf("%v", _m.Provider)) builder.WriteString(", ") + builder.WriteString("api_mode=") + builder.WriteString(_m.APIMode) + builder.WriteString(", ") builder.WriteString("endpoint=") builder.WriteString(_m.Endpoint) builder.WriteString(", ") diff --git a/backend/ent/channelmonitor/channelmonitor.go b/backend/ent/channelmonitor/channelmonitor.go index e5a6bfe7..0723ad0d 100644 --- a/backend/ent/channelmonitor/channelmonitor.go +++ b/backend/ent/channelmonitor/channelmonitor.go @@ -23,6 +23,8 @@ const ( FieldName = "name" // FieldProvider holds the string denoting the provider field in the database. FieldProvider = "provider" + // FieldAPIMode holds the string denoting the api_mode field in the database. + FieldAPIMode = "api_mode" // FieldEndpoint holds the string denoting the endpoint field in the database. FieldEndpoint = "endpoint" // FieldAPIKeyEncrypted holds the string denoting the api_key_encrypted field in the database. @@ -87,6 +89,7 @@ var Columns = []string{ FieldUpdatedAt, FieldName, FieldProvider, + FieldAPIMode, FieldEndpoint, FieldAPIKeyEncrypted, FieldPrimaryModel, @@ -121,6 +124,10 @@ var ( UpdateDefaultUpdatedAt func() time.Time // NameValidator is a validator for the "name" field. It is called by the builders before save. NameValidator func(string) error + // DefaultAPIMode holds the default value on creation for the "api_mode" field. + DefaultAPIMode string + // APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save. + APIModeValidator func(string) error // EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save. EndpointValidator func(string) error // APIKeyEncryptedValidator is a validator for the "api_key_encrypted" field. It is called by the builders before save. @@ -197,6 +204,11 @@ func ByProvider(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldProvider, opts...).ToFunc() } +// ByAPIMode orders the results by the api_mode field. +func ByAPIMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAPIMode, opts...).ToFunc() +} + // ByEndpoint orders the results by the endpoint field. func ByEndpoint(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldEndpoint, opts...).ToFunc() diff --git a/backend/ent/channelmonitor/where.go b/backend/ent/channelmonitor/where.go index 755d83a3..8bd8f627 100644 --- a/backend/ent/channelmonitor/where.go +++ b/backend/ent/channelmonitor/where.go @@ -70,6 +70,11 @@ func Name(v string) predicate.ChannelMonitor { return predicate.ChannelMonitor(sql.FieldEQ(FieldName, v)) } +// APIMode applies equality check predicate on the "api_mode" field. It's identical to APIModeEQ. +func APIMode(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v)) +} + // Endpoint applies equality check predicate on the "endpoint" field. It's identical to EndpointEQ. func Endpoint(v string) predicate.ChannelMonitor { return predicate.ChannelMonitor(sql.FieldEQ(FieldEndpoint, v)) @@ -285,6 +290,71 @@ func ProviderNotIn(vs ...Provider) predicate.ChannelMonitor { return predicate.ChannelMonitor(sql.FieldNotIn(FieldProvider, vs...)) } +// APIModeEQ applies the EQ predicate on the "api_mode" field. +func APIModeEQ(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldEQ(FieldAPIMode, v)) +} + +// APIModeNEQ applies the NEQ predicate on the "api_mode" field. +func APIModeNEQ(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldNEQ(FieldAPIMode, v)) +} + +// APIModeIn applies the In predicate on the "api_mode" field. +func APIModeIn(vs ...string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldIn(FieldAPIMode, vs...)) +} + +// APIModeNotIn applies the NotIn predicate on the "api_mode" field. +func APIModeNotIn(vs ...string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldNotIn(FieldAPIMode, vs...)) +} + +// APIModeGT applies the GT predicate on the "api_mode" field. +func APIModeGT(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldGT(FieldAPIMode, v)) +} + +// APIModeGTE applies the GTE predicate on the "api_mode" field. +func APIModeGTE(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldGTE(FieldAPIMode, v)) +} + +// APIModeLT applies the LT predicate on the "api_mode" field. +func APIModeLT(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldLT(FieldAPIMode, v)) +} + +// APIModeLTE applies the LTE predicate on the "api_mode" field. +func APIModeLTE(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldLTE(FieldAPIMode, v)) +} + +// APIModeContains applies the Contains predicate on the "api_mode" field. +func APIModeContains(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldContains(FieldAPIMode, v)) +} + +// APIModeHasPrefix applies the HasPrefix predicate on the "api_mode" field. +func APIModeHasPrefix(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldHasPrefix(FieldAPIMode, v)) +} + +// APIModeHasSuffix applies the HasSuffix predicate on the "api_mode" field. +func APIModeHasSuffix(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldHasSuffix(FieldAPIMode, v)) +} + +// APIModeEqualFold applies the EqualFold predicate on the "api_mode" field. +func APIModeEqualFold(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldEqualFold(FieldAPIMode, v)) +} + +// APIModeContainsFold applies the ContainsFold predicate on the "api_mode" field. +func APIModeContainsFold(v string) predicate.ChannelMonitor { + return predicate.ChannelMonitor(sql.FieldContainsFold(FieldAPIMode, v)) +} + // EndpointEQ applies the EQ predicate on the "endpoint" field. func EndpointEQ(v string) predicate.ChannelMonitor { return predicate.ChannelMonitor(sql.FieldEQ(FieldEndpoint, v)) diff --git a/backend/ent/channelmonitor_create.go b/backend/ent/channelmonitor_create.go index 2f70c300..2593893f 100644 --- a/backend/ent/channelmonitor_create.go +++ b/backend/ent/channelmonitor_create.go @@ -65,6 +65,20 @@ func (_c *ChannelMonitorCreate) SetProvider(v channelmonitor.Provider) *ChannelM return _c } +// SetAPIMode sets the "api_mode" field. +func (_c *ChannelMonitorCreate) SetAPIMode(v string) *ChannelMonitorCreate { + _c.mutation.SetAPIMode(v) + return _c +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_c *ChannelMonitorCreate) SetNillableAPIMode(v *string) *ChannelMonitorCreate { + if v != nil { + _c.SetAPIMode(*v) + } + return _c +} + // SetEndpoint sets the "endpoint" field. func (_c *ChannelMonitorCreate) SetEndpoint(v string) *ChannelMonitorCreate { _c.mutation.SetEndpoint(v) @@ -275,6 +289,10 @@ func (_c *ChannelMonitorCreate) defaults() { v := channelmonitor.DefaultUpdatedAt() _c.mutation.SetUpdatedAt(v) } + if _, ok := _c.mutation.APIMode(); !ok { + v := channelmonitor.DefaultAPIMode + _c.mutation.SetAPIMode(v) + } if _, ok := _c.mutation.ExtraModels(); !ok { v := channelmonitor.DefaultExtraModels _c.mutation.SetExtraModels(v) @@ -321,6 +339,14 @@ func (_c *ChannelMonitorCreate) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)} } } + if _, ok := _c.mutation.APIMode(); !ok { + return &ValidationError{Name: "api_mode", err: errors.New(`ent: missing required field "ChannelMonitor.api_mode"`)} + } + if v, ok := _c.mutation.APIMode(); ok { + if err := channelmonitor.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)} + } + } if _, ok := _c.mutation.Endpoint(); !ok { return &ValidationError{Name: "endpoint", err: errors.New(`ent: missing required field "ChannelMonitor.endpoint"`)} } @@ -421,6 +447,10 @@ func (_c *ChannelMonitorCreate) createSpec() (*ChannelMonitor, *sqlgraph.CreateS _spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value) _node.Provider = value } + if value, ok := _c.mutation.APIMode(); ok { + _spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value) + _node.APIMode = value + } if value, ok := _c.mutation.Endpoint(); ok { _spec.SetField(channelmonitor.FieldEndpoint, field.TypeString, value) _node.Endpoint = value @@ -606,6 +636,18 @@ func (u *ChannelMonitorUpsert) UpdateProvider() *ChannelMonitorUpsert { return u } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorUpsert) SetAPIMode(v string) *ChannelMonitorUpsert { + u.Set(channelmonitor.FieldAPIMode, v) + return u +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorUpsert) UpdateAPIMode() *ChannelMonitorUpsert { + u.SetExcluded(channelmonitor.FieldAPIMode) + return u +} + // SetEndpoint sets the "endpoint" field. func (u *ChannelMonitorUpsert) SetEndpoint(v string) *ChannelMonitorUpsert { u.Set(channelmonitor.FieldEndpoint, v) @@ -885,6 +927,20 @@ func (u *ChannelMonitorUpsertOne) UpdateProvider() *ChannelMonitorUpsertOne { }) } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorUpsertOne) SetAPIMode(v string) *ChannelMonitorUpsertOne { + return u.Update(func(s *ChannelMonitorUpsert) { + s.SetAPIMode(v) + }) +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorUpsertOne) UpdateAPIMode() *ChannelMonitorUpsertOne { + return u.Update(func(s *ChannelMonitorUpsert) { + s.UpdateAPIMode() + }) +} + // SetEndpoint sets the "endpoint" field. func (u *ChannelMonitorUpsertOne) SetEndpoint(v string) *ChannelMonitorUpsertOne { return u.Update(func(s *ChannelMonitorUpsert) { @@ -1362,6 +1418,20 @@ func (u *ChannelMonitorUpsertBulk) UpdateProvider() *ChannelMonitorUpsertBulk { }) } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorUpsertBulk) SetAPIMode(v string) *ChannelMonitorUpsertBulk { + return u.Update(func(s *ChannelMonitorUpsert) { + s.SetAPIMode(v) + }) +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorUpsertBulk) UpdateAPIMode() *ChannelMonitorUpsertBulk { + return u.Update(func(s *ChannelMonitorUpsert) { + s.UpdateAPIMode() + }) +} + // SetEndpoint sets the "endpoint" field. func (u *ChannelMonitorUpsertBulk) SetEndpoint(v string) *ChannelMonitorUpsertBulk { return u.Update(func(s *ChannelMonitorUpsert) { diff --git a/backend/ent/channelmonitor_update.go b/backend/ent/channelmonitor_update.go index 4bbcd564..2cd5e656 100644 --- a/backend/ent/channelmonitor_update.go +++ b/backend/ent/channelmonitor_update.go @@ -66,6 +66,20 @@ func (_u *ChannelMonitorUpdate) SetNillableProvider(v *channelmonitor.Provider) return _u } +// SetAPIMode sets the "api_mode" field. +func (_u *ChannelMonitorUpdate) SetAPIMode(v string) *ChannelMonitorUpdate { + _u.mutation.SetAPIMode(v) + return _u +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_u *ChannelMonitorUpdate) SetNillableAPIMode(v *string) *ChannelMonitorUpdate { + if v != nil { + _u.SetAPIMode(*v) + } + return _u +} + // SetEndpoint sets the "endpoint" field. func (_u *ChannelMonitorUpdate) SetEndpoint(v string) *ChannelMonitorUpdate { _u.mutation.SetEndpoint(v) @@ -418,6 +432,11 @@ func (_u *ChannelMonitorUpdate) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)} } } + if v, ok := _u.mutation.APIMode(); ok { + if err := channelmonitor.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)} + } + } if v, ok := _u.mutation.Endpoint(); ok { if err := channelmonitor.EndpointValidator(v); err != nil { return &ValidationError{Name: "endpoint", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.endpoint": %w`, err)} @@ -472,6 +491,9 @@ func (_u *ChannelMonitorUpdate) sqlSave(ctx context.Context) (_node int, err err if value, ok := _u.mutation.Provider(); ok { _spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value) } + if value, ok := _u.mutation.APIMode(); ok { + _spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value) + } if value, ok := _u.mutation.Endpoint(); ok { _spec.SetField(channelmonitor.FieldEndpoint, field.TypeString, value) } @@ -701,6 +723,20 @@ func (_u *ChannelMonitorUpdateOne) SetNillableProvider(v *channelmonitor.Provide return _u } +// SetAPIMode sets the "api_mode" field. +func (_u *ChannelMonitorUpdateOne) SetAPIMode(v string) *ChannelMonitorUpdateOne { + _u.mutation.SetAPIMode(v) + return _u +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_u *ChannelMonitorUpdateOne) SetNillableAPIMode(v *string) *ChannelMonitorUpdateOne { + if v != nil { + _u.SetAPIMode(*v) + } + return _u +} + // SetEndpoint sets the "endpoint" field. func (_u *ChannelMonitorUpdateOne) SetEndpoint(v string) *ChannelMonitorUpdateOne { _u.mutation.SetEndpoint(v) @@ -1066,6 +1102,11 @@ func (_u *ChannelMonitorUpdateOne) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.provider": %w`, err)} } } + if v, ok := _u.mutation.APIMode(); ok { + if err := channelmonitor.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.api_mode": %w`, err)} + } + } if v, ok := _u.mutation.Endpoint(); ok { if err := channelmonitor.EndpointValidator(v); err != nil { return &ValidationError{Name: "endpoint", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitor.endpoint": %w`, err)} @@ -1137,6 +1178,9 @@ func (_u *ChannelMonitorUpdateOne) sqlSave(ctx context.Context) (_node *ChannelM if value, ok := _u.mutation.Provider(); ok { _spec.SetField(channelmonitor.FieldProvider, field.TypeEnum, value) } + if value, ok := _u.mutation.APIMode(); ok { + _spec.SetField(channelmonitor.FieldAPIMode, field.TypeString, value) + } if value, ok := _u.mutation.Endpoint(); ok { _spec.SetField(channelmonitor.FieldEndpoint, field.TypeString, value) } diff --git a/backend/ent/channelmonitorrequesttemplate.go b/backend/ent/channelmonitorrequesttemplate.go index b8429a4d..7f417efb 100644 --- a/backend/ent/channelmonitorrequesttemplate.go +++ b/backend/ent/channelmonitorrequesttemplate.go @@ -26,6 +26,8 @@ type ChannelMonitorRequestTemplate struct { Name string `json:"name,omitempty"` // Provider holds the value of the "provider" field. Provider channelmonitorrequesttemplate.Provider `json:"provider,omitempty"` + // OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions + APIMode string `json:"api_mode,omitempty"` // Description holds the value of the "description" field. Description string `json:"description,omitempty"` // ExtraHeaders holds the value of the "extra_headers" field. @@ -67,7 +69,7 @@ func (*ChannelMonitorRequestTemplate) scanValues(columns []string) ([]any, error values[i] = new([]byte) case channelmonitorrequesttemplate.FieldID: values[i] = new(sql.NullInt64) - case channelmonitorrequesttemplate.FieldName, channelmonitorrequesttemplate.FieldProvider, channelmonitorrequesttemplate.FieldDescription, channelmonitorrequesttemplate.FieldBodyOverrideMode: + case channelmonitorrequesttemplate.FieldName, channelmonitorrequesttemplate.FieldProvider, channelmonitorrequesttemplate.FieldAPIMode, channelmonitorrequesttemplate.FieldDescription, channelmonitorrequesttemplate.FieldBodyOverrideMode: values[i] = new(sql.NullString) case channelmonitorrequesttemplate.FieldCreatedAt, channelmonitorrequesttemplate.FieldUpdatedAt: values[i] = new(sql.NullTime) @@ -116,6 +118,12 @@ func (_m *ChannelMonitorRequestTemplate) assignValues(columns []string, values [ } else if value.Valid { _m.Provider = channelmonitorrequesttemplate.Provider(value.String) } + case channelmonitorrequesttemplate.FieldAPIMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field api_mode", values[i]) + } else if value.Valid { + _m.APIMode = value.String + } case channelmonitorrequesttemplate.FieldDescription: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field description", values[i]) @@ -197,6 +205,9 @@ func (_m *ChannelMonitorRequestTemplate) String() string { builder.WriteString("provider=") builder.WriteString(fmt.Sprintf("%v", _m.Provider)) builder.WriteString(", ") + builder.WriteString("api_mode=") + builder.WriteString(_m.APIMode) + builder.WriteString(", ") builder.WriteString("description=") builder.WriteString(_m.Description) builder.WriteString(", ") diff --git a/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go b/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go index 65b8d641..db04aee1 100644 --- a/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go +++ b/backend/ent/channelmonitorrequesttemplate/channelmonitorrequesttemplate.go @@ -23,6 +23,8 @@ const ( FieldName = "name" // FieldProvider holds the string denoting the provider field in the database. FieldProvider = "provider" + // FieldAPIMode holds the string denoting the api_mode field in the database. + FieldAPIMode = "api_mode" // FieldDescription holds the string denoting the description field in the database. FieldDescription = "description" // FieldExtraHeaders holds the string denoting the extra_headers field in the database. @@ -51,6 +53,7 @@ var Columns = []string{ FieldUpdatedAt, FieldName, FieldProvider, + FieldAPIMode, FieldDescription, FieldExtraHeaders, FieldBodyOverrideMode, @@ -76,6 +79,10 @@ var ( UpdateDefaultUpdatedAt func() time.Time // NameValidator is a validator for the "name" field. It is called by the builders before save. NameValidator func(string) error + // DefaultAPIMode holds the default value on creation for the "api_mode" field. + DefaultAPIMode string + // APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save. + APIModeValidator func(string) error // DefaultDescription holds the default value on creation for the "description" field. DefaultDescription string // DescriptionValidator is a validator for the "description" field. It is called by the builders before save. @@ -140,6 +147,11 @@ func ByProvider(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldProvider, opts...).ToFunc() } +// ByAPIMode orders the results by the api_mode field. +func ByAPIMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAPIMode, opts...).ToFunc() +} + // ByDescription orders the results by the description field. func ByDescription(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDescription, opts...).ToFunc() diff --git a/backend/ent/channelmonitorrequesttemplate/where.go b/backend/ent/channelmonitorrequesttemplate/where.go index b95e5df0..9f6d7333 100644 --- a/backend/ent/channelmonitorrequesttemplate/where.go +++ b/backend/ent/channelmonitorrequesttemplate/where.go @@ -70,6 +70,11 @@ func Name(v string) predicate.ChannelMonitorRequestTemplate { return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldName, v)) } +// APIMode applies equality check predicate on the "api_mode" field. It's identical to APIModeEQ. +func APIMode(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldAPIMode, v)) +} + // Description applies equality check predicate on the "description" field. It's identical to DescriptionEQ. func Description(v string) predicate.ChannelMonitorRequestTemplate { return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldDescription, v)) @@ -245,6 +250,71 @@ func ProviderNotIn(vs ...Provider) predicate.ChannelMonitorRequestTemplate { return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldProvider, vs...)) } +// APIModeEQ applies the EQ predicate on the "api_mode" field. +func APIModeEQ(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldAPIMode, v)) +} + +// APIModeNEQ applies the NEQ predicate on the "api_mode" field. +func APIModeNEQ(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldNEQ(FieldAPIMode, v)) +} + +// APIModeIn applies the In predicate on the "api_mode" field. +func APIModeIn(vs ...string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldIn(FieldAPIMode, vs...)) +} + +// APIModeNotIn applies the NotIn predicate on the "api_mode" field. +func APIModeNotIn(vs ...string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldNotIn(FieldAPIMode, vs...)) +} + +// APIModeGT applies the GT predicate on the "api_mode" field. +func APIModeGT(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldGT(FieldAPIMode, v)) +} + +// APIModeGTE applies the GTE predicate on the "api_mode" field. +func APIModeGTE(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldGTE(FieldAPIMode, v)) +} + +// APIModeLT applies the LT predicate on the "api_mode" field. +func APIModeLT(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldLT(FieldAPIMode, v)) +} + +// APIModeLTE applies the LTE predicate on the "api_mode" field. +func APIModeLTE(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldLTE(FieldAPIMode, v)) +} + +// APIModeContains applies the Contains predicate on the "api_mode" field. +func APIModeContains(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldContains(FieldAPIMode, v)) +} + +// APIModeHasPrefix applies the HasPrefix predicate on the "api_mode" field. +func APIModeHasPrefix(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldHasPrefix(FieldAPIMode, v)) +} + +// APIModeHasSuffix applies the HasSuffix predicate on the "api_mode" field. +func APIModeHasSuffix(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldHasSuffix(FieldAPIMode, v)) +} + +// APIModeEqualFold applies the EqualFold predicate on the "api_mode" field. +func APIModeEqualFold(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldEqualFold(FieldAPIMode, v)) +} + +// APIModeContainsFold applies the ContainsFold predicate on the "api_mode" field. +func APIModeContainsFold(v string) predicate.ChannelMonitorRequestTemplate { + return predicate.ChannelMonitorRequestTemplate(sql.FieldContainsFold(FieldAPIMode, v)) +} + // DescriptionEQ applies the EQ predicate on the "description" field. func DescriptionEQ(v string) predicate.ChannelMonitorRequestTemplate { return predicate.ChannelMonitorRequestTemplate(sql.FieldEQ(FieldDescription, v)) diff --git a/backend/ent/channelmonitorrequesttemplate_create.go b/backend/ent/channelmonitorrequesttemplate_create.go index 1ba842cd..45405270 100644 --- a/backend/ent/channelmonitorrequesttemplate_create.go +++ b/backend/ent/channelmonitorrequesttemplate_create.go @@ -63,6 +63,20 @@ func (_c *ChannelMonitorRequestTemplateCreate) SetProvider(v channelmonitorreque return _c } +// SetAPIMode sets the "api_mode" field. +func (_c *ChannelMonitorRequestTemplateCreate) SetAPIMode(v string) *ChannelMonitorRequestTemplateCreate { + _c.mutation.SetAPIMode(v) + return _c +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_c *ChannelMonitorRequestTemplateCreate) SetNillableAPIMode(v *string) *ChannelMonitorRequestTemplateCreate { + if v != nil { + _c.SetAPIMode(*v) + } + return _c +} + // SetDescription sets the "description" field. func (_c *ChannelMonitorRequestTemplateCreate) SetDescription(v string) *ChannelMonitorRequestTemplateCreate { _c.mutation.SetDescription(v) @@ -161,6 +175,10 @@ func (_c *ChannelMonitorRequestTemplateCreate) defaults() { v := channelmonitorrequesttemplate.DefaultUpdatedAt() _c.mutation.SetUpdatedAt(v) } + if _, ok := _c.mutation.APIMode(); !ok { + v := channelmonitorrequesttemplate.DefaultAPIMode + _c.mutation.SetAPIMode(v) + } if _, ok := _c.mutation.Description(); !ok { v := channelmonitorrequesttemplate.DefaultDescription _c.mutation.SetDescription(v) @@ -199,6 +217,14 @@ func (_c *ChannelMonitorRequestTemplateCreate) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)} } } + if _, ok := _c.mutation.APIMode(); !ok { + return &ValidationError{Name: "api_mode", err: errors.New(`ent: missing required field "ChannelMonitorRequestTemplate.api_mode"`)} + } + if v, ok := _c.mutation.APIMode(); ok { + if err := channelmonitorrequesttemplate.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.api_mode": %w`, err)} + } + } if v, ok := _c.mutation.Description(); ok { if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil { return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)} @@ -258,6 +284,10 @@ func (_c *ChannelMonitorRequestTemplateCreate) createSpec() (*ChannelMonitorRequ _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value) _node.Provider = value } + if value, ok := _c.mutation.APIMode(); ok { + _spec.SetField(channelmonitorrequesttemplate.FieldAPIMode, field.TypeString, value) + _node.APIMode = value + } if value, ok := _c.mutation.Description(); ok { _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value) _node.Description = value @@ -378,6 +408,18 @@ func (u *ChannelMonitorRequestTemplateUpsert) UpdateProvider() *ChannelMonitorRe return u } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorRequestTemplateUpsert) SetAPIMode(v string) *ChannelMonitorRequestTemplateUpsert { + u.Set(channelmonitorrequesttemplate.FieldAPIMode, v) + return u +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorRequestTemplateUpsert) UpdateAPIMode() *ChannelMonitorRequestTemplateUpsert { + u.SetExcluded(channelmonitorrequesttemplate.FieldAPIMode) + return u +} + // SetDescription sets the "description" field. func (u *ChannelMonitorRequestTemplateUpsert) SetDescription(v string) *ChannelMonitorRequestTemplateUpsert { u.Set(channelmonitorrequesttemplate.FieldDescription, v) @@ -525,6 +567,20 @@ func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateProvider() *ChannelMonito }) } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorRequestTemplateUpsertOne) SetAPIMode(v string) *ChannelMonitorRequestTemplateUpsertOne { + return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { + s.SetAPIMode(v) + }) +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorRequestTemplateUpsertOne) UpdateAPIMode() *ChannelMonitorRequestTemplateUpsertOne { + return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { + s.UpdateAPIMode() + }) +} + // SetDescription sets the "description" field. func (u *ChannelMonitorRequestTemplateUpsertOne) SetDescription(v string) *ChannelMonitorRequestTemplateUpsertOne { return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { @@ -848,6 +904,20 @@ func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateProvider() *ChannelMonit }) } +// SetAPIMode sets the "api_mode" field. +func (u *ChannelMonitorRequestTemplateUpsertBulk) SetAPIMode(v string) *ChannelMonitorRequestTemplateUpsertBulk { + return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { + s.SetAPIMode(v) + }) +} + +// UpdateAPIMode sets the "api_mode" field to the value that was provided on create. +func (u *ChannelMonitorRequestTemplateUpsertBulk) UpdateAPIMode() *ChannelMonitorRequestTemplateUpsertBulk { + return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { + s.UpdateAPIMode() + }) +} + // SetDescription sets the "description" field. func (u *ChannelMonitorRequestTemplateUpsertBulk) SetDescription(v string) *ChannelMonitorRequestTemplateUpsertBulk { return u.Update(func(s *ChannelMonitorRequestTemplateUpsert) { diff --git a/backend/ent/channelmonitorrequesttemplate_update.go b/backend/ent/channelmonitorrequesttemplate_update.go index 8f55ba04..f27cac1c 100644 --- a/backend/ent/channelmonitorrequesttemplate_update.go +++ b/backend/ent/channelmonitorrequesttemplate_update.go @@ -63,6 +63,20 @@ func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableProvider(v *channelmon return _u } +// SetAPIMode sets the "api_mode" field. +func (_u *ChannelMonitorRequestTemplateUpdate) SetAPIMode(v string) *ChannelMonitorRequestTemplateUpdate { + _u.mutation.SetAPIMode(v) + return _u +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_u *ChannelMonitorRequestTemplateUpdate) SetNillableAPIMode(v *string) *ChannelMonitorRequestTemplateUpdate { + if v != nil { + _u.SetAPIMode(*v) + } + return _u +} + // SetDescription sets the "description" field. func (_u *ChannelMonitorRequestTemplateUpdate) SetDescription(v string) *ChannelMonitorRequestTemplateUpdate { _u.mutation.SetDescription(v) @@ -204,6 +218,11 @@ func (_u *ChannelMonitorRequestTemplateUpdate) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)} } } + if v, ok := _u.mutation.APIMode(); ok { + if err := channelmonitorrequesttemplate.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.api_mode": %w`, err)} + } + } if v, ok := _u.mutation.Description(); ok { if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil { return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)} @@ -238,6 +257,9 @@ func (_u *ChannelMonitorRequestTemplateUpdate) sqlSave(ctx context.Context) (_no if value, ok := _u.mutation.Provider(); ok { _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value) } + if value, ok := _u.mutation.APIMode(); ok { + _spec.SetField(channelmonitorrequesttemplate.FieldAPIMode, field.TypeString, value) + } if value, ok := _u.mutation.Description(); ok { _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value) } @@ -355,6 +377,20 @@ func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableProvider(v *channel return _u } +// SetAPIMode sets the "api_mode" field. +func (_u *ChannelMonitorRequestTemplateUpdateOne) SetAPIMode(v string) *ChannelMonitorRequestTemplateUpdateOne { + _u.mutation.SetAPIMode(v) + return _u +} + +// SetNillableAPIMode sets the "api_mode" field if the given value is not nil. +func (_u *ChannelMonitorRequestTemplateUpdateOne) SetNillableAPIMode(v *string) *ChannelMonitorRequestTemplateUpdateOne { + if v != nil { + _u.SetAPIMode(*v) + } + return _u +} + // SetDescription sets the "description" field. func (_u *ChannelMonitorRequestTemplateUpdateOne) SetDescription(v string) *ChannelMonitorRequestTemplateUpdateOne { _u.mutation.SetDescription(v) @@ -509,6 +545,11 @@ func (_u *ChannelMonitorRequestTemplateUpdateOne) check() error { return &ValidationError{Name: "provider", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.provider": %w`, err)} } } + if v, ok := _u.mutation.APIMode(); ok { + if err := channelmonitorrequesttemplate.APIModeValidator(v); err != nil { + return &ValidationError{Name: "api_mode", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.api_mode": %w`, err)} + } + } if v, ok := _u.mutation.Description(); ok { if err := channelmonitorrequesttemplate.DescriptionValidator(v); err != nil { return &ValidationError{Name: "description", err: fmt.Errorf(`ent: validator failed for field "ChannelMonitorRequestTemplate.description": %w`, err)} @@ -560,6 +601,9 @@ func (_u *ChannelMonitorRequestTemplateUpdateOne) sqlSave(ctx context.Context) ( if value, ok := _u.mutation.Provider(); ok { _spec.SetField(channelmonitorrequesttemplate.FieldProvider, field.TypeEnum, value) } + if value, ok := _u.mutation.APIMode(); ok { + _spec.SetField(channelmonitorrequesttemplate.FieldAPIMode, field.TypeString, value) + } if value, ok := _u.mutation.Description(); ok { _spec.SetField(channelmonitorrequesttemplate.FieldDescription, field.TypeString, value) } diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index f98761ea..b1731a35 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -428,6 +428,7 @@ var ( {Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, {Name: "name", Type: field.TypeString, Size: 100}, {Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini"}}, + {Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"}, {Name: "endpoint", Type: field.TypeString, Size: 500}, {Name: "api_key_encrypted", Type: field.TypeString}, {Name: "primary_model", Type: field.TypeString, Size: 200}, @@ -450,7 +451,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "channel_monitors_channel_monitor_request_templates_request_template", - Columns: []*schema.Column{ChannelMonitorsColumns[17]}, + Columns: []*schema.Column{ChannelMonitorsColumns[18]}, RefColumns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[0]}, OnDelete: schema.SetNull, }, @@ -459,22 +460,27 @@ var ( { Name: "channelmonitor_enabled_last_checked_at", Unique: false, - Columns: []*schema.Column{ChannelMonitorsColumns[10], ChannelMonitorsColumns[12]}, + Columns: []*schema.Column{ChannelMonitorsColumns[11], ChannelMonitorsColumns[13]}, }, { Name: "channelmonitor_provider", Unique: false, Columns: []*schema.Column{ChannelMonitorsColumns[4]}, }, + { + Name: "channelmonitor_provider_api_mode", + Unique: false, + Columns: []*schema.Column{ChannelMonitorsColumns[4], ChannelMonitorsColumns[5]}, + }, { Name: "channelmonitor_group_name", Unique: false, - Columns: []*schema.Column{ChannelMonitorsColumns[9]}, + Columns: []*schema.Column{ChannelMonitorsColumns[10]}, }, { Name: "channelmonitor_template_id", Unique: false, - Columns: []*schema.Column{ChannelMonitorsColumns[17]}, + Columns: []*schema.Column{ChannelMonitorsColumns[18]}, }, }, } @@ -566,6 +572,7 @@ var ( {Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, {Name: "name", Type: field.TypeString, Size: 100}, {Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini"}}, + {Name: "api_mode", Type: field.TypeString, Size: 32, Default: "chat_completions"}, {Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""}, {Name: "extra_headers", Type: field.TypeJSON}, {Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"}, @@ -582,6 +589,11 @@ var ( Unique: true, Columns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[4], ChannelMonitorRequestTemplatesColumns[3]}, }, + { + Name: "channelmonitorrequesttemplate_provider_api_mode", + Unique: false, + Columns: []*schema.Column{ChannelMonitorRequestTemplatesColumns[4], ChannelMonitorRequestTemplatesColumns[5]}, + }, }, } // ErrorPassthroughRulesColumns holds the columns for the "error_passthrough_rules" table. diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 45c56314..af0edc68 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -8752,6 +8752,7 @@ type ChannelMonitorMutation struct { updated_at *time.Time name *string provider *channelmonitor.Provider + api_mode *string endpoint *string api_key_encrypted *string primary_model *string @@ -9023,6 +9024,42 @@ func (m *ChannelMonitorMutation) ResetProvider() { m.provider = nil } +// SetAPIMode sets the "api_mode" field. +func (m *ChannelMonitorMutation) SetAPIMode(s string) { + m.api_mode = &s +} + +// APIMode returns the value of the "api_mode" field in the mutation. +func (m *ChannelMonitorMutation) APIMode() (r string, exists bool) { + v := m.api_mode + if v == nil { + return + } + return *v, true +} + +// OldAPIMode returns the old "api_mode" field's value of the ChannelMonitor entity. +// If the ChannelMonitor 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 *ChannelMonitorMutation) OldAPIMode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAPIMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAPIMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAPIMode: %w", err) + } + return oldValue.APIMode, nil +} + +// ResetAPIMode resets all changes to the "api_mode" field. +func (m *ChannelMonitorMutation) ResetAPIMode() { + m.api_mode = nil +} + // SetEndpoint sets the "endpoint" field. func (m *ChannelMonitorMutation) SetEndpoint(s string) { m.endpoint = &s @@ -9780,7 +9817,7 @@ func (m *ChannelMonitorMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ChannelMonitorMutation) Fields() []string { - fields := make([]string, 0, 17) + fields := make([]string, 0, 18) if m.created_at != nil { fields = append(fields, channelmonitor.FieldCreatedAt) } @@ -9793,6 +9830,9 @@ func (m *ChannelMonitorMutation) Fields() []string { if m.provider != nil { fields = append(fields, channelmonitor.FieldProvider) } + if m.api_mode != nil { + fields = append(fields, channelmonitor.FieldAPIMode) + } if m.endpoint != nil { fields = append(fields, channelmonitor.FieldEndpoint) } @@ -9848,6 +9888,8 @@ func (m *ChannelMonitorMutation) Field(name string) (ent.Value, bool) { return m.Name() case channelmonitor.FieldProvider: return m.Provider() + case channelmonitor.FieldAPIMode: + return m.APIMode() case channelmonitor.FieldEndpoint: return m.Endpoint() case channelmonitor.FieldAPIKeyEncrypted: @@ -9891,6 +9933,8 @@ func (m *ChannelMonitorMutation) OldField(ctx context.Context, name string) (ent return m.OldName(ctx) case channelmonitor.FieldProvider: return m.OldProvider(ctx) + case channelmonitor.FieldAPIMode: + return m.OldAPIMode(ctx) case channelmonitor.FieldEndpoint: return m.OldEndpoint(ctx) case channelmonitor.FieldAPIKeyEncrypted: @@ -9954,6 +9998,13 @@ func (m *ChannelMonitorMutation) SetField(name string, value ent.Value) error { } m.SetProvider(v) return nil + case channelmonitor.FieldAPIMode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAPIMode(v) + return nil case channelmonitor.FieldEndpoint: v, ok := value.(string) if !ok { @@ -10160,6 +10211,9 @@ func (m *ChannelMonitorMutation) ResetField(name string) error { case channelmonitor.FieldProvider: m.ResetProvider() return nil + case channelmonitor.FieldAPIMode: + m.ResetAPIMode() + return nil case channelmonitor.FieldEndpoint: m.ResetEndpoint() return nil @@ -12591,6 +12645,7 @@ type ChannelMonitorRequestTemplateMutation struct { updated_at *time.Time name *string provider *channelmonitorrequesttemplate.Provider + api_mode *string description *string extra_headers *map[string]string body_override_mode *string @@ -12846,6 +12901,42 @@ func (m *ChannelMonitorRequestTemplateMutation) ResetProvider() { m.provider = nil } +// SetAPIMode sets the "api_mode" field. +func (m *ChannelMonitorRequestTemplateMutation) SetAPIMode(s string) { + m.api_mode = &s +} + +// APIMode returns the value of the "api_mode" field in the mutation. +func (m *ChannelMonitorRequestTemplateMutation) APIMode() (r string, exists bool) { + v := m.api_mode + if v == nil { + return + } + return *v, true +} + +// OldAPIMode returns the old "api_mode" field's value of the ChannelMonitorRequestTemplate entity. +// If the ChannelMonitorRequestTemplate 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 *ChannelMonitorRequestTemplateMutation) OldAPIMode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAPIMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAPIMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAPIMode: %w", err) + } + return oldValue.APIMode, nil +} + +// ResetAPIMode resets all changes to the "api_mode" field. +func (m *ChannelMonitorRequestTemplateMutation) ResetAPIMode() { + m.api_mode = nil +} + // SetDescription sets the "description" field. func (m *ChannelMonitorRequestTemplateMutation) SetDescription(s string) { m.description = &s @@ -13104,7 +13195,7 @@ func (m *ChannelMonitorRequestTemplateMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ChannelMonitorRequestTemplateMutation) Fields() []string { - fields := make([]string, 0, 8) + fields := make([]string, 0, 9) if m.created_at != nil { fields = append(fields, channelmonitorrequesttemplate.FieldCreatedAt) } @@ -13117,6 +13208,9 @@ func (m *ChannelMonitorRequestTemplateMutation) Fields() []string { if m.provider != nil { fields = append(fields, channelmonitorrequesttemplate.FieldProvider) } + if m.api_mode != nil { + fields = append(fields, channelmonitorrequesttemplate.FieldAPIMode) + } if m.description != nil { fields = append(fields, channelmonitorrequesttemplate.FieldDescription) } @@ -13145,6 +13239,8 @@ func (m *ChannelMonitorRequestTemplateMutation) Field(name string) (ent.Value, b return m.Name() case channelmonitorrequesttemplate.FieldProvider: return m.Provider() + case channelmonitorrequesttemplate.FieldAPIMode: + return m.APIMode() case channelmonitorrequesttemplate.FieldDescription: return m.Description() case channelmonitorrequesttemplate.FieldExtraHeaders: @@ -13170,6 +13266,8 @@ func (m *ChannelMonitorRequestTemplateMutation) OldField(ctx context.Context, na return m.OldName(ctx) case channelmonitorrequesttemplate.FieldProvider: return m.OldProvider(ctx) + case channelmonitorrequesttemplate.FieldAPIMode: + return m.OldAPIMode(ctx) case channelmonitorrequesttemplate.FieldDescription: return m.OldDescription(ctx) case channelmonitorrequesttemplate.FieldExtraHeaders: @@ -13215,6 +13313,13 @@ func (m *ChannelMonitorRequestTemplateMutation) SetField(name string, value ent. } m.SetProvider(v) return nil + case channelmonitorrequesttemplate.FieldAPIMode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAPIMode(v) + return nil case channelmonitorrequesttemplate.FieldDescription: v, ok := value.(string) if !ok { @@ -13319,6 +13424,9 @@ func (m *ChannelMonitorRequestTemplateMutation) ResetField(name string) error { case channelmonitorrequesttemplate.FieldProvider: m.ResetProvider() return nil + case channelmonitorrequesttemplate.FieldAPIMode: + m.ResetAPIMode() + return nil case channelmonitorrequesttemplate.FieldDescription: m.ResetDescription() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index b1899173..6d541e2f 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -464,8 +464,14 @@ func init() { return nil } }() + // channelmonitorDescAPIMode is the schema descriptor for api_mode field. + channelmonitorDescAPIMode := channelmonitorFields[2].Descriptor() + // channelmonitor.DefaultAPIMode holds the default value on creation for the api_mode field. + channelmonitor.DefaultAPIMode = channelmonitorDescAPIMode.Default.(string) + // channelmonitor.APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save. + channelmonitor.APIModeValidator = channelmonitorDescAPIMode.Validators[0].(func(string) error) // channelmonitorDescEndpoint is the schema descriptor for endpoint field. - channelmonitorDescEndpoint := channelmonitorFields[2].Descriptor() + channelmonitorDescEndpoint := channelmonitorFields[3].Descriptor() // channelmonitor.EndpointValidator is a validator for the "endpoint" field. It is called by the builders before save. channelmonitor.EndpointValidator = func() func(string) error { validators := channelmonitorDescEndpoint.Validators @@ -483,11 +489,11 @@ func init() { } }() // channelmonitorDescAPIKeyEncrypted is the schema descriptor for api_key_encrypted field. - channelmonitorDescAPIKeyEncrypted := channelmonitorFields[3].Descriptor() + channelmonitorDescAPIKeyEncrypted := channelmonitorFields[4].Descriptor() // channelmonitor.APIKeyEncryptedValidator is a validator for the "api_key_encrypted" field. It is called by the builders before save. channelmonitor.APIKeyEncryptedValidator = channelmonitorDescAPIKeyEncrypted.Validators[0].(func(string) error) // channelmonitorDescPrimaryModel is the schema descriptor for primary_model field. - channelmonitorDescPrimaryModel := channelmonitorFields[4].Descriptor() + channelmonitorDescPrimaryModel := channelmonitorFields[5].Descriptor() // channelmonitor.PrimaryModelValidator is a validator for the "primary_model" field. It is called by the builders before save. channelmonitor.PrimaryModelValidator = func() func(string) error { validators := channelmonitorDescPrimaryModel.Validators @@ -505,29 +511,29 @@ func init() { } }() // channelmonitorDescExtraModels is the schema descriptor for extra_models field. - channelmonitorDescExtraModels := channelmonitorFields[5].Descriptor() + channelmonitorDescExtraModels := channelmonitorFields[6].Descriptor() // channelmonitor.DefaultExtraModels holds the default value on creation for the extra_models field. channelmonitor.DefaultExtraModels = channelmonitorDescExtraModels.Default.([]string) // channelmonitorDescGroupName is the schema descriptor for group_name field. - channelmonitorDescGroupName := channelmonitorFields[6].Descriptor() + channelmonitorDescGroupName := channelmonitorFields[7].Descriptor() // channelmonitor.DefaultGroupName holds the default value on creation for the group_name field. channelmonitor.DefaultGroupName = channelmonitorDescGroupName.Default.(string) // channelmonitor.GroupNameValidator is a validator for the "group_name" field. It is called by the builders before save. channelmonitor.GroupNameValidator = channelmonitorDescGroupName.Validators[0].(func(string) error) // channelmonitorDescEnabled is the schema descriptor for enabled field. - channelmonitorDescEnabled := channelmonitorFields[7].Descriptor() + channelmonitorDescEnabled := channelmonitorFields[8].Descriptor() // channelmonitor.DefaultEnabled holds the default value on creation for the enabled field. channelmonitor.DefaultEnabled = channelmonitorDescEnabled.Default.(bool) // channelmonitorDescIntervalSeconds is the schema descriptor for interval_seconds field. - channelmonitorDescIntervalSeconds := channelmonitorFields[8].Descriptor() + channelmonitorDescIntervalSeconds := channelmonitorFields[9].Descriptor() // channelmonitor.IntervalSecondsValidator is a validator for the "interval_seconds" field. It is called by the builders before save. channelmonitor.IntervalSecondsValidator = channelmonitorDescIntervalSeconds.Validators[0].(func(int) error) // channelmonitorDescExtraHeaders is the schema descriptor for extra_headers field. - channelmonitorDescExtraHeaders := channelmonitorFields[12].Descriptor() + channelmonitorDescExtraHeaders := channelmonitorFields[13].Descriptor() // channelmonitor.DefaultExtraHeaders holds the default value on creation for the extra_headers field. channelmonitor.DefaultExtraHeaders = channelmonitorDescExtraHeaders.Default.(map[string]string) // channelmonitorDescBodyOverrideMode is the schema descriptor for body_override_mode field. - channelmonitorDescBodyOverrideMode := channelmonitorFields[13].Descriptor() + channelmonitorDescBodyOverrideMode := channelmonitorFields[14].Descriptor() // channelmonitor.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field. channelmonitor.DefaultBodyOverrideMode = channelmonitorDescBodyOverrideMode.Default.(string) // channelmonitor.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save. @@ -661,18 +667,24 @@ func init() { return nil } }() + // channelmonitorrequesttemplateDescAPIMode is the schema descriptor for api_mode field. + channelmonitorrequesttemplateDescAPIMode := channelmonitorrequesttemplateFields[2].Descriptor() + // channelmonitorrequesttemplate.DefaultAPIMode holds the default value on creation for the api_mode field. + channelmonitorrequesttemplate.DefaultAPIMode = channelmonitorrequesttemplateDescAPIMode.Default.(string) + // channelmonitorrequesttemplate.APIModeValidator is a validator for the "api_mode" field. It is called by the builders before save. + channelmonitorrequesttemplate.APIModeValidator = channelmonitorrequesttemplateDescAPIMode.Validators[0].(func(string) error) // channelmonitorrequesttemplateDescDescription is the schema descriptor for description field. - channelmonitorrequesttemplateDescDescription := channelmonitorrequesttemplateFields[2].Descriptor() + channelmonitorrequesttemplateDescDescription := channelmonitorrequesttemplateFields[3].Descriptor() // channelmonitorrequesttemplate.DefaultDescription holds the default value on creation for the description field. channelmonitorrequesttemplate.DefaultDescription = channelmonitorrequesttemplateDescDescription.Default.(string) // channelmonitorrequesttemplate.DescriptionValidator is a validator for the "description" field. It is called by the builders before save. channelmonitorrequesttemplate.DescriptionValidator = channelmonitorrequesttemplateDescDescription.Validators[0].(func(string) error) // channelmonitorrequesttemplateDescExtraHeaders is the schema descriptor for extra_headers field. - channelmonitorrequesttemplateDescExtraHeaders := channelmonitorrequesttemplateFields[3].Descriptor() + channelmonitorrequesttemplateDescExtraHeaders := channelmonitorrequesttemplateFields[4].Descriptor() // channelmonitorrequesttemplate.DefaultExtraHeaders holds the default value on creation for the extra_headers field. channelmonitorrequesttemplate.DefaultExtraHeaders = channelmonitorrequesttemplateDescExtraHeaders.Default.(map[string]string) // channelmonitorrequesttemplateDescBodyOverrideMode is the schema descriptor for body_override_mode field. - channelmonitorrequesttemplateDescBodyOverrideMode := channelmonitorrequesttemplateFields[4].Descriptor() + channelmonitorrequesttemplateDescBodyOverrideMode := channelmonitorrequesttemplateFields[5].Descriptor() // channelmonitorrequesttemplate.DefaultBodyOverrideMode holds the default value on creation for the body_override_mode field. channelmonitorrequesttemplate.DefaultBodyOverrideMode = channelmonitorrequesttemplateDescBodyOverrideMode.Default.(string) // channelmonitorrequesttemplate.BodyOverrideModeValidator is a validator for the "body_override_mode" field. It is called by the builders before save. diff --git a/backend/ent/schema/channel_monitor.go b/backend/ent/schema/channel_monitor.go index 355ade4b..431ab9c8 100644 --- a/backend/ent/schema/channel_monitor.go +++ b/backend/ent/schema/channel_monitor.go @@ -36,6 +36,10 @@ func (ChannelMonitor) Fields() []ent.Field { MaxLen(100), field.Enum("provider"). Values("openai", "anthropic", "gemini"), + field.String("api_mode"). + Default("chat_completions"). + MaxLen(32). + Comment("OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions"), field.String("endpoint"). NotEmpty(). MaxLen(500). @@ -104,6 +108,7 @@ func (ChannelMonitor) Indexes() []ent.Index { return []ent.Index{ index.Fields("enabled", "last_checked_at"), index.Fields("provider"), + index.Fields("provider", "api_mode"), index.Fields("group_name"), index.Fields("template_id"), } diff --git a/backend/ent/schema/channel_monitor_request_template.go b/backend/ent/schema/channel_monitor_request_template.go index 59df2f29..0e0ce3a0 100644 --- a/backend/ent/schema/channel_monitor_request_template.go +++ b/backend/ent/schema/channel_monitor_request_template.go @@ -40,6 +40,10 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field { MaxLen(100), field.Enum("provider"). Values("openai", "anthropic", "gemini"), + field.String("api_mode"). + Default("chat_completions"). + MaxLen(32). + Comment("OpenAI request protocol: chat_completions or responses; non-OpenAI uses chat_completions"), field.String("description"). Optional(). Default(""). @@ -76,5 +80,6 @@ func (ChannelMonitorRequestTemplate) Indexes() []ent.Index { return []ent.Index{ // 同一 provider 内 name 唯一:允许 Anthropic + OpenAI 重名 "伪装官方客户端"。 index.Fields("provider", "name").Unique(), + index.Fields("provider", "api_mode"), } } diff --git a/backend/internal/handler/admin/channel_monitor_handler.go b/backend/internal/handler/admin/channel_monitor_handler.go index e92c81fe..5560a513 100644 --- a/backend/internal/handler/admin/channel_monitor_handler.go +++ b/backend/internal/handler/admin/channel_monitor_handler.go @@ -38,6 +38,7 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch type channelMonitorCreateRequest struct { Name string `json:"name" binding:"required,max=100"` Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"` + APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"` Endpoint string `json:"endpoint" binding:"required,max=500"` APIKey string `json:"api_key" binding:"required,max=2000"` PrimaryModel string `json:"primary_model" binding:"required,max=200"` @@ -54,6 +55,7 @@ type channelMonitorCreateRequest struct { type channelMonitorUpdateRequest struct { Name *string `json:"name" binding:"omitempty,max=100"` Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini"` + APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"` Endpoint *string `json:"endpoint" binding:"omitempty,max=500"` APIKey *string `json:"api_key" binding:"omitempty,max=2000"` PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"` @@ -72,6 +74,7 @@ type channelMonitorResponse struct { ID int64 `json:"id"` Name string `json:"name"` Provider string `json:"provider"` + APIMode string `json:"api_mode"` Endpoint string `json:"endpoint"` APIKeyMasked string `json:"api_key_masked"` APIKeyDecryptFailed bool `json:"api_key_decrypt_failed"` @@ -138,6 +141,7 @@ func channelMonitorToResponse(m *service.ChannelMonitor) *channelMonitorResponse ID: m.ID, Name: m.Name, Provider: m.Provider, + APIMode: m.APIMode, Endpoint: m.Endpoint, APIKeyMasked: maskAPIKey(m.APIKey), APIKeyDecryptFailed: m.APIKeyDecryptFailed, @@ -303,6 +307,7 @@ func (h *ChannelMonitorHandler) Create(c *gin.Context) { m, err := h.monitorService.Create(c.Request.Context(), service.ChannelMonitorCreateParams{ Name: req.Name, Provider: req.Provider, + APIMode: req.APIMode, Endpoint: req.Endpoint, APIKey: req.APIKey, PrimaryModel: req.PrimaryModel, @@ -338,6 +343,7 @@ func (h *ChannelMonitorHandler) Update(c *gin.Context) { m, err := h.monitorService.Update(c.Request.Context(), id, service.ChannelMonitorUpdateParams{ Name: req.Name, Provider: req.Provider, + APIMode: req.APIMode, Endpoint: req.Endpoint, APIKey: req.APIKey, PrimaryModel: req.PrimaryModel, diff --git a/backend/internal/handler/admin/channel_monitor_template_handler.go b/backend/internal/handler/admin/channel_monitor_template_handler.go index bebe0929..c842f465 100644 --- a/backend/internal/handler/admin/channel_monitor_template_handler.go +++ b/backend/internal/handler/admin/channel_monitor_template_handler.go @@ -27,6 +27,7 @@ func NewChannelMonitorRequestTemplateHandler(templateService *service.ChannelMon type channelMonitorTemplateCreateRequest struct { Name string `json:"name" binding:"required,max=100"` Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"` + APIMode string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"` Description string `json:"description" binding:"max=500"` ExtraHeaders map[string]string `json:"extra_headers"` BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"` @@ -35,6 +36,7 @@ type channelMonitorTemplateCreateRequest struct { type channelMonitorTemplateUpdateRequest struct { Name *string `json:"name" binding:"omitempty,max=100"` + APIMode *string `json:"api_mode" binding:"omitempty,oneof=chat_completions responses"` Description *string `json:"description" binding:"omitempty,max=500"` ExtraHeaders *map[string]string `json:"extra_headers"` BodyOverrideMode *string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"` @@ -45,6 +47,7 @@ type channelMonitorTemplateResponse struct { ID int64 `json:"id"` Name string `json:"name"` Provider string `json:"provider"` + APIMode string `json:"api_mode"` Description string `json:"description"` ExtraHeaders map[string]string `json:"extra_headers"` BodyOverrideMode string `json:"body_override_mode"` @@ -67,6 +70,7 @@ func (h *ChannelMonitorRequestTemplateHandler) toResponse(c *gin.Context, t *ser ID: t.ID, Name: t.Name, Provider: t.Provider, + APIMode: t.APIMode, Description: t.Description, ExtraHeaders: headers, BodyOverrideMode: t.BodyOverrideMode, @@ -93,6 +97,7 @@ func parseTemplateID(c *gin.Context) (int64, bool) { func (h *ChannelMonitorRequestTemplateHandler) List(c *gin.Context) { items, err := h.templateService.List(c.Request.Context(), service.ChannelMonitorRequestTemplateListParams{ Provider: strings.TrimSpace(c.Query("provider")), + APIMode: strings.TrimSpace(c.Query("api_mode")), }) if err != nil { response.ErrorFrom(c, err) @@ -129,6 +134,7 @@ func (h *ChannelMonitorRequestTemplateHandler) Create(c *gin.Context) { t, err := h.templateService.Create(c.Request.Context(), service.ChannelMonitorRequestTemplateCreateParams{ Name: req.Name, Provider: req.Provider, + APIMode: req.APIMode, Description: req.Description, ExtraHeaders: req.ExtraHeaders, BodyOverrideMode: req.BodyOverrideMode, @@ -154,6 +160,7 @@ func (h *ChannelMonitorRequestTemplateHandler) Update(c *gin.Context) { } t, err := h.templateService.Update(c.Request.Context(), id, service.ChannelMonitorRequestTemplateUpdateParams{ Name: req.Name, + APIMode: req.APIMode, Description: req.Description, ExtraHeaders: req.ExtraHeaders, BodyOverrideMode: req.BodyOverrideMode, @@ -209,6 +216,7 @@ type associatedMonitorBriefResponse struct { ID int64 `json:"id"` Name string `json:"name"` Provider string `json:"provider"` + APIMode string `json:"api_mode"` Enabled bool `json:"enabled"` } @@ -227,7 +235,7 @@ func (h *ChannelMonitorRequestTemplateHandler) AssociatedMonitors(c *gin.Context out := make([]associatedMonitorBriefResponse, 0, len(items)) for _, m := range items { out = append(out, associatedMonitorBriefResponse{ - ID: m.ID, Name: m.Name, Provider: m.Provider, Enabled: m.Enabled, + ID: m.ID, Name: m.Name, Provider: m.Provider, APIMode: m.APIMode, Enabled: m.Enabled, }) } response.Success(c, gin.H{"items": out}) diff --git a/backend/internal/repository/channel_monitor_repo.go b/backend/internal/repository/channel_monitor_repo.go index 800ee43b..6666a130 100644 --- a/backend/internal/repository/channel_monitor_repo.go +++ b/backend/internal/repository/channel_monitor_repo.go @@ -37,6 +37,7 @@ func (r *channelMonitorRepository) Create(ctx context.Context, m *service.Channe builder := client.ChannelMonitor.Create(). SetName(m.Name). SetProvider(channelmonitor.Provider(m.Provider)). + SetAPIMode(defaultAPIModeRepo(m.APIMode)). SetEndpoint(m.Endpoint). SetAPIKeyEncrypted(m.APIKey). // 调用方传入的已是密文 SetPrimaryModel(m.PrimaryModel). @@ -79,6 +80,7 @@ func (r *channelMonitorRepository) Update(ctx context.Context, m *service.Channe updater := client.ChannelMonitor.UpdateOneID(m.ID). SetName(m.Name). SetProvider(channelmonitor.Provider(m.Provider)). + SetAPIMode(defaultAPIModeRepo(m.APIMode)). SetEndpoint(m.Endpoint). SetAPIKeyEncrypted(m.APIKey). SetPrimaryModel(m.PrimaryModel). @@ -708,6 +710,7 @@ func entToServiceMonitor(row *dbent.ChannelMonitor) *service.ChannelMonitor { ID: row.ID, Name: row.Name, Provider: string(row.Provider), + APIMode: defaultAPIModeRepo(row.APIMode), Endpoint: row.Endpoint, APIKey: row.APIKeyEncrypted, // 仍为密文,service 层负责解密 PrimaryModel: row.PrimaryModel, @@ -747,6 +750,13 @@ func defaultBodyModeRepo(mode string) string { return mode } +func defaultAPIModeRepo(apiMode string) string { + if apiMode == "" { + return "chat_completions" + } + return apiMode +} + func emptySliceIfNil(in []string) []string { if in == nil { return []string{} diff --git a/backend/internal/repository/channel_monitor_template_repo.go b/backend/internal/repository/channel_monitor_template_repo.go index 845d186b..3a972360 100644 --- a/backend/internal/repository/channel_monitor_template_repo.go +++ b/backend/internal/repository/channel_monitor_template_repo.go @@ -30,6 +30,7 @@ func (r *channelMonitorRequestTemplateRepository) Create(ctx context.Context, t builder := client.ChannelMonitorRequestTemplate.Create(). SetName(t.Name). SetProvider(channelmonitorrequesttemplate.Provider(t.Provider)). + SetAPIMode(defaultAPIModeRepo(t.APIMode)). SetDescription(t.Description). SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)). SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode)) @@ -61,6 +62,7 @@ func (r *channelMonitorRequestTemplateRepository) Update(ctx context.Context, t client := clientFromContext(ctx, r.client) updater := client.ChannelMonitorRequestTemplate.UpdateOneID(t.ID). SetName(t.Name). + SetAPIMode(defaultAPIModeRepo(t.APIMode)). SetDescription(t.Description). SetExtraHeaders(emptyHeadersIfNilRepo(t.ExtraHeaders)). SetBodyOverrideMode(defaultBodyModeRepo(t.BodyOverrideMode)) @@ -90,8 +92,11 @@ func (r *channelMonitorRequestTemplateRepository) List(ctx context.Context, para if params.Provider != "" { q = q.Where(channelmonitorrequesttemplate.ProviderEQ(channelmonitorrequesttemplate.Provider(params.Provider))) } + if params.APIMode != "" { + q = q.Where(channelmonitorrequesttemplate.APIModeEQ(defaultAPIModeRepo(params.APIMode))) + } rows, err := q. - Order(dbent.Asc(channelmonitorrequesttemplate.FieldProvider), dbent.Asc(channelmonitorrequesttemplate.FieldName)). + Order(dbent.Asc(channelmonitorrequesttemplate.FieldProvider), dbent.Asc(channelmonitorrequesttemplate.FieldAPIMode), dbent.Asc(channelmonitorrequesttemplate.FieldName)). All(ctx) if err != nil { return nil, fmt.Errorf("list monitor templates: %w", err) @@ -122,7 +127,10 @@ func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Co Where( channelmonitor.TemplateIDEQ(id), channelmonitor.IDIn(monitorIDs...), + channelmonitor.ProviderEQ(channelmonitor.Provider(tpl.Provider)), + channelmonitor.APIModeEQ(defaultAPIModeRepo(tpl.APIMode)), ). + SetAPIMode(defaultAPIModeRepo(tpl.APIMode)). SetExtraHeaders(emptyHeadersIfNilRepo(tpl.ExtraHeaders)). SetBodyOverrideMode(defaultBodyModeRepo(tpl.BodyOverrideMode)) if tpl.BodyOverride != nil { @@ -165,6 +173,7 @@ func (r *channelMonitorRequestTemplateRepository) ListAssociatedMonitors(ctx con ID: row.ID, Name: row.Name, Provider: string(row.Provider), + APIMode: defaultAPIModeRepo(row.APIMode), Enabled: row.Enabled, }) } @@ -185,6 +194,7 @@ func entToServiceTemplate(row *dbent.ChannelMonitorRequestTemplate) *service.Cha ID: row.ID, Name: row.Name, Provider: string(row.Provider), + APIMode: defaultAPIModeRepo(row.APIMode), Description: row.Description, ExtraHeaders: headers, BodyOverrideMode: row.BodyOverrideMode, diff --git a/backend/internal/service/channel_monitor_checker.go b/backend/internal/service/channel_monitor_checker.go index 33570629..25737e45 100644 --- a/backend/internal/service/channel_monitor_checker.go +++ b/backend/internal/service/channel_monitor_checker.go @@ -40,6 +40,8 @@ func newSSRFSafeHTTPClient(timeout time.Duration) *http.Client { // CheckOptions 承载一次检测的自定义入参。 // 所有字段都是可选(零值即等价于"用默认行为")。 type CheckOptions struct { + // APIMode 仅对 OpenAI provider 生效;空串等同 chat_completions。 + APIMode string // ExtraHeaders 用户自定义 HTTP 头(merge 到 adapter 默认 headers,用户优先)。 ExtraHeaders map[string]string // BodyOverrideMode: off | merge | replace @@ -164,21 +166,7 @@ type providerAdapter struct { // //nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。 var providerAdapters = map[string]providerAdapter{ - MonitorProviderOpenAI: { - buildPath: func(string) string { return providerOpenAIPath }, - buildBody: func(model, prompt string) ([]byte, error) { - return json.Marshal(map[string]any{ - "model": model, - "messages": []map[string]string{{"role": "user", "content": prompt}}, - "max_tokens": monitorChallengeMaxTokens, - "stream": false, - }) - }, - buildHeaders: func(apiKey string) map[string]string { - return map[string]string{"Authorization": "Bearer " + apiKey} - }, - textPath: "choices.0.message.content", - }, + MonitorProviderOpenAI: providerOpenAIChatAdapter, MonitorProviderAnthropic: { buildPath: func(string) string { return providerAnthropicPath }, buildBody: func(model, prompt string) ([]byte, error) { @@ -215,6 +203,50 @@ var providerAdapters = map[string]providerAdapter{ }, } +//nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。 +var providerOpenAIChatAdapter = providerAdapter{ + buildPath: func(string) string { return providerOpenAIPath }, + buildBody: func(model, prompt string) ([]byte, error) { + return json.Marshal(map[string]any{ + "model": model, + "messages": []map[string]string{{"role": "user", "content": prompt}}, + "max_tokens": monitorChallengeMaxTokens, + "stream": false, + }) + }, + buildHeaders: func(apiKey string) map[string]string { + return map[string]string{"Authorization": "Bearer " + apiKey} + }, + textPath: "choices.0.message.content", +} + +//nolint:gochecknoglobals // 适配器表是只读静态数据,初始化后不变更。 +var providerOpenAIResponsesAdapter = providerAdapter{ + buildPath: func(string) string { return providerOpenAIResponsesPath }, + buildBody: func(model, prompt string) ([]byte, error) { + return json.Marshal(map[string]any{ + "model": model, + "instructions": "You are a channel health-check endpoint. Answer the arithmetic challenge exactly and briefly.", + "input": prompt, + "max_output_tokens": monitorChallengeMaxTokens, + "stream": false, + }) + }, + buildHeaders: func(apiKey string) map[string]string { + return map[string]string{"Authorization": "Bearer " + apiKey} + }, + textPath: "output.0.content.0.text", +} + +// providerAdapterFor 按 provider + api_mode 选择具体 adapter。 +func providerAdapterFor(provider, apiMode string) (providerAdapter, string, bool) { + if provider == MonitorProviderOpenAI && defaultAPIMode(apiMode) == MonitorAPIModeResponses { + return providerOpenAIResponsesAdapter, MonitorAPIModeResponses, true + } + adapter, ok := providerAdapters[provider] + return adapter, MonitorAPIModeChatCompletions, ok +} + // isSupportedProvider 校验 provider 字符串是否在 adapter 表中。 // 供 validate.go 的 validateProvider 复用,避免两份 switch 漂移。 func isSupportedProvider(p string) bool { @@ -231,11 +263,15 @@ func isSupportedProvider(p string) bool { // - status: HTTP 状态码 // - err: 网络 / 序列化错误 func callProvider(ctx context.Context, provider, endpoint, apiKey, model, prompt string, opts *CheckOptions) (extractedText, rawBody string, status int, err error) { - adapter, ok := providerAdapters[provider] + requestedAPIMode := checkAPIMode(opts) + if err := validateAPIMode(provider, requestedAPIMode); err != nil { + return "", "", 0, err + } + adapter, apiMode, ok := providerAdapterFor(provider, requestedAPIMode) if !ok { return "", "", 0, fmt.Errorf("unsupported provider %q", provider) } - body, err := buildRequestBody(adapter, provider, model, prompt, opts) + body, err := buildRequestBody(adapter, provider, apiMode, model, prompt, opts) if err != nil { return "", "", 0, err } @@ -275,13 +311,16 @@ func mergeHeaders(base map[string]string, opts *CheckOptions) map[string]string // - replace: 直接 marshal BodyOverride 作为完整 body // // 任何 mode 返回的 []byte 都已经是合法 JSON,可直接送入 postRawJSON。 -func buildRequestBody(adapter providerAdapter, provider, model, prompt string, opts *CheckOptions) ([]byte, error) { +func buildRequestBody(adapter providerAdapter, provider, apiMode, model, prompt string, opts *CheckOptions) ([]byte, error) { mode := bodyOverrideMode(opts) if mode == MonitorBodyOverrideModeReplace { if opts == nil || len(opts.BodyOverride) == 0 { return nil, fmt.Errorf("replace mode: body_override is empty") } + if err := validateReplaceRequestBody(provider, apiMode, opts.BodyOverride); err != nil { + return nil, err + } body, err := json.Marshal(opts.BodyOverride) if err != nil { return nil, fmt.Errorf("marshal body_override (replace): %w", err) @@ -301,7 +340,7 @@ func buildRequestBody(adapter providerAdapter, provider, model, prompt string, o if err := json.Unmarshal(defaultBody, &defaultMap); err != nil { return nil, fmt.Errorf("unmarshal default body for merge: %w", err) } - deny := bodyMergeKeyDenyList[provider] + deny := bodyMergeKeyDenyList[bodyMergeDenyKey(provider, apiMode)] for k, v := range opts.BodyOverride { if deny[k] { continue @@ -321,9 +360,63 @@ func buildRequestBody(adapter providerAdapter, provider, model, prompt string, o // //nolint:gochecknoglobals // 静态查表,初始化后不变。 var bodyMergeKeyDenyList = map[string]map[string]bool{ - MonitorProviderOpenAI: {"model": true, "messages": true, "stream": true}, - MonitorProviderAnthropic: {"model": true, "messages": true}, - MonitorProviderGemini: {"contents": true}, + MonitorProviderOpenAI + ":" + MonitorAPIModeChatCompletions: {"model": true, "messages": true, "stream": true}, + MonitorProviderOpenAI + ":" + MonitorAPIModeResponses: {"model": true, "instructions": true, "input": true, "stream": true}, + MonitorProviderAnthropic: {"model": true, "messages": true}, + MonitorProviderGemini: {"contents": true}, +} + +func checkAPIMode(opts *CheckOptions) string { + if opts == nil { + return MonitorAPIModeChatCompletions + } + return defaultAPIMode(opts.APIMode) +} + +func bodyMergeDenyKey(provider, apiMode string) string { + if provider == MonitorProviderOpenAI { + return provider + ":" + defaultAPIMode(apiMode) + } + return provider +} + +func validateReplaceRequestBody(provider, apiMode string, body map[string]any) error { + if provider != MonitorProviderOpenAI { + return nil + } + switch defaultAPIMode(apiMode) { + case MonitorAPIModeResponses: + if strings.TrimSpace(stringFromAny(body["instructions"])) == "" || !hasNonEmptyBodyValue(body["input"]) { + return fmt.Errorf("replace mode responses body: instructions and input are required") + } + case MonitorAPIModeChatCompletions: + if !hasNonEmptyBodyValue(body["messages"]) { + return fmt.Errorf("replace mode chat_completions body: messages are required") + } + } + return nil +} + +func stringFromAny(v any) string { + s, _ := v.(string) + return s +} + +func hasNonEmptyBodyValue(v any) bool { + switch val := v.(type) { + case nil: + return false + case string: + return strings.TrimSpace(val) != "" + case []any: + return len(val) > 0 + case []map[string]any: + return len(val) > 0 + case []map[string]string: + return len(val) > 0 + default: + return true + } } // postRawJSON 发送 POST + 已序列化好的 JSON 字节,限制响应体大小,返回响应字节、HTTP status、错误。 diff --git a/backend/internal/service/channel_monitor_checker_body_test.go b/backend/internal/service/channel_monitor_checker_body_test.go index 323cf8b7..620cf565 100644 --- a/backend/internal/service/channel_monitor_checker_body_test.go +++ b/backend/internal/service/channel_monitor_checker_body_test.go @@ -7,6 +7,8 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "regexp" + "strconv" "strings" "testing" "time" @@ -57,6 +59,76 @@ func setupFakeAnthropic(t *testing.T, handler *captureHandler) string { return srv.URL } +type openAICaptureHandler struct { + lastBody map[string]any + lastHeaders http.Header + lastPath string + status int +} + +func (h *openAICaptureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.lastHeaders = r.Header.Clone() + h.lastPath = r.URL.Path + defer func() { _ = r.Body.Close() }() + var parsed map[string]any + _ = json.NewDecoder(r.Body).Decode(&parsed) + h.lastBody = parsed + + if h.status == 0 { + h.status = http.StatusOK + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(h.status) + + answer := answerFromOpenAIRequest(parsed) + if h.lastPath == providerOpenAIResponsesPath { + _ = json.NewEncoder(w).Encode(map[string]any{ + "output": []map[string]any{{ + "content": []map[string]any{{"type": "output_text", "text": answer}}, + }}, + }) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "choices": []map[string]any{{"message": map[string]any{"content": answer}}}, + }) +} + +func setupFakeOpenAI(t *testing.T, handler *openAICaptureHandler) string { + t.Helper() + swapMonitorHTTPClient(t) + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + return srv.URL +} + +func answerFromOpenAIRequest(body map[string]any) string { + prompt, _ := body["input"].(string) + if prompt == "" { + if messages, ok := body["messages"].([]any); ok && len(messages) > 0 { + if msg, ok := messages[0].(map[string]any); ok { + prompt, _ = msg["content"].(string) + } + } + } + return answerFromChallengePrompt(prompt) +} + +var challengeQuestionRegex = regexp.MustCompile(`Q: (\d+) ([+-]) (\d+) = \?\nA:$`) + +func answerFromChallengePrompt(prompt string) string { + m := challengeQuestionRegex.FindStringSubmatch(prompt) + if len(m) != 4 { + return "0" + } + left, _ := strconv.Atoi(m[1]) + right, _ := strconv.Atoi(m[3]) + if m[2] == "+" { + return strconv.Itoa(left + right) + } + return strconv.Itoa(left - right) +} + func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) { h := &captureHandler{respondText: "the answer is 42"} endpoint := setupFakeAnthropic(t, h) @@ -75,6 +147,95 @@ func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) { } } +func TestRunCheckForModel_OpenAI_DefaultChatRequest(t *testing.T) { + h := &openAICaptureHandler{} + endpoint := setupFakeOpenAI(t, h) + + res := runCheckForModel(context.Background(), MonitorProviderOpenAI, endpoint, "sk-openai", "gpt-test", nil) + + if res.Status != MonitorStatusOperational { + t.Fatalf("default chat request should pass challenge, got status=%s message=%q", res.Status, res.Message) + } + if h.lastPath != providerOpenAIPath { + t.Fatalf("expected chat completions path %q, got %q", providerOpenAIPath, h.lastPath) + } + if h.lastBody["model"] != "gpt-test" { + t.Errorf("chat body should contain model=gpt-test, got %v", h.lastBody["model"]) + } + if _, ok := h.lastBody["messages"]; !ok { + t.Error("chat body should contain messages") + } + if _, ok := h.lastBody["instructions"]; ok { + t.Error("chat body must not contain top-level instructions") + } + if h.lastBody["stream"] != false { + t.Errorf("chat body should set stream=false, got %v", h.lastBody["stream"]) + } + if h.lastHeaders.Get("Authorization") != "Bearer sk-openai" { + t.Errorf("expected bearer auth header, got %q", h.lastHeaders.Get("Authorization")) + } +} + +func TestRunCheckForModel_OpenAIResponses_DefaultRequest(t *testing.T) { + h := &openAICaptureHandler{} + endpoint := setupFakeOpenAI(t, h) + + res := runCheckForModel(context.Background(), MonitorProviderOpenAI, endpoint, "sk-openai", "gpt-test", &CheckOptions{ + APIMode: MonitorAPIModeResponses, + }) + + if res.Status != MonitorStatusOperational { + t.Fatalf("default responses request should pass challenge, got status=%s message=%q", res.Status, res.Message) + } + if h.lastPath != providerOpenAIResponsesPath { + t.Fatalf("expected responses path %q, got %q", providerOpenAIResponsesPath, h.lastPath) + } + if h.lastBody["model"] != "gpt-test" { + t.Errorf("responses body should contain model=gpt-test, got %v", h.lastBody["model"]) + } + instructions, _ := h.lastBody["instructions"].(string) + if strings.TrimSpace(instructions) == "" { + t.Error("responses body should contain non-empty instructions") + } + input, _ := h.lastBody["input"].(string) + if strings.TrimSpace(input) == "" { + t.Error("responses body should contain non-empty input") + } + if _, ok := h.lastBody["messages"]; ok { + t.Error("responses body must not contain chat messages") + } + if h.lastBody["stream"] != false { + t.Errorf("responses body should set stream=false, got %v", h.lastBody["stream"]) + } + if h.lastHeaders.Get("Authorization") != "Bearer sk-openai" { + t.Errorf("expected bearer auth header, got %q", h.lastHeaders.Get("Authorization")) + } +} + +func TestRunCheckForModel_OpenAIResponsesReplaceMissingInstructionsFailsLocally(t *testing.T) { + h := &openAICaptureHandler{} + endpoint := setupFakeOpenAI(t, h) + + res := runCheckForModel(context.Background(), MonitorProviderOpenAI, endpoint, "sk-openai", "gpt-test", &CheckOptions{ + APIMode: MonitorAPIModeResponses, + BodyOverrideMode: MonitorBodyOverrideModeReplace, + BodyOverride: map[string]any{ + "model": "gpt-test", + "input": "hello", + }, + }) + + if res.Status != MonitorStatusError { + t.Fatalf("invalid responses replace body should fail locally as error, got status=%s", res.Status) + } + if !strings.Contains(res.Message, "instructions and input are required") { + t.Errorf("expected local validation message about instructions/input, got %q", res.Message) + } + if h.lastPath != "" { + t.Errorf("invalid replace body should fail before HTTP request, got path %q", h.lastPath) + } +} + func TestRunCheckForModel_MergeMode_UserFieldsWinButDenyListProtects(t *testing.T) { h := &captureHandler{respondText: "the answer is 42"} endpoint := setupFakeAnthropic(t, h) diff --git a/backend/internal/service/channel_monitor_const.go b/backend/internal/service/channel_monitor_const.go index 2e1614f7..d1dffac5 100644 --- a/backend/internal/service/channel_monitor_const.go +++ b/backend/internal/service/channel_monitor_const.go @@ -47,6 +47,8 @@ const ( // providerOpenAIPath OpenAI Chat Completions 路径。 providerOpenAIPath = "/v1/chat/completions" + // providerOpenAIResponsesPath OpenAI Responses API 路径。 + providerOpenAIResponsesPath = "/v1/responses" // providerAnthropicPath Anthropic Messages 路径。 providerAnthropicPath = "/v1/messages" // providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。 @@ -112,6 +114,12 @@ var ( ErrChannelMonitorInvalidProvider = infraerrors.BadRequest( "CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini", ) + ErrChannelMonitorInvalidAPIMode = infraerrors.BadRequest( + "CHANNEL_MONITOR_INVALID_API_MODE", "api_mode must be chat_completions or responses; responses is only supported for openai", + ) + ErrChannelMonitorInvalidRequestBody = infraerrors.BadRequest( + "CHANNEL_MONITOR_INVALID_REQUEST_BODY", "openai replace-mode body_override must include non-empty messages for chat_completions or non-empty instructions and input for responses", + ) ErrChannelMonitorInvalidInterval = infraerrors.BadRequest( "CHANNEL_MONITOR_INVALID_INTERVAL", "interval_seconds must be in [15, 3600]", ) diff --git a/backend/internal/service/channel_monitor_service.go b/backend/internal/service/channel_monitor_service.go index 7050e141..6eec0ae0 100644 --- a/backend/internal/service/channel_monitor_service.go +++ b/backend/internal/service/channel_monitor_service.go @@ -107,7 +107,7 @@ func (s *ChannelMonitorService) Create(ctx context.Context, p ChannelMonitorCrea if err := validateCreateParams(p); err != nil { return nil, err } - if err := validateBodyModeParams(p.BodyOverrideMode, p.BodyOverride); err != nil { + if err := validateBodyModeForProtocol(p.Provider, p.APIMode, p.BodyOverrideMode, p.BodyOverride); err != nil { return nil, err } if err := validateExtraHeaders(p.ExtraHeaders); err != nil { @@ -120,6 +120,7 @@ func (s *ChannelMonitorService) Create(ctx context.Context, p ChannelMonitorCrea m := &ChannelMonitor{ Name: strings.TrimSpace(p.Name), Provider: p.Provider, + APIMode: defaultAPIMode(p.APIMode), Endpoint: normalizeEndpoint(p.Endpoint), APIKey: encrypted, // 注意:传入 repository 时该字段为密文 PrimaryModel: strings.TrimSpace(p.PrimaryModel), @@ -150,6 +151,9 @@ func validateCreateParams(p ChannelMonitorCreateParams) error { if err := validateProvider(p.Provider); err != nil { return err } + if err := validateAPIMode(p.Provider, p.APIMode); err != nil { + return err + } if err := validateInterval(p.IntervalSeconds); err != nil { return err } @@ -298,6 +302,7 @@ func (s *ChannelMonitorService) runChecksConcurrent(ctx context.Context, m *Chan // 所有模型共用同一份 CheckOptions(来自监控的快照字段)。 opts := &CheckOptions{ + APIMode: m.APIMode, ExtraHeaders: m.ExtraHeaders, BodyOverrideMode: m.BodyOverrideMode, BodyOverride: m.BodyOverride, @@ -469,6 +474,7 @@ func (s *ChannelMonitorService) decryptInPlace(m *ChannelMonitor) { // 行数稍超过 30:这是逐字段平铺的 dispatcher,每个 if 都是 1-3 行的"非 nil 则覆盖"模式, // 拆分反而会增加跳转噪音、影响可读性,故保留为单函数。 func applyMonitorUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams) error { + providerChanged := false if p.Name != nil { existing.Name = strings.TrimSpace(*p.Name) } @@ -477,6 +483,7 @@ func applyMonitorUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams) return err } existing.Provider = *p.Provider + providerChanged = true } if p.Endpoint != nil { if err := validateEndpoint(*p.Endpoint); err != nil { @@ -502,11 +509,11 @@ func applyMonitorUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams) } existing.IntervalSeconds = *p.IntervalSeconds } - return applyMonitorAdvancedUpdate(existing, p) + return applyMonitorAdvancedUpdate(existing, p, providerChanged) } // applyMonitorAdvancedUpdate 处理自定义请求快照相关字段,从 applyMonitorUpdate 拆出避免过长。 -func applyMonitorAdvancedUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams) error { +func applyMonitorAdvancedUpdate(existing *ChannelMonitor, p ChannelMonitorUpdateParams, providerChanged bool) error { if p.ClearTemplate { existing.TemplateID = nil } else if p.TemplateID != nil { @@ -519,6 +526,15 @@ func applyMonitorAdvancedUpdate(existing *ChannelMonitor, p ChannelMonitorUpdate } existing.ExtraHeaders = emptyHeadersIfNil(*p.ExtraHeaders) } + newAPIMode := defaultAPIMode(existing.APIMode) + if p.APIMode != nil { + newAPIMode = defaultAPIMode(*p.APIMode) + } else if existing.Provider != MonitorProviderOpenAI { + newAPIMode = MonitorAPIModeChatCompletions + } + if err := validateAPIMode(existing.Provider, newAPIMode); err != nil { + return err + } // BodyOverrideMode / BodyOverride 联合校验,和模板一致。 newMode := existing.BodyOverrideMode newBody := existing.BodyOverride @@ -528,12 +544,13 @@ func applyMonitorAdvancedUpdate(existing *ChannelMonitor, p ChannelMonitorUpdate if p.BodyOverride != nil { newBody = *p.BodyOverride } - if p.BodyOverrideMode != nil || p.BodyOverride != nil { - if err := validateBodyModeParams(newMode, newBody); err != nil { + if providerChanged || p.APIMode != nil || p.BodyOverrideMode != nil || p.BodyOverride != nil { + if err := validateBodyModeForProtocol(existing.Provider, newAPIMode, newMode, newBody); err != nil { return err } existing.BodyOverrideMode = defaultBodyMode(newMode) existing.BodyOverride = newBody } + existing.APIMode = newAPIMode return nil } diff --git a/backend/internal/service/channel_monitor_template_service.go b/backend/internal/service/channel_monitor_template_service.go index 8d2e8173..4d50952d 100644 --- a/backend/internal/service/channel_monitor_template_service.go +++ b/backend/internal/service/channel_monitor_template_service.go @@ -14,14 +14,14 @@ type ChannelMonitorRequestTemplateRepository interface { Update(ctx context.Context, t *ChannelMonitorRequestTemplate) error Delete(ctx context.Context, id int64) error List(ctx context.Context, params ChannelMonitorRequestTemplateListParams) ([]*ChannelMonitorRequestTemplate, error) - // ApplyToMonitors 把模板当前的 extra_headers / body_override_mode / body_override + // ApplyToMonitors 把模板当前的 api_mode / extra_headers / body_override_mode / body_override // 批量覆盖到指定 monitorIDs 的监控上(同时还要求这些监控当前 template_id = id, // 防止误覆盖未关联的监控)。monitorIDs 必须非空;空列表直接返回 0 不写库。 // 返回被覆盖的监控数量。 ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error) // CountAssociatedMonitors 统计 template_id = id 的监控数(用于 UI 展示「应用到 N 个配置」)。 CountAssociatedMonitors(ctx context.Context, id int64) (int64, error) - // ListAssociatedMonitors 列出所有 template_id = id 的监控简略信息(id/name/provider/enabled) + // ListAssociatedMonitors 列出所有 template_id = id 的监控简略信息(id/name/provider/api_mode/enabled) // 给 apply picker UI 用,避免前端再做一次 list+filter。 ListAssociatedMonitors(ctx context.Context, id int64) ([]*AssociatedMonitorBrief, error) } @@ -31,6 +31,7 @@ type AssociatedMonitorBrief struct { ID int64 Name string Provider string + APIMode string Enabled bool } @@ -53,6 +54,15 @@ func (s *ChannelMonitorRequestTemplateService) List(ctx context.Context, params return nil, err } } + if params.APIMode != "" { + if params.Provider == "" { + if err := validateAPIMode(MonitorProviderOpenAI, params.APIMode); err != nil { + return nil, err + } + } else if err := validateAPIMode(params.Provider, params.APIMode); err != nil { + return nil, err + } + } return s.repo.List(ctx, params) } @@ -69,6 +79,7 @@ func (s *ChannelMonitorRequestTemplateService) Create(ctx context.Context, p Cha t := &ChannelMonitorRequestTemplate{ Name: strings.TrimSpace(p.Name), Provider: p.Provider, + APIMode: defaultAPIMode(p.APIMode), Description: strings.TrimSpace(p.Description), ExtraHeaders: emptyHeadersIfNil(p.ExtraHeaders), BodyOverrideMode: defaultBodyMode(p.BodyOverrideMode), @@ -144,7 +155,10 @@ func validateTemplateCreateParams(p ChannelMonitorRequestTemplateCreateParams) e if err := validateProvider(p.Provider); err != nil { return ErrChannelMonitorTemplateInvalidProvider } - if err := validateBodyModeParams(p.BodyOverrideMode, p.BodyOverride); err != nil { + if err := validateAPIMode(p.Provider, p.APIMode); err != nil { + return ErrChannelMonitorTemplateInvalidAPIMode + } + if err := validateBodyModeForProtocol(p.Provider, p.APIMode, p.BodyOverrideMode, p.BodyOverride); err != nil { return err } if err := validateExtraHeaders(p.ExtraHeaders); err != nil { @@ -165,6 +179,13 @@ func applyTemplateUpdate(existing *ChannelMonitorRequestTemplate, p ChannelMonit if p.Description != nil { existing.Description = strings.TrimSpace(*p.Description) } + newAPIMode := defaultAPIMode(existing.APIMode) + if p.APIMode != nil { + newAPIMode = defaultAPIMode(*p.APIMode) + } + if err := validateAPIMode(existing.Provider, newAPIMode); err != nil { + return ErrChannelMonitorTemplateInvalidAPIMode + } if p.ExtraHeaders != nil { if err := validateExtraHeaders(*p.ExtraHeaders); err != nil { return err @@ -180,14 +201,29 @@ func applyTemplateUpdate(existing *ChannelMonitorRequestTemplate, p ChannelMonit if p.BodyOverride != nil { newBody = *p.BodyOverride } - if err := validateBodyModeParams(newMode, newBody); err != nil { + if err := validateBodyModeForProtocol(existing.Provider, newAPIMode, newMode, newBody); err != nil { return err } + existing.APIMode = newAPIMode existing.BodyOverrideMode = defaultBodyMode(newMode) existing.BodyOverride = newBody return nil } +// validateBodyModeForProtocol 校验 body_override_mode 与 provider/api_mode 的协议特定要求。 +func validateBodyModeForProtocol(provider, apiMode, mode string, body map[string]any) error { + if err := validateBodyModeParams(mode, body); err != nil { + return err + } + if defaultBodyMode(mode) != MonitorBodyOverrideModeReplace { + return nil + } + if err := validateReplaceRequestBody(provider, defaultAPIMode(apiMode), body); err != nil { + return ErrChannelMonitorInvalidRequestBody + } + return nil +} + // validateBodyModeParams 校验 body_override_mode 合法,且 merge/replace 模式下 body_override 非空。 func validateBodyModeParams(mode string, body map[string]any) error { switch mode { diff --git a/backend/internal/service/channel_monitor_template_types.go b/backend/internal/service/channel_monitor_template_types.go index e5bf7568..03cd518d 100644 --- a/backend/internal/service/channel_monitor_template_types.go +++ b/backend/internal/service/channel_monitor_template_types.go @@ -12,6 +12,7 @@ type ChannelMonitorRequestTemplate struct { ID int64 Name string Provider string + APIMode string Description string ExtraHeaders map[string]string BodyOverrideMode string @@ -23,12 +24,14 @@ type ChannelMonitorRequestTemplate struct { // ChannelMonitorRequestTemplateListParams 列表过滤。 type ChannelMonitorRequestTemplateListParams struct { Provider string // 空 = 全部;非空则按 provider 过滤 + APIMode string // 空 = 全部;非空则按 api_mode 过滤 } // ChannelMonitorRequestTemplateCreateParams 创建参数。 type ChannelMonitorRequestTemplateCreateParams struct { Name string Provider string + APIMode string Description string ExtraHeaders map[string]string BodyOverrideMode string @@ -39,6 +42,7 @@ type ChannelMonitorRequestTemplateCreateParams struct { // 注意 Provider 不可修改:改 provider 会让已关联监控的 body 黑名单语义错乱。 type ChannelMonitorRequestTemplateUpdateParams struct { Name *string + APIMode *string Description *string ExtraHeaders *map[string]string BodyOverrideMode *string @@ -53,6 +57,9 @@ var ( ErrChannelMonitorTemplateInvalidProvider = infraerrors.BadRequest( "CHANNEL_MONITOR_TEMPLATE_INVALID_PROVIDER", "template provider must be one of openai/anthropic/gemini", ) + ErrChannelMonitorTemplateInvalidAPIMode = infraerrors.BadRequest( + "CHANNEL_MONITOR_TEMPLATE_INVALID_API_MODE", "template api_mode must be chat_completions or responses; responses is only supported for openai", + ) ErrChannelMonitorTemplateMissingName = infraerrors.BadRequest( "CHANNEL_MONITOR_TEMPLATE_MISSING_NAME", "template name is required", ) @@ -71,6 +78,9 @@ var ( ErrChannelMonitorTemplateProviderMismatch = infraerrors.BadRequest( "CHANNEL_MONITOR_TEMPLATE_PROVIDER_MISMATCH", "monitor provider does not match template provider", ) + ErrChannelMonitorTemplateAPIModeMismatch = infraerrors.BadRequest( + "CHANNEL_MONITOR_TEMPLATE_API_MODE_MISMATCH", "monitor api_mode does not match template api_mode", + ) ErrChannelMonitorTemplateApplyEmpty = infraerrors.BadRequest( "CHANNEL_MONITOR_TEMPLATE_APPLY_EMPTY", "monitor_ids must be a non-empty array", ) diff --git a/backend/internal/service/channel_monitor_types.go b/backend/internal/service/channel_monitor_types.go index b797a89b..ef86eeb8 100644 --- a/backend/internal/service/channel_monitor_types.go +++ b/backend/internal/service/channel_monitor_types.go @@ -15,11 +15,23 @@ const ( MonitorBodyOverrideModeReplace = "replace" ) +// MonitorAPIMode 描述 OpenAI provider 的请求协议。 +// +// - chat_completions OpenAI-compatible Chat Completions: /v1/chat/completions + messages +// - responses OpenAI Responses API: /v1/responses + instructions/input +// +// 非 OpenAI provider 固定使用 chat_completions 作为占位默认值,避免为每个 provider 单独扩表。 +const ( + MonitorAPIModeChatCompletions = "chat_completions" + MonitorAPIModeResponses = "responses" +) + // ChannelMonitor 渠道监控配置(service 层模型,不直接暴露 ent 类型)。 type ChannelMonitor struct { ID int64 Name string Provider string + APIMode string Endpoint string APIKey string // 解密后的明文 API Key(仅在 service 内部使用,handler 层不应直接序列化返回) PrimaryModel string @@ -56,6 +68,7 @@ type ChannelMonitorListParams struct { type ChannelMonitorCreateParams struct { Name string Provider string + APIMode string Endpoint string APIKey string PrimaryModel string @@ -74,6 +87,7 @@ type ChannelMonitorCreateParams struct { type ChannelMonitorUpdateParams struct { Name *string Provider *string + APIMode *string Endpoint *string APIKey *string // 空字符串表示不修改;非空字符串覆盖 PrimaryModel *string diff --git a/backend/internal/service/channel_monitor_validate.go b/backend/internal/service/channel_monitor_validate.go index 16bbec71..6ff22b9f 100644 --- a/backend/internal/service/channel_monitor_validate.go +++ b/backend/internal/service/channel_monitor_validate.go @@ -18,6 +18,23 @@ func validateProvider(p string) error { return nil } +// validateAPIMode 校验 provider 与 api_mode 的组合。 +// responses 只对 OpenAI 有意义;其它 provider 使用 chat_completions 作为默认占位。 +func validateAPIMode(provider, apiMode string) error { + apiMode = defaultAPIMode(apiMode) + switch apiMode { + case MonitorAPIModeChatCompletions: + return nil + case MonitorAPIModeResponses: + if provider == "" || provider == MonitorProviderOpenAI { + return nil + } + return ErrChannelMonitorInvalidAPIMode + default: + return ErrChannelMonitorInvalidAPIMode + } +} + // validateInterval 校验 interval_seconds 范围。 func validateInterval(sec int) error { if sec < monitorMinIntervalSeconds || sec > monitorMaxIntervalSeconds { @@ -97,3 +114,11 @@ func normalizeModels(in []string) []string { } return out } + +// defaultAPIMode 空串归一为 chat_completions,保证历史数据与旧客户端兼容。 +func defaultAPIMode(apiMode string) string { + if strings.TrimSpace(apiMode) == "" { + return MonitorAPIModeChatCompletions + } + return strings.TrimSpace(apiMode) +} diff --git a/backend/migrations/138_channel_monitor_openai_api_mode.sql b/backend/migrations/138_channel_monitor_openai_api_mode.sql new file mode 100644 index 00000000..5b16f39c --- /dev/null +++ b/backend/migrations/138_channel_monitor_openai_api_mode.sql @@ -0,0 +1,40 @@ +-- Migration: 137_channel_monitor_openai_api_mode +-- 为渠道监控和请求模板增加 OpenAI 协议模式: +-- chat_completions -> /v1/chat/completions + messages +-- responses -> /v1/responses + instructions/input +-- 历史数据默认保持 chat_completions,避免改变现有监控行为。 + +ALTER TABLE channel_monitors + ADD COLUMN IF NOT EXISTS api_mode VARCHAR(32) NOT NULL DEFAULT 'chat_completions'; + +ALTER TABLE channel_monitor_request_templates + ADD COLUMN IF NOT EXISTS api_mode VARCHAR(32) NOT NULL DEFAULT 'chat_completions'; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'channel_monitors_api_mode_check' + AND table_name = 'channel_monitors' + ) THEN + ALTER TABLE channel_monitors + ADD CONSTRAINT channel_monitors_api_mode_check + CHECK (api_mode IN ('chat_completions', 'responses')); + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'channel_monitor_request_templates_api_mode_check' + AND table_name = 'channel_monitor_request_templates' + ) THEN + ALTER TABLE channel_monitor_request_templates + ADD CONSTRAINT channel_monitor_request_templates_api_mode_check + CHECK (api_mode IN ('chat_completions', 'responses')); + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_channel_monitors_provider_api_mode + ON channel_monitors (provider, api_mode); + +CREATE INDEX IF NOT EXISTS idx_channel_monitor_templates_provider_api_mode + ON channel_monitor_request_templates (provider, api_mode); diff --git a/backend/migrations/139_seed_openai_monitor_templates.sql b/backend/migrations/139_seed_openai_monitor_templates.sql new file mode 100644 index 00000000..6326cd0a --- /dev/null +++ b/backend/migrations/139_seed_openai_monitor_templates.sql @@ -0,0 +1,47 @@ +-- Migration: 138_seed_openai_monitor_templates +-- 内置 OpenAI 渠道监控模板。重点是把协议模式显式化: +-- 1) OpenAI-compatible 使用 Chat Completions payload +-- 2) Responses / 本站自检 使用 Responses payload,默认 body 由后端 adapter 填入 instructions + input +-- 所有模板都可直接选择;ON CONFLICT 保证重复部署不覆盖用户编辑。 + +INSERT INTO channel_monitor_request_templates ( + name, provider, api_mode, description, extra_headers, body_override_mode, body_override +) +VALUES +( + 'OpenAI Compatible 默认检测', + 'openai', + 'chat_completions', + '适用于大多数 OpenAI-compatible 上游:POST /v1/chat/completions,后端自动生成 messages 数学 challenge。', + '{}'::jsonb, + 'off', + NULL +), +( + 'OpenAI Compatible 低 token 检测', + 'openai', + 'chat_completions', + '仍走 /v1/chat/completions,仅把 max_tokens 调低;model/messages/stream 由后端保护,避免误伤 challenge。', + '{}'::jsonb, + 'merge', + '{"max_tokens": 20}'::jsonb +), +( + 'OpenAI Responses / 本站自检', + 'openai', + 'responses', + '适用于本站或原生 Responses API:POST /v1/responses,默认 payload 自动带 instructions 与 input,避免 Instructions are required。', + '{}'::jsonb, + 'off', + NULL +), +( + 'OpenAI Responses 低 token 检测', + 'openai', + 'responses', + '仍走 /v1/responses,仅把 max_output_tokens 调低;instructions/input/model/stream 由后端保护。', + '{}'::jsonb, + 'merge', + '{"max_output_tokens": 20}'::jsonb +) +ON CONFLICT (provider, name) DO NOTHING; diff --git a/frontend/src/api/admin/channelMonitor.ts b/frontend/src/api/admin/channelMonitor.ts index 949c4bc8..bdef7d33 100644 --- a/frontend/src/api/admin/channelMonitor.ts +++ b/frontend/src/api/admin/channelMonitor.ts @@ -8,11 +8,13 @@ import { apiClient } from '../client' export type Provider = 'openai' | 'anthropic' | 'gemini' export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error' export type BodyOverrideMode = 'off' | 'merge' | 'replace' +export type APIMode = 'chat_completions' | 'responses' export interface ChannelMonitor { id: number name: string provider: Provider + api_mode: APIMode endpoint: string api_key_masked: string /** @@ -70,6 +72,7 @@ export interface ListResponse { export interface CreateParams { name: string provider: Provider + api_mode?: APIMode endpoint: string api_key: string primary_model: string diff --git a/frontend/src/api/admin/channelMonitorTemplate.ts b/frontend/src/api/admin/channelMonitorTemplate.ts index 01b3c2d0..7b048104 100644 --- a/frontend/src/api/admin/channelMonitorTemplate.ts +++ b/frontend/src/api/admin/channelMonitorTemplate.ts @@ -6,12 +6,13 @@ */ import { apiClient } from '../client' -import type { BodyOverrideMode, Provider } from './channelMonitor' +import type { APIMode, BodyOverrideMode, Provider } from './channelMonitor' export interface ChannelMonitorTemplate { id: number name: string provider: Provider + api_mode: APIMode description: string extra_headers: Record body_override_mode: BodyOverrideMode @@ -24,6 +25,7 @@ export interface ChannelMonitorTemplate { export interface ListParams { provider?: Provider + api_mode?: APIMode } export interface ListResponse { @@ -33,6 +35,7 @@ export interface ListResponse { export interface CreateParams { name: string provider: Provider + api_mode?: APIMode description?: string extra_headers?: Record body_override_mode?: BodyOverrideMode @@ -41,6 +44,7 @@ export interface CreateParams { export interface UpdateParams { name?: string + api_mode?: APIMode description?: string extra_headers?: Record body_override_mode?: BodyOverrideMode @@ -55,6 +59,7 @@ export interface AssociatedMonitorBrief { id: number name: string provider: Provider + api_mode: APIMode enabled: boolean } diff --git a/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue b/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue index 0d6b4ace..404b6916 100644 --- a/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue +++ b/frontend/src/components/admin/monitor/MonitorAdvancedRequestConfig.vue @@ -106,9 +106,15 @@ diff --git a/frontend/src/constants/channelMonitor.ts b/frontend/src/constants/channelMonitor.ts index 7523a878..eb99680c 100644 --- a/frontend/src/constants/channelMonitor.ts +++ b/frontend/src/constants/channelMonitor.ts @@ -7,18 +7,26 @@ * `useChannelMonitorFormat`. */ -import type { Provider, MonitorStatus } from '@/api/admin/channelMonitor' +import type { APIMode, Provider, MonitorStatus } from '@/api/admin/channelMonitor' export const PROVIDER_OPENAI: Provider = 'openai' export const PROVIDER_ANTHROPIC: Provider = 'anthropic' export const PROVIDER_GEMINI: Provider = 'gemini' +export const API_MODE_CHAT_COMPLETIONS: APIMode = 'chat_completions' +export const API_MODE_RESPONSES: APIMode = 'responses' + export const PROVIDERS: readonly Provider[] = [ PROVIDER_OPENAI, PROVIDER_ANTHROPIC, PROVIDER_GEMINI, ] +export const API_MODES: readonly APIMode[] = [ + API_MODE_CHAT_COMPLETIONS, + API_MODE_RESPONSES, +] + export const STATUS_OPERATIONAL: MonitorStatus = 'operational' export const STATUS_DEGRADED: MonitorStatus = 'degraded' export const STATUS_FAILED: MonitorStatus = 'failed' diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1eea64a4..3048c0e6 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2627,6 +2627,11 @@ export default { name: 'Name', namePlaceholder: 'Enter monitor name', provider: 'Platform', + apiMode: 'OpenAI protocol', + apiModeChatCompletions: 'OpenAI Compatible', + apiModeChatCompletionsHint: 'Use /v1/chat/completions with messages; works for most compatible providers.', + apiModeResponses: 'Responses API', + apiModeResponsesHint: 'Use /v1/responses with default instructions + input; best for self-check/Codex paths.', endpoint: 'Endpoint', endpointPlaceholder: 'https://api.example.com', useCurrentDomain: 'Use current service', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 91565bca..fe478c55 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2704,6 +2704,11 @@ export default { name: '名称', namePlaceholder: '输入监控名称', provider: '平台', + apiMode: 'OpenAI 协议', + apiModeChatCompletions: 'OpenAI Compatible', + apiModeChatCompletionsHint: '使用 /v1/chat/completions,发送 messages;适合大多数兼容站。', + apiModeResponses: 'Responses API', + apiModeResponsesHint: '使用 /v1/responses,默认带 instructions + input;适合本站自检/Codex。', endpoint: '上游地址', endpointPlaceholder: 'https://api.example.com', useCurrentDomain: '使用当前服务',