Merge pull request #2585 from Arron196/feature/channel-monitor-openai-detection

优化渠道监控 OpenAI 检测协议与内置模板
This commit is contained in:
Wesley Liddick 2026-05-20 08:50:44 +08:00 committed by GitHub
commit 03730fbcf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 1318 additions and 61 deletions

View File

@ -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(", ")

View File

@ -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()

View File

@ -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))

View File

@ -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) {

View File

@ -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)
}

View File

@ -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(", ")

View File

@ -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()

View File

@ -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))

View File

@ -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) {

View File

@ -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)
}

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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"),
}

View File

@ -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"),
}
}

View File

@ -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,

View File

@ -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})

View File

@ -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{}

View File

@ -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,

View File

@ -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、错误。

View File

@ -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)

View File

@ -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]",
)

View File

@ -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
}

View File

@ -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 {

View File

@ -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",
)

View File

@ -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

View File

@ -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)
}

View File

@ -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);

View File

@ -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 APIPOST /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;

View File

@ -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

View File

@ -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<string, string>
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<string, string>
body_override_mode?: BodyOverrideMode
@ -41,6 +44,7 @@ export interface CreateParams {
export interface UpdateParams {
name?: string
api_mode?: APIMode
description?: string
extra_headers?: Record<string, string>
body_override_mode?: BodyOverrideMode
@ -55,6 +59,7 @@ export interface AssociatedMonitorBrief {
id: number
name: string
provider: Provider
api_mode: APIMode
enabled: boolean
}

View File

@ -106,9 +106,15 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { BodyOverrideMode } from '@/api/admin/channelMonitor'
import type { APIMode, BodyOverrideMode, Provider } from '@/api/admin/channelMonitor'
import {
API_MODE_RESPONSES,
PROVIDER_OPENAI,
} from '@/constants/channelMonitor'
const props = defineProps<{
provider?: Provider
apiMode?: APIMode
extraHeaders: Record<string, string>
bodyOverrideMode: BodyOverrideMode
bodyOverride: Record<string, unknown> | null
@ -293,6 +299,18 @@ const bodyModeHint = computed(() => {
})
const bodyPlaceholder = computed(() => {
if (props.provider === PROVIDER_OPENAI && props.apiMode === API_MODE_RESPONSES) {
if (props.bodyOverrideMode === 'merge') {
return '{\n "max_output_tokens": 20\n}'
}
return '{\n "model": "gpt-4o-mini",\n "instructions": "You are a health check endpoint. Reply briefly.",\n "input": "Reply with exactly: ok",\n "max_output_tokens": 20,\n "stream": false\n}'
}
if (props.provider === PROVIDER_OPENAI) {
if (props.bodyOverrideMode === 'merge') {
return '{\n "max_tokens": 20\n}'
}
return '{\n "model": "gpt-4o-mini",\n "messages": [{"role":"user","content":"Reply with exactly: ok"}],\n "max_tokens": 20,\n "stream": false\n}'
}
if (props.bodyOverrideMode === 'merge') {
return '{\n "system": "You are Claude Code..."\n}'
}

View File

@ -29,6 +29,24 @@
</div>
</div>
<div v-if="form.provider === PROVIDER_OPENAI" class="rounded-lg border border-blue-100 bg-blue-50/50 p-3 dark:border-blue-500/20 dark:bg-blue-500/10">
<label class="input-label">{{ t('admin.channelMonitor.form.apiMode') }}</label>
<div class="grid gap-3 sm:grid-cols-2">
<button
v-for="opt in apiModeOptions"
:key="opt.value"
type="button"
:aria-pressed="form.api_mode === opt.value"
class="rounded-lg border-2 px-3 py-2 text-left transition-colors"
:class="apiModeButtonClass(opt.value)"
@click="form.api_mode = opt.value"
>
<span class="block text-sm font-semibold">{{ opt.label }}</span>
<span class="mt-0.5 block text-xs opacity-80">{{ opt.hint }}</span>
</button>
</div>
</div>
<div>
<label class="input-label">{{ t('admin.channelMonitor.form.endpoint') }} <span class="text-red-500">*</span></label>
<div class="flex gap-2">
@ -115,6 +133,8 @@
</div>
<MonitorAdvancedRequestConfig
:provider="form.provider"
:api-mode="form.api_mode"
:extra-headers="form.extra_headers"
:body-override-mode="form.body_override_mode"
:body-override="form.body_override"
@ -168,6 +188,7 @@ import type {
BodyOverrideMode,
ChannelMonitor,
CreateParams,
APIMode,
Provider,
UpdateParams,
} from '@/api/admin/channelMonitor'
@ -186,6 +207,8 @@ import {
PROVIDER_OPENAI,
PROVIDER_ANTHROPIC,
PROVIDER_GEMINI,
API_MODE_CHAT_COMPLETIONS,
API_MODE_RESPONSES,
DEFAULT_INTERVAL_SECONDS,
} from '@/constants/channelMonitor'
@ -224,6 +247,7 @@ const userGroupRates = ref<Record<number, number>>({})
interface MonitorForm {
name: string
provider: Provider
api_mode: APIMode
endpoint: string
api_key: string
primary_model: string
@ -241,6 +265,7 @@ interface MonitorForm {
const form = reactive<MonitorForm>({
name: '',
provider: PROVIDER_ANTHROPIC,
api_mode: API_MODE_CHAT_COMPLETIONS,
endpoint: '',
api_key: '',
primary_model: '',
@ -254,15 +279,21 @@ const form = reactive<MonitorForm>({
body_override: null,
})
// dialog cache provider
let suppressFormWatchers = false
// dialog cache provider / api mode
const templatesCache = ref<ChannelMonitorTemplate[]>([])
const templatesLoading = ref(false)
const templateOptions = computed(() => {
const items = templatesCache.value.filter((t) => t.provider === form.provider)
const items = templatesCache.value.filter((t) => {
if (t.provider !== form.provider) return false
if (form.provider !== PROVIDER_OPENAI) return true
return normalizeAPIMode(t.api_mode) === form.api_mode
})
return [
{ value: '', label: t('admin.channelMonitor.templateField.none') },
...items.map((t) => ({ value: String(t.id), label: t.name })),
...items.map((t) => ({ value: String(t.id), label: templateOptionLabel(t) })),
]
})
@ -294,13 +325,57 @@ const templateSelectValue = computed<string>({
// =
const tpl = templatesCache.value.find((t) => t.id === id)
if (tpl) {
suppressFormWatchers = true
form.api_mode = normalizeAPIMode(tpl.api_mode)
form.template_id = id
form.extra_headers = { ...(tpl.extra_headers || {}) }
form.body_override_mode = tpl.body_override_mode
form.body_override = tpl.body_override ? { ...tpl.body_override } : null
suppressFormWatchers = false
}
},
})
const apiModeOptions = computed<{ value: APIMode; label: string; hint: string }[]>(() => [
{
value: API_MODE_CHAT_COMPLETIONS,
label: t('admin.channelMonitor.form.apiModeChatCompletions'),
hint: t('admin.channelMonitor.form.apiModeChatCompletionsHint'),
},
{
value: API_MODE_RESPONSES,
label: t('admin.channelMonitor.form.apiModeResponses'),
hint: t('admin.channelMonitor.form.apiModeResponsesHint'),
},
])
function normalizeAPIMode(mode: APIMode | undefined | null): APIMode {
return mode === API_MODE_RESPONSES ? API_MODE_RESPONSES : API_MODE_CHAT_COMPLETIONS
}
function apiModeButtonClass(mode: APIMode): string {
const active = form.api_mode === mode
if (active) {
return 'border-primary-500 bg-white text-primary-700 shadow-sm dark:border-primary-400 dark:bg-primary-500/15 dark:text-primary-300'
}
return 'border-blue-100 bg-white/70 text-gray-600 hover:border-primary-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400'
}
function templateOptionLabel(tpl: ChannelMonitorTemplate): string {
if (tpl.provider !== PROVIDER_OPENAI) return tpl.name
const labelKey = normalizeAPIMode(tpl.api_mode) === API_MODE_RESPONSES
? 'admin.channelMonitor.form.apiModeResponses'
: 'admin.channelMonitor.form.apiModeChatCompletions'
return `${tpl.name} · ${t(labelKey)}`
}
function clearRequestSnapshot() {
form.template_id = null
form.extra_headers = {}
form.body_override_mode = 'off'
form.body_override = null
}
interface ProviderOption {
value: Provider
label: string
@ -318,13 +393,26 @@ const providerOptions = computed<ProviderOption[]>(() => [
// picks a new key.
// template_id provider
watch(() => form.provider, () => {
if (suppressFormWatchers) return
form.api_key = ''
form.template_id = null
})
if (form.provider !== PROVIDER_OPENAI) {
form.api_mode = API_MODE_CHAT_COMPLETIONS
}
clearRequestSnapshot()
}, { flush: 'sync' })
watch(() => form.api_mode, () => {
if (suppressFormWatchers) return
if (form.provider === PROVIDER_OPENAI) {
clearRequestSnapshot()
}
}, { flush: 'sync' })
function resetForm() {
suppressFormWatchers = true
form.name = ''
form.provider = PROVIDER_ANTHROPIC
form.api_mode = API_MODE_CHAT_COMPLETIONS
form.endpoint = ''
form.api_key = ''
form.primary_model = ''
@ -336,11 +424,14 @@ function resetForm() {
form.extra_headers = {}
form.body_override_mode = 'off'
form.body_override = null
suppressFormWatchers = false
}
function loadFromMonitor(m: ChannelMonitor) {
suppressFormWatchers = true
form.name = m.name
form.provider = m.provider
form.api_mode = normalizeAPIMode(m.api_mode)
form.endpoint = m.endpoint
form.api_key = ''
form.primary_model = m.primary_model
@ -352,6 +443,7 @@ function loadFromMonitor(m: ChannelMonitor) {
form.extra_headers = { ...(m.extra_headers || {}) }
form.body_override_mode = m.body_override_mode || 'off'
form.body_override = m.body_override ? { ...m.body_override } : null
suppressFormWatchers = false
}
// Re-sync form whenever the dialog is opened or the target monitor changes.
@ -404,6 +496,7 @@ function buildPayload(): CreateParams {
return {
name: form.name.trim(),
provider: form.provider,
api_mode: form.provider === PROVIDER_OPENAI ? form.api_mode : API_MODE_CHAT_COMPLETIONS,
endpoint: form.endpoint.trim(),
api_key: form.api_key.trim(),
primary_model: form.primary_model.trim(),

View File

@ -56,6 +56,7 @@
/>
<span class="font-medium text-gray-900 dark:text-white">{{ m.name }}</span>
<span class="text-xs text-gray-400">{{ m.provider }}</span>
<span v-if="m.provider === 'openai'" class="text-xs text-gray-400">{{ m.api_mode }}</span>
<span
v-if="!m.enabled"
class="ml-auto rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 dark:bg-dark-700 dark:text-gray-400"

View File

@ -65,6 +65,13 @@
>
{{ modeLabel(tpl.body_override_mode) }}
</span>
<span
v-if="tpl.provider === PROVIDER_OPENAI"
class="inline-flex items-center rounded-md px-1.5 py-0.5 text-xs"
:class="apiModeBadgeClass(tpl.api_mode)"
>
{{ apiModeLabel(tpl.api_mode) }}
</span>
<span
v-if="tpl.associated_monitors > 0"
class="text-xs text-gray-500 dark:text-gray-400"
@ -137,6 +144,23 @@
</div>
</div>
<div v-if="form.provider === PROVIDER_OPENAI" class="rounded-lg border border-blue-100 bg-blue-50/50 p-3 dark:border-blue-500/20 dark:bg-blue-500/10">
<label class="input-label">{{ t('admin.channelMonitor.form.apiMode') }}</label>
<div class="grid gap-3 sm:grid-cols-2">
<button
v-for="opt in apiModeOptions"
:key="opt.value"
type="button"
class="rounded-lg border-2 px-3 py-2 text-left transition-colors"
:class="apiModeButtonClass(opt.value)"
@click="form.api_mode = opt.value"
>
<span class="block text-sm font-semibold">{{ opt.label }}</span>
<span class="mt-0.5 block text-xs opacity-80">{{ opt.hint }}</span>
</button>
</div>
</div>
<div>
<label class="input-label">
{{ t('admin.channelMonitor.template.form.description') }}
@ -150,6 +174,8 @@
</div>
<MonitorAdvancedRequestConfig
:provider="form.provider"
:api-mode="form.api_mode"
:extra-headers="form.extra_headers"
:body-override-mode="form.body_override_mode"
:body-override="form.body_override"
@ -207,6 +233,7 @@ import { useAppStore } from '@/stores/app'
import { extractApiErrorMessage } from '@/utils/apiError'
import { adminAPI } from '@/api/admin'
import type {
APIMode,
BodyOverrideMode,
Provider,
} from '@/api/admin/channelMonitor'
@ -221,6 +248,8 @@ import {
PROVIDER_ANTHROPIC,
PROVIDER_OPENAI,
PROVIDER_GEMINI,
API_MODE_CHAT_COMPLETIONS,
API_MODE_RESPONSES,
} from '@/constants/channelMonitor'
const props = defineProps<{ show: boolean }>()
@ -263,6 +292,7 @@ interface TemplateForm {
id: number | null
name: string
provider: Provider
api_mode: APIMode
description: string
extra_headers: Record<string, string>
body_override_mode: BodyOverrideMode
@ -278,6 +308,7 @@ function emptyForm(provider: Provider): TemplateForm {
id: null,
name: '',
provider,
api_mode: API_MODE_CHAT_COMPLETIONS,
description: '',
extra_headers: {},
body_override_mode: 'off',
@ -289,6 +320,7 @@ function loadForm(tpl: ChannelMonitorTemplate) {
form.id = tpl.id
form.name = tpl.name
form.provider = tpl.provider
form.api_mode = normalizeAPIMode(tpl.api_mode)
form.description = tpl.description
form.extra_headers = { ...(tpl.extra_headers || {}) }
form.body_override_mode = tpl.body_override_mode
@ -346,6 +378,7 @@ async function handleSubmit() {
await adminAPI.channelMonitorTemplate.create({
name: form.name.trim(),
provider: form.provider,
api_mode: form.provider === PROVIDER_OPENAI ? form.api_mode : API_MODE_CHAT_COMPLETIONS,
description: form.description.trim(),
extra_headers: form.extra_headers,
body_override_mode: form.body_override_mode,
@ -355,6 +388,7 @@ async function handleSubmit() {
} else if (typeof editing.value === 'number') {
await adminAPI.channelMonitorTemplate.update(editing.value, {
name: form.name.trim(),
api_mode: form.provider === PROVIDER_OPENAI ? form.api_mode : API_MODE_CHAT_COMPLETIONS,
description: form.description.trim(),
extra_headers: form.extra_headers,
body_override_mode: form.body_override_mode,
@ -444,4 +478,48 @@ function modeBadgeClass(mode: BodyOverrideMode): string {
function modeLabel(mode: BodyOverrideMode): string {
return t(`admin.channelMonitor.advanced.bodyMode${mode.charAt(0).toUpperCase()}${mode.slice(1)}`)
}
const apiModeOptions = computed<{ value: APIMode; label: string; hint: string }[]>(() => [
{
value: API_MODE_CHAT_COMPLETIONS,
label: t('admin.channelMonitor.form.apiModeChatCompletions'),
hint: t('admin.channelMonitor.form.apiModeChatCompletionsHint'),
},
{
value: API_MODE_RESPONSES,
label: t('admin.channelMonitor.form.apiModeResponses'),
hint: t('admin.channelMonitor.form.apiModeResponsesHint'),
},
])
watch(() => form.provider, (provider) => {
if (provider !== PROVIDER_OPENAI) {
form.api_mode = API_MODE_CHAT_COMPLETIONS
}
})
function normalizeAPIMode(mode: APIMode | undefined | null): APIMode {
return mode === API_MODE_RESPONSES ? API_MODE_RESPONSES : API_MODE_CHAT_COMPLETIONS
}
function apiModeButtonClass(mode: APIMode): string {
const active = form.api_mode === mode
if (active) {
return 'border-primary-500 bg-white text-primary-700 shadow-sm dark:border-primary-400 dark:bg-primary-500/15 dark:text-primary-300'
}
return 'border-blue-100 bg-white/70 text-gray-600 hover:border-primary-300 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400'
}
function apiModeLabel(mode: APIMode): string {
return normalizeAPIMode(mode) === API_MODE_RESPONSES
? t('admin.channelMonitor.form.apiModeResponses')
: t('admin.channelMonitor.form.apiModeChatCompletions')
}
function apiModeBadgeClass(mode: APIMode): string {
if (normalizeAPIMode(mode) === API_MODE_RESPONSES) {
return 'bg-blue-100 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300'
}
return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300'
}
</script>

View File

@ -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'

View File

@ -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',

View File

@ -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: '使用当前服务',