Przeglądaj źródła

feat: web search adaptor plugin (#216)

* feat: web search adaptor plugin

* fix: ci lint

* fix: remove web_search_options field on web search plugin

* docs: add web search docs
zijiren 7 miesięcy temu
rodzic
commit
25044cd2fb
100 zmienionych plików z 2118 dodań i 239 usunięć
  1. 7 3
      core/common/gin.go
  2. 5 5
      core/controller/channel-test.go
  3. 1 1
      core/controller/dashboard.go
  4. 8 8
      core/controller/model.go
  5. 3 3
      core/controller/modelconfig.go
  6. 53 11
      core/controller/relay-controller.go
  7. 4 4
      core/middleware/distributor.go
  8. 13 13
      core/model/cache.go
  9. 36 23
      core/model/modelconfig.go
  10. 1 1
      core/relay/adaptor/ai360/adaptor.go
  11. 1 1
      core/relay/adaptor/ai360/constants.go
  12. 1 1
      core/relay/adaptor/ali/adaptor.go
  13. 1 1
      core/relay/adaptor/ali/constants.go
  14. 1 1
      core/relay/adaptor/anthropic/adaptor.go
  15. 1 1
      core/relay/adaptor/anthropic/constants.go
  16. 3 0
      core/relay/adaptor/anthropic/openai.go
  17. 2 2
      core/relay/adaptor/aws/adaptor.go
  18. 3 3
      core/relay/adaptor/aws/registry.go
  19. 1 1
      core/relay/adaptor/baichuan/adaptor.go
  20. 1 1
      core/relay/adaptor/baichuan/constants.go
  21. 1 1
      core/relay/adaptor/baidu/adaptor.go
  22. 1 1
      core/relay/adaptor/baidu/constants.go
  23. 1 1
      core/relay/adaptor/baiduv2/adaptor.go
  24. 1 1
      core/relay/adaptor/baiduv2/constants.go
  25. 1 1
      core/relay/adaptor/cloudflare/adaptor.go
  26. 1 1
      core/relay/adaptor/cloudflare/constant.go
  27. 1 1
      core/relay/adaptor/cohere/adaptor.go
  28. 1 1
      core/relay/adaptor/cohere/constant.go
  29. 1 1
      core/relay/adaptor/coze/adaptor.go
  30. 1 1
      core/relay/adaptor/coze/constants.go
  31. 1 1
      core/relay/adaptor/deepseek/adaptor.go
  32. 1 1
      core/relay/adaptor/deepseek/constants.go
  33. 1 1
      core/relay/adaptor/doc2x/adaptor.go
  34. 1 1
      core/relay/adaptor/doc2x/constants.go
  35. 1 1
      core/relay/adaptor/doubao/constants.go
  36. 1 1
      core/relay/adaptor/doubao/main.go
  37. 1 1
      core/relay/adaptor/doubaoaudio/constants.go
  38. 1 1
      core/relay/adaptor/doubaoaudio/main.go
  39. 1 1
      core/relay/adaptor/gemini/adaptor.go
  40. 1 1
      core/relay/adaptor/gemini/constants.go
  41. 1 1
      core/relay/adaptor/geminiopenai/adaptor.go
  42. 1 1
      core/relay/adaptor/groq/adaptor.go
  43. 1 1
      core/relay/adaptor/groq/constants.go
  44. 23 61
      core/relay/adaptor/interface.go
  45. 1 1
      core/relay/adaptor/jina/adaptor.go
  46. 1 1
      core/relay/adaptor/jina/constants.go
  47. 1 1
      core/relay/adaptor/lingyiwanwu/adaptor.go
  48. 1 1
      core/relay/adaptor/lingyiwanwu/constants.go
  49. 1 1
      core/relay/adaptor/minimax/adaptor.go
  50. 1 1
      core/relay/adaptor/minimax/constants.go
  51. 1 1
      core/relay/adaptor/mistral/adaptor.go
  52. 1 1
      core/relay/adaptor/mistral/constants.go
  53. 1 1
      core/relay/adaptor/moonshot/adaptor.go
  54. 1 1
      core/relay/adaptor/moonshot/constants.go
  55. 1 1
      core/relay/adaptor/novita/adaptor.go
  56. 1 1
      core/relay/adaptor/novita/constants.go
  57. 1 1
      core/relay/adaptor/ollama/adaptor.go
  58. 1 1
      core/relay/adaptor/ollama/constants.go
  59. 1 1
      core/relay/adaptor/openai/adaptor.go
  60. 1 1
      core/relay/adaptor/openai/constants.go
  61. 1 1
      core/relay/adaptor/openrouter/adaptor.go
  62. 1 1
      core/relay/adaptor/siliconflow/adaptor.go
  63. 1 1
      core/relay/adaptor/siliconflow/constants.go
  64. 1 1
      core/relay/adaptor/stepfun/adaptor.go
  65. 1 1
      core/relay/adaptor/stepfun/constants.go
  66. 1 1
      core/relay/adaptor/tencent/adaptor.go
  67. 1 1
      core/relay/adaptor/tencent/constants.go
  68. 1 1
      core/relay/adaptor/text-embeddings-inference/adaptor.go
  69. 1 1
      core/relay/adaptor/text-embeddings-inference/constants.go
  70. 63 0
      core/relay/adaptor/utils.go
  71. 1 1
      core/relay/adaptor/vertexai/adaptor.go
  72. 1 1
      core/relay/adaptor/vertexai/claude/constants.go
  73. 1 1
      core/relay/adaptor/vertexai/registry.go
  74. 1 1
      core/relay/adaptor/xai/adaptor.go
  75. 1 1
      core/relay/adaptor/xai/constants.go
  76. 1 1
      core/relay/adaptor/xunfei/adaptor.go
  77. 1 1
      core/relay/adaptor/xunfei/constants.go
  78. 1 1
      core/relay/adaptor/zhipu/adaptor.go
  79. 1 1
      core/relay/adaptor/zhipu/constants.go
  80. 2 2
      core/relay/controller/anthropic.go
  81. 2 2
      core/relay/controller/chat.go
  82. 2 2
      core/relay/controller/completions.go
  83. 2 2
      core/relay/controller/edits.go
  84. 2 2
      core/relay/controller/embed.go
  85. 3 3
      core/relay/controller/image.go
  86. 2 2
      core/relay/controller/pdf.go
  87. 2 2
      core/relay/controller/rerank.go
  88. 2 2
      core/relay/controller/stt.go
  89. 2 2
      core/relay/controller/tts.go
  90. 28 16
      core/relay/meta/meta.go
  91. 35 0
      core/relay/plugin/noop/noop.go
  92. 67 0
      core/relay/plugin/types.go
  93. 247 0
      core/relay/plugin/web-search/README.md
  94. 247 0
      core/relay/plugin/web-search/README.zh.md
  95. 214 0
      core/relay/plugin/web-search/prompts/arxiv.md
  96. 39 0
      core/relay/plugin/web-search/prompts/chinese-internet.md
  97. 217 0
      core/relay/plugin/web-search/prompts/full.md
  98. 41 0
      core/relay/plugin/web-search/prompts/internet.md
  99. 51 0
      core/relay/plugin/web-search/prompts/private.md
  100. 622 0
      core/relay/plugin/web-search/search.go

+ 7 - 3
core/common/gin.go

@@ -40,6 +40,12 @@ func (l *LimitedReader) Read(p []byte) (n int, err error) {
 	return
 }
 
+func SetRequestBody(req *http.Request, body []byte) {
+	ctx := req.Context()
+	bufCtx := context.WithValue(ctx, RequestBodyKey{}, body)
+	*req = *req.WithContext(bufCtx)
+}
+
 func GetRequestBody(req *http.Request) ([]byte, error) {
 	contentType := req.Header.Get("Content-Type")
 	if contentType == "application/x-www-form-urlencoded" ||
@@ -78,9 +84,7 @@ func GetRequestBody(req *http.Request) ([]byte, error) {
 	if err != nil {
 		return nil, fmt.Errorf("request body read failed: %w", err)
 	}
-	ctx := req.Context()
-	bufCtx := context.WithValue(ctx, RequestBodyKey{}, buf)
-	*req = *req.WithContext(bufCtx)
+	SetRequestBody(req, buf)
 	return buf, nil
 }
 

+ 5 - 5
core/controller/channel-test.go

@@ -33,11 +33,11 @@ import (
 const channelTestRequestID = "channel-test"
 
 var (
-	modelConfigCache     map[string]*model.ModelConfig = make(map[string]*model.ModelConfig)
+	modelConfigCache     map[string]model.ModelConfig = make(map[string]model.ModelConfig)
 	modelConfigCacheOnce sync.Once
 )
 
-func guessModelConfig(model string) *model.ModelConfig {
+func guessModelConfig(modelName string) model.ModelConfig {
 	modelConfigCacheOnce.Do(func() {
 		for _, c := range adaptors.ChannelAdaptor {
 			for _, m := range c.GetModelList() {
@@ -48,10 +48,10 @@ func guessModelConfig(model string) *model.ModelConfig {
 		}
 	})
 
-	if cachedConfig, ok := modelConfigCache[model]; ok {
+	if cachedConfig, ok := modelConfigCache[modelName]; ok {
 		return cachedConfig
 	}
-	return nil
+	return model.ModelConfig{}
 }
 
 // testSingleModel tests a single model in the channel
@@ -62,7 +62,7 @@ func testSingleModel(mc *model.ModelCaches, channel *model.Channel, modelName st
 	}
 	if modelConfig.Type == mode.Unknown {
 		newModelConfig := guessModelConfig(modelName)
-		if newModelConfig != nil {
+		if newModelConfig.Type != mode.Unknown {
 			modelConfig = newModelConfig
 		}
 	}

+ 1 - 1
core/controller/dashboard.go

@@ -270,7 +270,7 @@ func GetGroupDashboardModels(c *gin.Context) {
 			}) {
 				continue
 			}
-			newEnabledModelConfigs = append(newEnabledModelConfigs, middleware.GetGroupAdjustedModelConfig(groupCache, *mc))
+			newEnabledModelConfigs = append(newEnabledModelConfigs, middleware.GetGroupAdjustedModelConfig(groupCache, mc))
 		}
 	}
 	middleware.SuccessResponse(c, newEnabledModelConfigs)

+ 8 - 8
core/controller/model.go

@@ -55,14 +55,14 @@ func (c *BuiltinModelConfig) MarshalJSON() ([]byte, error) {
 	})
 }
 
-func SortBuiltinModelConfigsFunc(i, j *BuiltinModelConfig) int {
-	return model.SortModelConfigsFunc((*model.ModelConfig)(i), (*model.ModelConfig)(j))
+func SortBuiltinModelConfigsFunc(i, j BuiltinModelConfig) int {
+	return model.SortModelConfigsFunc((model.ModelConfig)(i), (model.ModelConfig)(j))
 }
 
 var (
-	builtinModels             []*BuiltinModelConfig
+	builtinModels             []BuiltinModelConfig
 	builtinModelsMap          map[string]*OpenAIModels
-	builtinChannelType2Models map[model.ChannelType][]*BuiltinModelConfig
+	builtinChannelType2Models map[model.ChannelType][]BuiltinModelConfig
 )
 
 var permission = []OpenAIModelPermission{
@@ -83,12 +83,12 @@ var permission = []OpenAIModelPermission{
 }
 
 func init() {
-	builtinChannelType2Models = make(map[model.ChannelType][]*BuiltinModelConfig)
+	builtinChannelType2Models = make(map[model.ChannelType][]BuiltinModelConfig)
 	builtinModelsMap = make(map[string]*OpenAIModels)
 	// https://platform.openai.com/docs/models/model-endpoint-compatibility
 	for i, adaptor := range adaptors.ChannelAdaptor {
 		modelNames := adaptor.GetModelList()
-		builtinChannelType2Models[i] = make([]*BuiltinModelConfig, len(modelNames))
+		builtinChannelType2Models[i] = make([]BuiltinModelConfig, len(modelNames))
 		for idx, _model := range modelNames {
 			if _model.Owner == "" {
 				_model.Owner = model.ModelOwner(i.String())
@@ -103,11 +103,11 @@ func init() {
 					Root:       _model.Model,
 					Parent:     nil,
 				}
-				builtinModels = append(builtinModels, (*BuiltinModelConfig)(_model))
+				builtinModels = append(builtinModels, (BuiltinModelConfig)(_model))
 			} else if v.OwnedBy != string(_model.Owner) {
 				log.Fatalf("model %s owner mismatch, expect %s, actual %s", _model.Model, string(_model.Owner), v.OwnedBy)
 			}
-			builtinChannelType2Models[i][idx] = (*BuiltinModelConfig)(_model)
+			builtinChannelType2Models[i][idx] = (BuiltinModelConfig)(_model)
 		}
 	}
 	for _, models := range builtinChannelType2Models {

+ 3 - 3
core/controller/modelconfig.go

@@ -112,7 +112,7 @@ func SearchModelConfigs(c *gin.Context) {
 type SaveModelConfigsRequest struct {
 	CreatedAt int64 `json:"created_at"`
 	UpdatedAt int64 `json:"updated_at"`
-	*model.ModelConfig
+	model.ModelConfig
 }
 
 // SaveModelConfigs godoc
@@ -126,12 +126,12 @@ type SaveModelConfigsRequest struct {
 //	@Success		200		{object}	middleware.APIResponse
 //	@Router			/api/model_configs/ [post]
 func SaveModelConfigs(c *gin.Context) {
-	var configs []*SaveModelConfigsRequest
+	var configs []SaveModelConfigsRequest
 	if err := c.ShouldBindJSON(&configs); err != nil {
 		middleware.ErrorResponse(c, http.StatusBadRequest, err.Error())
 		return
 	}
-	modelConfigs := make([]*model.ModelConfig, len(configs))
+	modelConfigs := make([]model.ModelConfig, len(configs))
 	for i, config := range configs {
 		modelConfigs[i] = config.ModelConfig
 	}

+ 53 - 11
core/controller/relay-controller.go

@@ -31,6 +31,8 @@ import (
 	"github.com/labring/aiproxy/core/relay/meta"
 	"github.com/labring/aiproxy/core/relay/mode"
 	relaymodel "github.com/labring/aiproxy/core/relay/model"
+	"github.com/labring/aiproxy/core/relay/plugin"
+	websearch "github.com/labring/aiproxy/core/relay/plugin/web-search"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -38,8 +40,8 @@ import (
 
 type (
 	RelayHandler    func(*gin.Context, *meta.Meta) *controller.HandleResult
-	GetRequestUsage func(*gin.Context, *model.ModelConfig) (model.Usage, error)
-	GetRequestPrice func(*gin.Context, *model.ModelConfig) (model.Price, error)
+	GetRequestUsage func(*gin.Context, model.ModelConfig) (model.Usage, error)
+	GetRequestPrice func(*gin.Context, model.ModelConfig) (model.Price, error)
 )
 
 type RelayController struct {
@@ -48,6 +50,7 @@ type RelayController struct {
 	Handler         RelayHandler
 }
 
+// TODO: convert to plugin
 type wrapAdaptor struct {
 	adaptor.Adaptor
 }
@@ -163,7 +166,13 @@ func relayHandler(c *gin.Context, meta *meta.Meta) *controller.HandleResult {
 		}
 	}
 
-	return controller.Handle(&wrapAdaptor{adaptor}, c, meta)
+	a := plugin.WrapperAdaptor(&wrapAdaptor{adaptor},
+		websearch.NewWebSearchPlugin(func(modelName string) (*model.Channel, error) {
+			return getWebSearchChannel(c, modelName)
+		}),
+	)
+
+	return controller.Handle(a, c, meta)
 }
 
 func relayController(m mode.Mode) RelayController {
@@ -318,9 +327,17 @@ var (
 
 func GetRandomChannel(mc *model.ModelCaches, availableSet []string, modelName string, errorRates map[int64]float64, ignoreChannel ...int64) (*model.Channel, []*model.Channel, error) {
 	channelMap := make(map[int]*model.Channel)
-	for _, set := range availableSet {
-		for _, channel := range mc.EnabledModel2ChannelsBySet[set][modelName] {
-			channelMap[channel.ID] = channel
+	if len(availableSet) != 0 {
+		for _, set := range availableSet {
+			for _, channel := range mc.EnabledModel2ChannelsBySet[set][modelName] {
+				channelMap[channel.ID] = channel
+			}
+		}
+	} else {
+		for _, sets := range mc.EnabledModel2ChannelsBySet {
+			for _, channel := range sets[modelName] {
+				channelMap[channel.ID] = channel
+			}
 		}
 	}
 	migratedChannels := make([]*model.Channel, 0, len(channelMap))
@@ -403,12 +420,11 @@ func NewMetaByContext(c *gin.Context, channel *model.Channel, mode mode.Mode, op
 }
 
 func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
-	log := middleware.GetLogger(c)
 	requestModel := middleware.GetRequestModel(c)
 	mc := middleware.GetModelConfig(c)
 
 	// Get initial channel
-	initialChannel, err := getInitialChannel(c, requestModel, log)
+	initialChannel, err := getInitialChannel(c, requestModel)
 	if err != nil || initialChannel == nil || initialChannel.channel == nil {
 		middleware.AbortLogWithMessageWithMode(mode, c,
 			http.StatusServiceUnavailable,
@@ -486,7 +502,7 @@ func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
 	)
 
 	// Retry loop
-	retryLoop(c, mode, retryState, relayController.Handler, log)
+	retryLoop(c, mode, retryState, relayController.Handler)
 }
 
 // recordResult records the consumption for the final result
@@ -572,7 +588,8 @@ type initialChannel struct {
 	migratedChannels  []*model.Channel
 }
 
-func getInitialChannel(c *gin.Context, modelName string, log *log.Entry) (*initialChannel, error) {
+func getInitialChannel(c *gin.Context, modelName string) (*initialChannel, error) {
+	log := middleware.GetLogger(c)
 	if channel := middleware.GetChannel(c); channel != nil {
 		log.Data["designated_channel"] = "true"
 		return &initialChannel{channel: channel, designatedChannel: true}, nil
@@ -607,6 +624,29 @@ func getInitialChannel(c *gin.Context, modelName string, log *log.Entry) (*initi
 	}, nil
 }
 
+func getWebSearchChannel(c *gin.Context, modelName string) (*model.Channel, error) {
+	log := middleware.GetLogger(c)
+	mc := middleware.GetModelCaches(c)
+
+	ids, err := monitor.GetBannedChannelsWithModel(c.Request.Context(), modelName)
+	if err != nil {
+		log.Errorf("get %s auto banned channels failed: %+v", modelName, err)
+	}
+	log.Debugf("%s model banned channels: %+v", modelName, ids)
+
+	errorRates, err := monitor.GetModelChannelErrorRate(c.Request.Context(), modelName)
+	if err != nil {
+		log.Errorf("get channel model error rates failed: %+v", err)
+	}
+
+	channel, _, err := getChannelWithFallback(mc, nil, modelName, errorRates, ids...)
+	if err != nil {
+		return nil, err
+	}
+
+	return channel, nil
+}
+
 func handleRelayResult(c *gin.Context, bizErr adaptor.Error, retry bool, retryTimes int) (done bool) {
 	if bizErr == nil {
 		return true
@@ -645,7 +685,9 @@ func initRetryState(retryTimes int, channel *initialChannel, meta *meta.Meta, re
 	return state
 }
 
-func retryLoop(c *gin.Context, mode mode.Mode, state *retryState, relayController RelayHandler, log *log.Entry) {
+func retryLoop(c *gin.Context, mode mode.Mode, state *retryState, relayController RelayHandler) {
+	log := middleware.GetLogger(c)
+
 	// do not use for i := range state.retryTimes, because the retryTimes is constant
 	i := 0
 

+ 4 - 4
core/middleware/distributor.go

@@ -132,10 +132,10 @@ func UpdateGroupModelTokennameTokensRequest(c *gin.Context, tpm, tps int64) {
 	// log.Data["tps"] = strconv.FormatInt(tps, 10)
 }
 
-func checkGroupModelRPMAndTPM(c *gin.Context, group *model.GroupCache, mc *model.ModelConfig, tokenName string) error {
+func checkGroupModelRPMAndTPM(c *gin.Context, group *model.GroupCache, mc model.ModelConfig, tokenName string) error {
 	log := GetLogger(c)
 
-	adjustedModelConfig := GetGroupAdjustedModelConfig(group, *mc)
+	adjustedModelConfig := GetGroupAdjustedModelConfig(group, mc)
 
 	groupModelCount, groupModelOverLimitCount, groupModelSecondCount := reqlimit.PushGroupModelRequest(c.Request.Context(), group.ID, mc.Model, adjustedModelConfig.RPM)
 	UpdateGroupModelRequest(c, group, groupModelCount+groupModelOverLimitCount, groupModelSecondCount)
@@ -461,8 +461,8 @@ func GetRequestMetadata(c *gin.Context) map[string]string {
 	return c.GetStringMapString(RequestMetadata)
 }
 
-func GetModelConfig(c *gin.Context) *model.ModelConfig {
-	return c.MustGet(ModelConfig).(*model.ModelConfig)
+func GetModelConfig(c *gin.Context) model.ModelConfig {
+	return c.MustGet(ModelConfig).(model.ModelConfig)
 }
 
 func NewMetaByContext(c *gin.Context,

+ 13 - 13
core/model/cache.go

@@ -672,7 +672,7 @@ func CacheGetPublicMCPReusingParam(mcpID, groupID string) (*PublicMCPReusingPara
 
 //nolint:revive
 type ModelConfigCache interface {
-	GetModelConfig(model string) (*ModelConfig, bool)
+	GetModelConfig(model string) (ModelConfig, bool)
 }
 
 // read-only cache
@@ -684,9 +684,9 @@ type ModelCaches struct {
 	// map[set][]model
 	EnabledModelsBySet map[string][]string
 	// map[set][]modelconfig
-	EnabledModelConfigsBySet map[string][]*ModelConfig
+	EnabledModelConfigsBySet map[string][]ModelConfig
 	// map[model]modelconfig
-	EnabledModelConfigsMap map[string]*ModelConfig
+	EnabledModelConfigsMap map[string]ModelConfig
 
 	// map[set]map[model][]channel
 	EnabledModel2ChannelsBySet map[string]map[string][]*Channel
@@ -811,10 +811,10 @@ func LoadChannelByID(id int) (*Channel, error) {
 var _ ModelConfigCache = (*modelConfigMapCache)(nil)
 
 type modelConfigMapCache struct {
-	modelConfigMap map[string]*ModelConfig
+	modelConfigMap map[string]ModelConfig
 }
 
-func (m *modelConfigMapCache) GetModelConfig(model string) (*ModelConfig, bool) {
+func (m *modelConfigMapCache) GetModelConfig(model string) (ModelConfig, bool) {
 	config, ok := m.modelConfigMap[model]
 	return config, ok
 }
@@ -825,7 +825,7 @@ type disabledModelConfigCache struct {
 	modelConfigs ModelConfigCache
 }
 
-func (d *disabledModelConfigCache) GetModelConfig(model string) (*ModelConfig, bool) {
+func (d *disabledModelConfigCache) GetModelConfig(model string) (ModelConfig, bool) {
 	if config, ok := d.modelConfigs.GetModelConfig(model); ok {
 		return config, true
 	}
@@ -837,7 +837,7 @@ func initializeModelConfigCache() (ModelConfigCache, error) {
 	if err != nil {
 		return nil, err
 	}
-	newModelConfigMap := make(map[string]*ModelConfig)
+	newModelConfigMap := make(map[string]ModelConfig)
 	for _, modelConfig := range modelConfigs {
 		newModelConfigMap[modelConfig.Model] = modelConfig
 	}
@@ -905,16 +905,16 @@ func sortChannelsByPriorityBySet(modelMapBySet map[string]map[string][]*Channel)
 
 func buildEnabledModelsBySet(modelMapBySet map[string]map[string][]*Channel, modelConfigCache ModelConfigCache) (
 	map[string][]string,
-	map[string][]*ModelConfig,
-	map[string]*ModelConfig,
+	map[string][]ModelConfig,
+	map[string]ModelConfig,
 ) {
 	modelsBySet := make(map[string][]string)
-	modelConfigsBySet := make(map[string][]*ModelConfig)
-	modelConfigsMap := make(map[string]*ModelConfig)
+	modelConfigsBySet := make(map[string][]ModelConfig)
+	modelConfigsMap := make(map[string]ModelConfig)
 
 	for set, modelMap := range modelMapBySet {
 		models := make([]string, 0)
-		configs := make([]*ModelConfig, 0)
+		configs := make([]ModelConfig, 0)
 		appended := make(map[string]struct{})
 
 		for model := range modelMap {
@@ -940,7 +940,7 @@ func buildEnabledModelsBySet(modelMapBySet map[string]map[string][]*Channel, mod
 	return modelsBySet, modelConfigsBySet, modelConfigsMap
 }
 
-func SortModelConfigsFunc(i, j *ModelConfig) int {
+func SortModelConfigsFunc(i, j ModelConfig) int {
 	if i.Owner != j.Owner {
 		if natural.Less(string(i.Owner), string(j.Owner)) {
 			return -1

+ 36 - 23
core/model/modelconfig.go

@@ -1,6 +1,7 @@
 package model
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 	"time"
@@ -18,31 +19,43 @@ const (
 
 //nolint:revive
 type ModelConfig struct {
-	CreatedAt        time.Time              `gorm:"index;autoCreateTime"          json:"created_at"`
-	UpdatedAt        time.Time              `gorm:"index;autoUpdateTime"          json:"updated_at"`
-	Config           map[ModelConfigKey]any `gorm:"serializer:fastjson;type:text" json:"config,omitempty"`
-	Model            string                 `gorm:"primaryKey"                    json:"model"`
-	Owner            ModelOwner             `gorm:"type:varchar(255);index"       json:"owner"`
-	Type             mode.Mode              `json:"type"`
-	ExcludeFromTests bool                   `json:"exclude_from_tests,omitempty"`
-	RPM              int64                  `json:"rpm,omitempty"`
-	TPM              int64                  `json:"tpm,omitempty"`
+	CreatedAt        time.Time                  `gorm:"index;autoCreateTime"          json:"created_at"`
+	UpdatedAt        time.Time                  `gorm:"index;autoUpdateTime"          json:"updated_at"`
+	Config           map[ModelConfigKey]any     `gorm:"serializer:fastjson;type:text" json:"config,omitempty"`
+	Plugin           map[string]json.RawMessage `gorm:"serializer:fastjson;type:text" json:"plugin,omitempty"`
+	Model            string                     `gorm:"primaryKey"                    json:"model"`
+	Owner            ModelOwner                 `gorm:"type:varchar(255);index"       json:"owner"`
+	Type             mode.Mode                  `json:"type"`
+	ExcludeFromTests bool                       `json:"exclude_from_tests,omitempty"`
+	RPM              int64                      `json:"rpm,omitempty"`
+	TPM              int64                      `json:"tpm,omitempty"`
 	// map[size]map[quality]price_per_image
 	ImageQualityPrices map[string]map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_quality_prices,omitempty"`
 	// map[size]price_per_image
 	ImagePrices  map[string]float64 `gorm:"serializer:fastjson;type:text" json:"image_prices,omitempty"`
 	Price        Price              `gorm:"embedded"                      json:"price,omitempty"`
-	RetryTimes   int64              `json:"retry_times"`
-	Timeout      int64              `json:"timeout"`
-	MaxErrorRate float64            `json:"max_error_rate"`
+	RetryTimes   int64              `json:"retry_times,omitempty"`
+	Timeout      int64              `json:"timeout,omitempty"`
+	MaxErrorRate float64            `json:"max_error_rate,omitempty"`
 }
 
-func NewDefaultModelConfig(model string) *ModelConfig {
-	return &ModelConfig{
+func NewDefaultModelConfig(model string) ModelConfig {
+	return ModelConfig{
 		Model: model,
 	}
 }
 
+func (c *ModelConfig) LoadPluginConfig(pluginName string, config any) error {
+	if len(c.Plugin) == 0 {
+		return nil
+	}
+	pluginConfig, ok := c.Plugin[pluginName]
+	if !ok || len(pluginConfig) == 0 {
+		return nil
+	}
+	return sonic.Unmarshal(pluginConfig, config)
+}
+
 func (c *ModelConfig) LoadFromGroupModelConfig(groupModelConfig GroupModelConfig) ModelConfig {
 	newC := *c
 	if groupModelConfig.OverrideLimit {
@@ -128,7 +141,7 @@ func GetModelConfigs(page int, perPage int, model string) (configs []*ModelConfi
 	return configs, total, err
 }
 
-func GetAllModelConfigs() (configs []*ModelConfig, err error) {
+func GetAllModelConfigs() (configs []ModelConfig, err error) {
 	tx := DB.Model(&ModelConfig{})
 	err = tx.Order("created_at desc").
 		Omit("created_at", "updated_at").
@@ -137,7 +150,7 @@ func GetAllModelConfigs() (configs []*ModelConfig, err error) {
 	return configs, err
 }
 
-func GetModelConfigsByModels(models []string) (configs []*ModelConfig, err error) {
+func GetModelConfigsByModels(models []string) (configs []ModelConfig, err error) {
 	tx := DB.Model(&ModelConfig{}).Where("model IN (?)", models)
 	err = tx.Order("created_at desc").
 		Omit("created_at", "updated_at").
@@ -146,8 +159,8 @@ func GetModelConfigsByModels(models []string) (configs []*ModelConfig, err error
 	return configs, err
 }
 
-func GetModelConfig(model string) (*ModelConfig, error) {
-	config := &ModelConfig{}
+func GetModelConfig(model string) (ModelConfig, error) {
+	config := ModelConfig{}
 	err := DB.Model(&ModelConfig{}).
 		Where("model = ?", model).
 		Omit("created_at", "updated_at").
@@ -156,7 +169,7 @@ func GetModelConfig(model string) (*ModelConfig, error) {
 	return config, HandleNotFound(err, ErrModelConfigNotFound)
 }
 
-func SearchModelConfigs(keyword string, page int, perPage int, model string, owner ModelOwner) (configs []*ModelConfig, total int64, err error) {
+func SearchModelConfigs(keyword string, page int, perPage int, model string, owner ModelOwner) (configs []ModelConfig, total int64, err error) {
 	tx := DB.Model(&ModelConfig{}).Where("model LIKE ?", "%"+keyword+"%")
 	if model != "" {
 		tx = tx.Where("model = ?", model)
@@ -207,16 +220,16 @@ func SearchModelConfigs(keyword string, page int, perPage int, model string, own
 	return configs, total, err
 }
 
-func SaveModelConfig(config *ModelConfig) (err error) {
+func SaveModelConfig(config ModelConfig) (err error) {
 	defer func() {
 		if err == nil {
 			_ = InitModelConfigAndChannelCache()
 		}
 	}()
-	return DB.Save(config).Error
+	return DB.Save(&config).Error
 }
 
-func SaveModelConfigs(configs []*ModelConfig) (err error) {
+func SaveModelConfigs(configs []ModelConfig) (err error) {
 	defer func() {
 		if err == nil {
 			_ = InitModelConfigAndChannelCache()
@@ -224,7 +237,7 @@ func SaveModelConfigs(configs []*ModelConfig) (err error) {
 	}()
 	return DB.Transaction(func(tx *gorm.DB) error {
 		for _, config := range configs {
-			if err := tx.Save(config).Error; err != nil {
+			if err := tx.Save(&config).Error; err != nil {
 				return err
 			}
 		}

+ 1 - 1
core/relay/adaptor/ai360/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/ai360/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "360GPT_S2_V9",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/ali/adaptor.go

@@ -140,6 +140,6 @@ func getEnableSearch(reqBody []byte) (bool, error) {
 	return enableSearch, nil
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/ali/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://help.aliyun.com/zh/model-studio/getting-started/models?spm=a2c4g.11186623.0.i12#ced16cb6cdfsy
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	// 通义千问-Max
 	{
 		Model: "qwen-max",

+ 1 - 1
core/relay/adaptor/anthropic/adaptor.go

@@ -105,6 +105,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/anthropic/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "claude-3-haiku-20240307",
 		Type:  mode.ChatCompletions,

+ 3 - 0
core/relay/adaptor/anthropic/openai.go

@@ -125,6 +125,9 @@ func OpenAIConvertRequest(meta *meta.Meta, req *http.Request) (*Request, error)
 
 	if onlyThinking.Thinking != nil {
 		claudeRequest.Thinking = onlyThinking.Thinking
+		if claudeRequest.Thinking.Type == "disabled" {
+			claudeRequest.Thinking = nil
+		}
 	} else if strings.Contains(meta.OriginModel, "think") {
 		claudeRequest.Thinking = &Thinking{
 			Type: "enabled",

+ 2 - 2
core/relay/adaptor/aws/adaptor.go

@@ -35,8 +35,8 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, _ *http.Response)
 	return adaptor.(utils.AwsAdapter).DoResponse(meta, c)
 }
 
-func (a *Adaptor) GetModelList() (models []*model.ModelConfig) {
-	models = make([]*model.ModelConfig, 0, len(adaptors))
+func (a *Adaptor) GetModelList() (models []model.ModelConfig) {
+	models = make([]model.ModelConfig, 0, len(adaptors))
 	for _, model := range adaptors {
 		models = append(models, model.config)
 	}

+ 3 - 3
core/relay/adaptor/aws/registry.go

@@ -15,7 +15,7 @@ const (
 )
 
 type Model struct {
-	config *model.ModelConfig
+	config model.ModelConfig
 	_type  ModelType
 }
 
@@ -23,10 +23,10 @@ var adaptors = map[string]Model{}
 
 func init() {
 	for _, model := range claude.AwsModelIDMap {
-		adaptors[model.Model] = Model{config: &model.ModelConfig, _type: AwsClaude}
+		adaptors[model.Model] = Model{config: model.ModelConfig, _type: AwsClaude}
 	}
 	for _, model := range llama3.AwsModelIDMap {
-		adaptors[model.Model] = Model{config: &model.ModelConfig, _type: AwsLlama3}
+		adaptors[model.Model] = Model{config: model.ModelConfig, _type: AwsLlama3}
 	}
 }
 

+ 1 - 1
core/relay/adaptor/baichuan/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/baichuan/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "Baichuan4-Turbo",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/baidu/adaptor.go

@@ -128,6 +128,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/baidu/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "BLOOMZ-7B",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/baiduv2/adaptor.go

@@ -91,6 +91,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	}
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/baiduv2/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Fm2vrveyu
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "ERNIE-4.0-8K-Latest",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/cloudflare/adaptor.go

@@ -50,6 +50,6 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 	}
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/cloudflare/constant.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "@cf/meta/llama-3.1-8b-instruct",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/cohere/adaptor.go

@@ -71,6 +71,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/cohere/constant.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "command",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/coze/adaptor.go

@@ -90,6 +90,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/coze/constants.go

@@ -2,4 +2,4 @@ package coze
 
 import "github.com/labring/aiproxy/core/model"
 
-var ModelList = []*model.ModelConfig{}
+var ModelList = []model.ModelConfig{}

+ 1 - 1
core/relay/adaptor/deepseek/adaptor.go

@@ -18,6 +18,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/deepseek/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "deepseek-chat",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/doc2x/adaptor.go

@@ -59,6 +59,6 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 	return nil
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/doc2x/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "pdf",
 		Type:  mode.ParsePdf,

+ 1 - 1
core/relay/adaptor/doubao/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://console.volcengine.com/ark/region:ark+cn-beijing/model
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "Doubao-1.5-vision-pro-32k",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/doubao/main.go

@@ -44,7 +44,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/doubaoaudio/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://www.volcengine.com/docs/6561/1257543
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "Doubao-tts",
 		Type:  mode.AudioSpeech,

+ 1 - 1
core/relay/adaptor/doubaoaudio/main.go

@@ -30,7 +30,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/gemini/adaptor.go

@@ -86,6 +86,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/gemini/constants.go

@@ -8,7 +8,7 @@ import (
 // https://ai.google.dev/models/gemini
 // https://ai.google.dev/gemini-api/docs/pricing
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "gemini-1.5-pro",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/geminiopenai/adaptor.go

@@ -16,6 +16,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return gemini.ModelList
 }

+ 1 - 1
core/relay/adaptor/groq/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/groq/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://console.groq.com/docs/models
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "gemma-7b-it",
 		Type:  mode.ChatCompletions,

+ 23 - 61
core/relay/adaptor/interface.go

@@ -3,10 +3,8 @@ package adaptor
 import (
 	"encoding/json"
 	"errors"
-	"fmt"
 	"io"
 	"net/http"
-	"reflect"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -14,14 +12,34 @@ import (
 	"github.com/labring/aiproxy/core/relay/meta"
 )
 
-type Adaptor interface {
-	GetBaseURL() string
+type GetRequestURL interface {
 	GetRequestURL(meta *meta.Meta) (string, error)
+}
+
+type SetupRequestHeader interface {
 	SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error
+}
+
+type ConvertRequest interface {
 	ConvertRequest(meta *meta.Meta, req *http.Request) (*ConvertRequestResult, error)
+}
+
+type DoRequest interface {
 	DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request) (*http.Response, error)
+}
+
+type DoResponse interface {
 	DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, Error)
-	GetModelList() []*model.ModelConfig
+}
+
+type Adaptor interface {
+	GetBaseURL() string
+	GetModelList() []model.ModelConfig
+	GetRequestURL
+	SetupRequestHeader
+	ConvertRequest
+	DoRequest
+	DoResponse
 }
 
 type ConvertRequestResult struct {
@@ -48,13 +66,6 @@ func (e ErrorImpl[T]) StatusCode() int {
 	return e.statusCode
 }
 
-func NewError[T any](statusCode int, err T) Error {
-	return ErrorImpl[T]{
-		error:      err,
-		statusCode: statusCode,
-	}
-}
-
 var ErrGetBalanceNotImplemented = errors.New("get balance not implemented")
 
 type Balancer interface {
@@ -88,55 +99,6 @@ type ConfigTemplate struct {
 	Type        ConfigType      `json:"type"`
 }
 
-func ValidateConfigTemplate(template ConfigTemplate) error {
-	if template.Name == "" {
-		return errors.New("config template is invalid: name is empty")
-	}
-	if template.Type == "" {
-		return fmt.Errorf("config template %s is invalid: type is empty", template.Name)
-	}
-	if template.Example != nil {
-		if err := ValidateConfigTemplateValue(template, template.Example); err != nil {
-			return fmt.Errorf("config template %s is invalid: %w", template.Name, err)
-		}
-	}
-	return nil
-}
-
-func ValidateConfigTemplateValue(template ConfigTemplate, value any) error {
-	if template.Validator == nil {
-		return nil
-	}
-	switch template.Type {
-	case ConfigTypeString:
-		_, ok := value.(string)
-		if !ok {
-			return fmt.Errorf("config template %s is invalid: value is not a string", template.Name)
-		}
-	case ConfigTypeNumber:
-		switch value.(type) {
-		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
-			return nil
-		default:
-			return fmt.Errorf("config template %s is invalid: value is not a number", template.Name)
-		}
-	case ConfigTypeBool:
-		_, ok := value.(bool)
-		if !ok {
-			return fmt.Errorf("config template %s is invalid: value is not a bool", template.Name)
-		}
-	case ConfigTypeObject:
-		if reflect.TypeOf(value).Kind() != reflect.Map &&
-			reflect.TypeOf(value).Kind() != reflect.Struct {
-			return fmt.Errorf("config template %s is invalid: value is not a object", template.Name)
-		}
-	}
-	if err := template.Validator(value); err != nil {
-		return fmt.Errorf("config template %s(%s) is invalid: %w", template.Name, template.Name, err)
-	}
-	return nil
-}
-
 type ConfigTemplates = map[string]ConfigTemplate
 
 type Config interface {

+ 1 - 1
core/relay/adaptor/jina/adaptor.go

@@ -39,6 +39,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	}
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/jina/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "jina-reranker-v2-base-multilingual",
 		Type:  mode.Rerank,

+ 1 - 1
core/relay/adaptor/lingyiwanwu/adaptor.go

@@ -16,7 +16,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/lingyiwanwu/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://platform.lingyiwanwu.com/docs
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "yi-lightning",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/minimax/adaptor.go

@@ -22,7 +22,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/minimax/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://www.minimaxi.com/document/guides/chat-model/V2?id=65e0736ab2845de20908e2dd
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "abab7-chat-preview",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/mistral/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/mistral/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "open-mistral-7b",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/moonshot/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/moonshot/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "moonshot-v1-8k",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/novita/adaptor.go

@@ -15,6 +15,6 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/novita/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://novita.ai/llm-api
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "meta-llama/llama-3-8b-instruct",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/ollama/adaptor.go

@@ -76,6 +76,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/ollama/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "codellama:7b-instruct",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/openai/adaptor.go

@@ -185,6 +185,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return DoResponse(meta, c, resp)
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/openai/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "gpt-3.5-turbo",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/openrouter/adaptor.go

@@ -100,6 +100,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	}
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return openai.ModelList
 }

+ 1 - 1
core/relay/adaptor/siliconflow/adaptor.go

@@ -23,7 +23,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/siliconflow/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://docs.siliconflow.cn/docs/getting-started
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "BAAI/bge-reranker-v2-m3",
 		Type:  mode.Rerank,

+ 1 - 1
core/relay/adaptor/stepfun/adaptor.go

@@ -29,7 +29,7 @@ func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.C
 	}
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/stepfun/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "step-1-8k",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/tencent/adaptor.go

@@ -18,7 +18,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/tencent/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://cloud.tencent.com/document/product/1729/104753
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "hunyuan-lite",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/text-embeddings-inference/adaptor.go

@@ -24,7 +24,7 @@ func (a *Adaptor) GetBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/text-embeddings-inference/constants.go

@@ -6,7 +6,7 @@ import (
 )
 
 // maybe we should use a list of models from https://github.com/huggingface/text-embeddings-inference?tab=readme-ov-file#supported-models
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "bge-reranker-v2-m3",
 		Type:  mode.Rerank,

+ 63 - 0
core/relay/adaptor/utils.go

@@ -0,0 +1,63 @@
+package adaptor
+
+import (
+	"errors"
+	"fmt"
+	"reflect"
+)
+
+func NewError[T any](statusCode int, err T) Error {
+	return ErrorImpl[T]{
+		error:      err,
+		statusCode: statusCode,
+	}
+}
+
+func ValidateConfigTemplate(template ConfigTemplate) error {
+	if template.Name == "" {
+		return errors.New("config template is invalid: name is empty")
+	}
+	if template.Type == "" {
+		return fmt.Errorf("config template %s is invalid: type is empty", template.Name)
+	}
+	if template.Example != nil {
+		if err := ValidateConfigTemplateValue(template, template.Example); err != nil {
+			return fmt.Errorf("config template %s is invalid: %w", template.Name, err)
+		}
+	}
+	return nil
+}
+
+func ValidateConfigTemplateValue(template ConfigTemplate, value any) error {
+	if template.Validator == nil {
+		return nil
+	}
+	switch template.Type {
+	case ConfigTypeString:
+		_, ok := value.(string)
+		if !ok {
+			return fmt.Errorf("config template %s is invalid: value is not a string", template.Name)
+		}
+	case ConfigTypeNumber:
+		switch value.(type) {
+		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
+			return nil
+		default:
+			return fmt.Errorf("config template %s is invalid: value is not a number", template.Name)
+		}
+	case ConfigTypeBool:
+		_, ok := value.(bool)
+		if !ok {
+			return fmt.Errorf("config template %s is invalid: value is not a bool", template.Name)
+		}
+	case ConfigTypeObject:
+		if reflect.TypeOf(value).Kind() != reflect.Map &&
+			reflect.TypeOf(value).Kind() != reflect.Struct {
+			return fmt.Errorf("config template %s is invalid: value is not a object", template.Name)
+		}
+	}
+	if err := template.Validator(value); err != nil {
+		return fmt.Errorf("config template %s(%s) is invalid: %w", template.Name, template.Name, err)
+	}
+	return nil
+}

+ 1 - 1
core/relay/adaptor/vertexai/adaptor.go

@@ -44,7 +44,7 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return adaptor.DoResponse(meta, c, resp)
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return modelList
 }
 

+ 1 - 1
core/relay/adaptor/vertexai/claude/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "claude-3-haiku@20240307",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/vertexai/registry.go

@@ -20,7 +20,7 @@ const (
 	VerterAIGemini
 )
 
-var modelList = []*model.ModelConfig{}
+var modelList = []model.ModelConfig{}
 
 func init() {
 	modelList = append(modelList, vertexclaude.ModelList...)

+ 1 - 1
core/relay/adaptor/xai/adaptor.go

@@ -28,6 +28,6 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return a.Adaptor.DoResponse(meta, c, resp)
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }

+ 1 - 1
core/relay/adaptor/xai/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "grok-3",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/xunfei/adaptor.go

@@ -29,7 +29,7 @@ func (a *Adaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.C
 	return a.Adaptor.ConvertRequest(meta, req)
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/xunfei/constants.go

@@ -7,7 +7,7 @@ import (
 
 // https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "SparkDesk-4.0-Ultra",
 		Type:  mode.ChatCompletions,

+ 1 - 1
core/relay/adaptor/zhipu/adaptor.go

@@ -31,7 +31,7 @@ func (a *Adaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Respons
 	return
 }
 
-func (a *Adaptor) GetModelList() []*model.ModelConfig {
+func (a *Adaptor) GetModelList() []model.ModelConfig {
 	return ModelList
 }
 

+ 1 - 1
core/relay/adaptor/zhipu/constants.go

@@ -5,7 +5,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/mode"
 )
 
-var ModelList = []*model.ModelConfig{
+var ModelList = []model.ModelConfig{
 	{
 		Model: "glm-3-turbo",
 		Type:  mode.ChatCompletions,

+ 2 - 2
core/relay/controller/anthropic.go

@@ -7,11 +7,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetAnthropicRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetAnthropicRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetAnthropicRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetAnthropicRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	textRequest, err := utils.UnmarshalAnthropicMessageRequest(c.Request)
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/chat.go

@@ -7,11 +7,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetChatRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetChatRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetChatRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetChatRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request)
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/completions.go

@@ -7,11 +7,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetCompletionsRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetCompletionsRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetCompletionsRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetCompletionsRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request)
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/edits.go

@@ -9,7 +9,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
-func GetImagesEditsRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetImagesEditsRequestPrice(c *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	size := c.PostForm("size")
 	quality := c.PostForm("quality")
 
@@ -29,7 +29,7 @@ func GetImagesEditsRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Pr
 	}, nil
 }
 
-func GetImagesEditsRequestUsage(c *gin.Context, mc *model.ModelConfig) (model.Usage, error) {
+func GetImagesEditsRequestUsage(c *gin.Context, mc model.ModelConfig) (model.Usage, error) {
 	mutliForms, err := c.MultipartForm()
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/embed.go

@@ -7,11 +7,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetEmbedRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetEmbedRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetEmbedRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetEmbedRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	textRequest, err := utils.UnmarshalGeneralOpenAIRequest(c.Request)
 	if err != nil {
 		return model.Usage{}, err

+ 3 - 3
core/relay/controller/image.go

@@ -25,7 +25,7 @@ func getImagesRequest(c *gin.Context) (*relaymodel.ImageRequest, error) {
 	return imageRequest, nil
 }
 
-func GetImagesOutputPrice(modelConfig *model.ModelConfig, size string, quality string) (float64, bool) {
+func GetImagesOutputPrice(modelConfig model.ModelConfig, size string, quality string) (float64, bool) {
 	switch {
 	case len(modelConfig.ImagePrices) == 0 && len(modelConfig.ImageQualityPrices) == 0:
 		return float64(modelConfig.Price.OutputPrice), true
@@ -40,7 +40,7 @@ func GetImagesOutputPrice(modelConfig *model.ModelConfig, size string, quality s
 	}
 }
 
-func GetImagesRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetImagesRequestPrice(c *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	imageRequest, err := getImagesRequest(c)
 	if err != nil {
 		return model.Price{}, err
@@ -62,7 +62,7 @@ func GetImagesRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price,
 	}, nil
 }
 
-func GetImagesRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetImagesRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	imageRequest, err := getImagesRequest(c)
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/pdf.go

@@ -5,10 +5,10 @@ import (
 	"github.com/labring/aiproxy/core/model"
 )
 
-func GetPdfRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetPdfRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetPdfRequestUsage(_ *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetPdfRequestUsage(_ *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	return model.Usage{}, nil
 }

+ 2 - 2
core/relay/controller/rerank.go

@@ -36,11 +36,11 @@ func rerankPromptTokens(rerankRequest *relaymodel.RerankRequest) int64 {
 	return tokens
 }
 
-func GetRerankRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetRerankRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetRerankRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetRerankRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	rerankRequest, err := getRerankRequest(c)
 	if err != nil {
 		return model.Usage{}, err

+ 2 - 2
core/relay/controller/stt.go

@@ -13,11 +13,11 @@ import (
 	"github.com/labring/aiproxy/core/model"
 )
 
-func GetSTTRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetSTTRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetSTTRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetSTTRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	audioFile, err := c.FormFile("file")
 	if err != nil {
 		return model.Usage{}, fmt.Errorf("failed to get audio file: %w", err)

+ 2 - 2
core/relay/controller/tts.go

@@ -8,11 +8,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetTTSRequestPrice(_ *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+func GetTTSRequestPrice(_ *gin.Context, mc model.ModelConfig) (model.Price, error) {
 	return mc.Price, nil
 }
 
-func GetTTSRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+func GetTTSRequestUsage(c *gin.Context, _ model.ModelConfig) (model.Usage, error) {
 	ttsRequest, err := utils.UnmarshalTTSRequest(c.Request)
 	if err != nil {
 		return model.Usage{}, err

+ 28 - 16
core/relay/meta/meta.go

@@ -9,11 +9,12 @@ import (
 )
 
 type ChannelMeta struct {
-	Name    string
-	BaseURL string
-	Key     string
-	ID      int
-	Type    model.ChannelType
+	Name         string
+	BaseURL      string
+	Key          string
+	ID           int
+	Type         model.ChannelType
+	ModelMapping map[string]string
 }
 
 type Meta struct {
@@ -22,7 +23,7 @@ type Meta struct {
 	ChannelConfig model.ChannelConfig
 	Group         *model.GroupCache
 	Token         *model.TokenCache
-	ModelConfig   *model.ModelConfig
+	ModelConfig   model.ModelConfig
 
 	Endpoint    string
 	RequestAt   time.Time
@@ -83,7 +84,7 @@ func NewMeta(
 	channel *model.Channel,
 	mode mode.Mode,
 	modelName string,
-	modelConfig *model.ModelConfig,
+	modelConfig model.ModelConfig,
 	opts ...Option,
 ) *Meta {
 	meta := Meta{
@@ -103,20 +104,31 @@ func NewMeta(
 	}
 
 	if channel != nil {
-		meta.Channel.Name = channel.Name
-		meta.Channel.BaseURL = channel.BaseURL
-		meta.Channel.Key = channel.Key
-		meta.Channel.ID = channel.ID
-		meta.Channel.Type = channel.Type
-		if channel.Config != nil {
-			meta.ChannelConfig = *channel.Config
-		}
-		meta.ActualModel, _ = GetMappedModelName(modelName, channel.ModelMapping)
+		meta.SetChannel(channel)
 	}
 
 	return &meta
 }
 
+func (m *Meta) SetChannel(channel *model.Channel) {
+	m.Channel.Name = channel.Name
+	m.Channel.BaseURL = channel.BaseURL
+	m.Channel.Key = channel.Key
+	m.Channel.ID = channel.ID
+	m.Channel.Type = channel.Type
+	m.Channel.ModelMapping = channel.ModelMapping
+	if channel.Config != nil {
+		m.ChannelConfig = *channel.Config
+	}
+	m.ActualModel, _ = GetMappedModelName(m.OriginModel, channel.ModelMapping)
+}
+
+func (m *Meta) CopyChannelFromMeta(meta *Meta) {
+	m.Channel = meta.Channel
+	m.ChannelConfig = meta.ChannelConfig
+	m.ActualModel, _ = GetMappedModelName(meta.OriginModel, meta.Channel.ModelMapping)
+}
+
 func (m *Meta) ClearValues() {
 	clear(m.values)
 }

+ 35 - 0
core/relay/plugin/noop/noop.go

@@ -0,0 +1,35 @@
+package noop
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/plugin"
+)
+
+var _ plugin.Plugin = (*Noop)(nil)
+
+type Noop struct{}
+
+func (n *Noop) GetRequestURL(meta *meta.Meta, do adaptor.GetRequestURL) (string, error) {
+	return do.GetRequestURL(meta)
+}
+
+func (n *Noop) SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request, do adaptor.SetupRequestHeader) error {
+	return do.SetupRequestHeader(meta, c, req)
+}
+
+func (n *Noop) ConvertRequest(meta *meta.Meta, req *http.Request, do adaptor.ConvertRequest) (*adaptor.ConvertRequestResult, error) {
+	return do.ConvertRequest(meta, req)
+}
+
+func (n *Noop) DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request, do adaptor.DoRequest) (*http.Response, error) {
+	return do.DoRequest(meta, c, req)
+}
+
+func (n *Noop) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response, do adaptor.DoResponse) (*model.Usage, adaptor.Error) {
+	return do.DoResponse(meta, c, resp)
+}

+ 67 - 0
core/relay/plugin/types.go

@@ -0,0 +1,67 @@
+package plugin
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/meta"
+)
+
+// adaptor hook
+type Plugin interface {
+	GetRequestURL(meta *meta.Meta, do adaptor.GetRequestURL) (string, error)
+
+	SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request, do adaptor.SetupRequestHeader) error
+
+	ConvertRequest(meta *meta.Meta, req *http.Request, do adaptor.ConvertRequest) (*adaptor.ConvertRequestResult, error)
+
+	DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request, do adaptor.DoRequest) (*http.Response, error)
+
+	DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response, do adaptor.DoResponse) (*model.Usage, adaptor.Error)
+}
+
+func WrapperAdaptor(adaptor adaptor.Adaptor, plugins ...Plugin) adaptor.Adaptor {
+	if len(plugins) == 0 {
+		return adaptor
+	}
+
+	wrapped := &wrappedAdaptor{
+		Adaptor: adaptor,
+		plugin:  plugins[0],
+	}
+
+	if len(plugins) > 1 {
+		return WrapperAdaptor(wrapped, plugins[1:]...)
+	}
+
+	return wrapped
+}
+
+var _ adaptor.Adaptor = (*wrappedAdaptor)(nil)
+
+type wrappedAdaptor struct {
+	adaptor.Adaptor
+	plugin Plugin
+}
+
+func (w *wrappedAdaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+	return w.plugin.GetRequestURL(meta, w.Adaptor)
+}
+
+func (w *wrappedAdaptor) SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error {
+	return w.plugin.SetupRequestHeader(meta, c, req, w.Adaptor)
+}
+
+func (w *wrappedAdaptor) ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+	return w.plugin.ConvertRequest(meta, req, w.Adaptor)
+}
+
+func (w *wrappedAdaptor) DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request) (*http.Response, error) {
+	return w.plugin.DoRequest(meta, c, req, w.Adaptor)
+}
+
+func (w *wrappedAdaptor) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+	return w.plugin.DoResponse(meta, c, resp, w.Adaptor)
+}

+ 247 - 0
core/relay/plugin/web-search/README.md

@@ -0,0 +1,247 @@
+# Web Search Plugin Configuration Guide
+
+## Overview
+
+The Web Search Plugin is a plugin that provides real-time web search capabilities for AI models, supporting multiple search engines (Google, Bing, Arxiv), with automatic search query rewriting and search result formatting.
+
+## Configuration Example
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "retry_times": 5,
+    "owner": "anthropic",
+    "type": 1,
+    "plugin": {
+        "web-search": {
+            "enable_plugin": true,
+            "default_enable": true,
+            "search_rewrite": {
+                "enable": true
+            },
+            "need_reference": true,
+            "search_from": [
+                {
+                    "type": "google",
+                    "spec": {
+                        "api_key": "api key",
+                        "cx": "cx"
+                    }
+                }
+            ]
+        }
+    }
+}
+```
+
+## Model Config Field Details
+
+### Basic Configuration
+
+| Field | Type | Required | Default | Description |
+|-------|------|----------|---------|-------------|
+| `model` | string | Yes | - | AI model name to use |
+| `retry_times` | int | No | 3 | Number of retries on request failure |
+| `type` | int | Yes | - | Model type identifier |
+
+### Web Search Plugin Configuration
+
+#### Main Configuration Items
+
+| Field | Type | Required | Default | Description |
+|-------|------|----------|---------|-------------|
+| `enable_plugin` | bool | Yes | false | Whether to enable the Web Search plugin |
+| `default_enable` | bool | No | false | Whether to enable web search by default for all requests. By default, if there's no `web_search_options` field in the user request, web search is not enabled |
+| `max_results` | int | No | 10 | Maximum number of results returned per search |
+| `need_reference` | bool | No | false | Whether to include reference information in the response |
+| `reference_location` | string | No | "head" | Reference position, options: `head`, `tail` |
+| `reference_format` | string | No | "**References:**\n%s" | Reference format template, must include `%s` placeholder |
+| `default_language` | string | No | - | Default search language |
+| `prompt_template` | string | No | - | Custom prompt template |
+
+#### Search Rewrite Configuration (`search_rewrite`)
+
+| Field | Type | Required | Default | Description |
+|-------|------|----------|---------|-------------|
+| `enable` | bool | No | false | Whether to enable search query rewriting |
+| `model_name` | string | No | - | Model name for query rewriting, uses current request model if empty |
+| `timeout_millisecond` | uint32 | No | 10000 | Rewrite request timeout (milliseconds) |
+| `max_count` | int | No | 3 | Maximum number of rewritten queries |
+
+#### Search Engine Configuration (`search_from`)
+
+Each search engine configuration contains the following fields:
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `type` | string | Yes | Search engine type: `google`, `bing`, `arxiv` |
+| `max_results` | int | No | Maximum results for this engine |
+| `spec` | object | Depends on type | Engine-specific configuration parameters |
+
+##### Google Search Engine Configuration (`spec`)
+
+```json
+{
+    "type": "google",
+    "spec": {
+        "api_key": "your_google_api_key",
+        "cx": "your_custom_search_engine_id"
+    }
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `api_key` | string | Yes | Google Custom Search API key |
+| `cx` | string | Yes | Google Custom Search Engine ID |
+
+##### Bing Search Engine Configuration (`spec`)
+
+```json
+{
+    "type": "bing",
+    "spec": {
+        "api_key": "your_bing_api_key"
+    }
+}
+```
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `api_key` | string | Yes | Bing Search API key |
+
+##### Arxiv Search Engine Configuration (`spec`)
+
+```json
+{
+    "type": "arxiv",
+    "spec": {}
+}
+```
+
+Arxiv search engine requires no additional configuration parameters.
+
+## User Request Configuration
+
+### web_search_options Field
+
+Users can add the `web_search_options` field in their requests to control search behavior:
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "Please search for the latest AI technology developments"
+        }
+    ],
+    "web_search_options": {
+        "search_context_size": "medium"
+    }
+}
+```
+
+#### web_search_options Configuration Items
+
+| Field | Type | Options | Description |
+|-------|------|---------|-------------|
+| `search_context_size` | string | `low`, `medium`, `high` | Controls the size of search context, affecting the number and depth of search queries |
+
+#### search_context_size Details
+
+The `search_context_size` field controls the breadth and depth of searches:
+
+- **`low`**: Generates 1 search query, suitable for simple, direct questions
+- **`medium`**: Generates 3 search queries (default), suitable for most scenarios
+- **`high`**: Generates 5 search queries, suitable for complex questions or scenarios requiring comprehensive information
+
+This field overrides the `search_rewrite.max_count` value in the configuration, allowing users to dynamically adjust search strategy based on specific needs.
+
+### Conditions for Enabling Search
+
+The Web Search plugin is enabled under the following conditions:
+
+1. **Default Enable**: When `default_enable` is `true` in the configuration, all requests will enable search
+2. **On-Demand Enable**: When `default_enable` is `false`, only requests containing the `web_search_options` field will enable search
+
+### Usage Examples
+
+#### Basic Search Request
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "What's the weather like today?"
+        }
+    ],
+    "web_search_options": {}
+}
+```
+
+#### High-Precision Search Request
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "Analyze the latest applications and development trends of artificial intelligence in the medical field"
+        }
+    ],
+    "web_search_options": {
+        "search_context_size": "high"
+    }
+}
+```
+
+## Usage Instructions
+
+### Basic Usage
+
+1. **Enable Plugin**: Set `enable_plugin` to `true`
+2. **Configure Search Engines**: Add at least one search engine configuration in the `search_from` array
+3. **Set Default Behavior**: Control whether to enable search by default through `default_enable`
+4. **User Control**: Users can control search behavior through the `web_search_options` field
+
+### Advanced Features
+
+#### Search Query Rewriting
+
+When `search_rewrite.enable` is enabled, the plugin will use AI models to automatically optimize user search queries, improving the relevance of search results.
+
+#### Reference Management
+
+When `need_reference` is `true`:
+
+- Search results will include numbered references `[1]`, `[2]`, etc.
+- Reference display position can be controlled through `reference_location`
+- Reference format can be customized through `reference_format`
+
+#### Dynamic Search Control
+
+Users can dynamically adjust search depth through the `search_context_size` parameter:
+
+- Use `low` for simple questions to reduce latency
+- Use `high` for complex questions to get more comprehensive information
+- Use `medium` by default to balance performance and quality
+
+## Important Notes
+
+1. **API Keys**: Ensure valid API keys are provided for selected search engines
+2. **Quota Limits**: Be aware of API call quota limits for various search engines
+3. **Performance Impact**: Enabling search functionality will increase response time; higher `search_context_size` settings result in greater latency
+4. **Cost Considerations**: Search API calls and additional AI model calls will incur costs
+5. **Request Cleanup**: The `web_search_options` field is automatically removed from requests after processing
+
+## Troubleshooting
+
+- If search functionality is not working, check if `enable_plugin` is set to `true`
+- Verify that search engine API keys are correctly configured
+- Ensure the model supports Chat Completions mode
+- Check network connectivity and API service availability
+- Ensure the request contains the `web_search_options` field (when `default_enable` is `false`)

+ 247 - 0
core/relay/plugin/web-search/README.zh.md

@@ -0,0 +1,247 @@
+# Web Search Plugin 配置说明
+
+## 概述
+
+Web Search Plugin 是一个为 AI 模型提供实时网络搜索能力的插件,支持多种搜索引擎(Google、Bing、Arxiv),能够自动重写搜索查询并格式化搜索结果。
+
+## 配置示例
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "retry_times": 5,
+    "owner": "anthropic",
+    "type": 1,
+    "plugin": {
+        "web-search": {
+            "enable_plugin": true,
+            "default_enable": true,
+            "search_rewrite": {
+                "enable": true
+            },
+            "need_reference": true,
+            "search_from": [
+                {
+                    "type": "google",
+                    "spec": {
+                        "api_key": "api key",
+                        "cx": "cx"
+                    }
+                }
+            ]
+        }
+    }
+}
+```
+
+## Model Config 配置字段详解
+
+### 基础配置
+
+| 字段 | 类型 | 必填 | 默认值 | 说明 |
+|------|------|------|--------|------|
+| `model` | string | 是 | - | 使用的 AI 模型名称 |
+| `retry_times` | int | 否 | 3 | 请求失败时的重试次数 |
+| `type` | int | 是 | - | 模型类型标识 |
+
+### Web Search 插件配置
+
+#### 主要配置项
+
+| 字段 | 类型 | 必填 | 默认值 | 说明 |
+|------|------|------|--------|------|
+| `enable_plugin` | bool | 是 | false | 是否启用 Web Search 插件 |
+| `default_enable` | bool | 否 | false | 是否默认为所有请求启用网络搜索,默认情况下,如果用户请求中没有 `web_search_options` 字段,则不启用网络搜索 |
+| `max_results` | int | 否 | 10 | 每次搜索返回的最大结果数量 |
+| `need_reference` | bool | 否 | false | 是否在回答中包含引用信息 |
+| `reference_location` | string | 否 | "head" | 引用位置,可选值:`head`、`tail` |
+| `reference_format` | string | 否 | "**References:**\n%s" | 引用格式模板,必须包含 `%s` 占位符 |
+| `default_language` | string | 否 | - | 默认搜索语言 |
+| `prompt_template` | string | 否 | - | 自定义提示词模板 |
+
+#### 搜索重写配置 (`search_rewrite`)
+
+| 字段 | 类型 | 必填 | 默认值 | 说明 |
+|------|------|------|--------|------|
+| `enable` | bool | 否 | false | 是否启用搜索查询重写功能 |
+| `model_name` | string | 否 | - | 用于重写查询的模型名称,为空时使用当前请求的模型 |
+| `timeout_millisecond` | uint32 | 否 | 10000 | 重写请求超时时间(毫秒) |
+| `max_count` | int | 否 | 3 | 最大重写查询数量 |
+
+#### 搜索引擎配置 (`search_from`)
+
+每个搜索引擎配置包含以下字段:
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `type` | string | 是 | 搜索引擎类型:`google`、`bing`、`arxiv` |
+| `max_results` | int | 否 | 该引擎的最大结果数量 |
+| `spec` | object | 视类型而定 | 引擎特定的配置参数 |
+
+##### Google 搜索引擎配置 (`spec`)
+
+```json
+{
+    "type": "google",
+    "spec": {
+        "api_key": "your_google_api_key",
+        "cx": "your_custom_search_engine_id"
+    }
+}
+```
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `api_key` | string | 是 | Google Custom Search API 密钥 |
+| `cx` | string | 是 | Google 自定义搜索引擎 ID |
+
+##### Bing 搜索引擎配置 (`spec`)
+
+```json
+{
+    "type": "bing",
+    "spec": {
+        "api_key": "your_bing_api_key"
+    }
+}
+```
+
+| 字段 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `api_key` | string | 是 | Bing Search API 密钥 |
+
+##### Arxiv 搜索引擎配置 (`spec`)
+
+```json
+{
+    "type": "arxiv",
+    "spec": {}
+}
+```
+
+Arxiv 搜索引擎无需额外配置参数。
+
+## 用户请求配置
+
+### web_search_options 字段
+
+用户可以在请求中添加 `web_search_options` 字段来控制搜索行为:
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "请搜索最新的AI技术发展"
+        }
+    ],
+    "web_search_options": {
+        "search_context_size": "medium"
+    }
+}
+```
+
+#### web_search_options 配置项
+
+| 字段 | 类型 | 可选值 | 说明 |
+|------|------|--------|------|
+| `search_context_size` | string | `low`、`medium`、`high` | 控制搜索上下文的大小,影响搜索查询的数量和深度 |
+
+#### search_context_size 详解
+
+`search_context_size` 字段用于控制搜索的广度和深度:
+
+- **`low`**:生成 1 个搜索查询,适合简单、直接的问题
+- **`medium`**:生成 3 个搜索查询(默认值),适合大多数场景
+- **`high`**:生成 5 个搜索查询,适合复杂问题或需要全面信息的场景
+
+该字段会覆盖配置中 `search_rewrite.max_count` 的值,允许用户根据具体需求动态调整搜索策略。
+
+### 启用搜索的条件
+
+Web Search 插件在以下情况下会被启用:
+
+1. **默认启用**:当配置中 `default_enable` 为 `true` 时,所有请求都会启用搜索
+2. **按需启用**:当 `default_enable` 为 `false` 时,只有包含 `web_search_options` 字段的请求才会启用搜索
+
+### 使用示例
+
+#### 基础搜索请求
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "今天的天气如何?"
+        }
+    ],
+    "web_search_options": {}
+}
+```
+
+#### 高精度搜索请求
+
+```json
+{
+    "model": "claude-3-7-sonnet-20250219",
+    "messages": [
+        {
+            "role": "user",
+            "content": "分析当前人工智能在医疗领域的最新应用和发展趋势"
+        }
+    ],
+    "web_search_options": {
+        "search_context_size": "high"
+    }
+}
+```
+
+## 使用说明
+
+### 基本使用
+
+1. **启用插件**:将 `enable_plugin` 设置为 `true`
+2. **配置搜索引擎**:在 `search_from` 数组中添加至少一个搜索引擎配置
+3. **设置默认行为**:通过 `default_enable` 控制是否默认启用搜索
+4. **用户控制**:用户可通过 `web_search_options` 字段控制搜索行为
+
+### 高级功能
+
+#### 搜索查询重写
+
+启用 `search_rewrite.enable` 后,插件会使用 AI 模型自动优化用户的搜索查询,提高搜索结果的相关性。
+
+#### 引用管理
+
+当 `need_reference` 为 `true` 时:
+
+- 搜索结果会包含编号引用 `[1]`、`[2]` 等
+- 可通过 `reference_location` 控制引用显示位置
+- 可通过 `reference_format` 自定义引用格式
+
+#### 动态搜索控制
+
+用户可以通过 `search_context_size` 参数动态调整搜索的深度:
+
+- 简单问题使用 `low` 减少延迟
+- 复杂问题使用 `high` 获取更全面的信息
+- 默认使用 `medium` 平衡性能和质量
+
+## 注意事项
+
+1. **API 密钥**:确保为所选搜索引擎提供有效的 API 密钥
+2. **配额限制**:注意各搜索引擎的 API 调用配额限制
+3. **性能影响**:启用搜索功能会增加响应时间,`search_context_size` 设置越高,延迟越大
+4. **成本考虑**:搜索 API 调用和额外的 AI 模型调用会产生费用
+5. **请求清理**:`web_search_options` 字段会在处理后自动从请求中移除
+
+## 故障排除
+
+- 如果搜索功能未生效,检查 `enable_plugin` 是否为 `true`
+- 验证搜索引擎 API 密钥是否正确配置
+- 确认模型支持 Chat Completions 模式
+- 检查网络连接和 API 服务可用性
+- 确保请求中包含 `web_search_options` 字段(当 `default_enable` 为 `false` 时)

+ 214 - 0
core/relay/plugin/web-search/prompts/arxiv.md

@@ -0,0 +1,214 @@
+# 目标
+你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing)/论文资料库(Arxiv),并按照如下情况回复相应内容:
+
+## 情况一:不需要查询搜索引擎/论文资料/私有知识库
+### 情况举例:
+1. **用户发送的消息**不是在提问或寻求帮助
+2. **用户发送的消息**是要求翻译文字
+
+### 思考过程
+根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程
+
+### 回复内容示例:
+none
+
+## 情况二:需要查询搜索引擎/论文资料
+### 情况举例:
+1. 答复**用户发送的消息**,需依赖互联网上最新的资料
+2. 答复**用户发送的消息**,需依赖论文等专业资料
+3. 通过查询资料,可以更好地答复**用户发送的消息**
+
+### 思考过程
+根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程:
+1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料
+2. Where: 判断了解这个知识和资料要向Google等搜索引擎提问,还是向Arxiv论文资料库进行查询,或者需要同时查询多个地方
+3. How: 分析对于要查询的知识和资料,应该提出什么样的问题
+4. Adjust: 明确要向什么地方查询什么问题后,按下面方式对问题进行调整
+  4.1. 向搜索引擎提问:用一句话概括问题,并且针对搜索引擎做问题优化
+  4.2. 向Arxiv论文资料库提问:
+    4.2.1. 明确问题所属领域,然后确定Arxiv的Category值,Category可选的枚举如下:
+      - cs.AI: Artificial Intelligence
+      - cs.AR: Hardware Architecture
+      - cs.CC: Computational Complexity
+      - cs.CE: Computational Engineering, Finance, and Science
+      - cs.CG: Computational Geometry
+      - cs.CL: Computation and Language
+      - cs.CR: Cryptography and Security
+      - cs.CV: Computer Vision and Pattern Recognition
+      - cs.CY: Computers and Society
+      - cs.DB: Databases
+      - cs.DC: Distributed, Parallel, and Cluster Computing
+      - cs.DL: Digital Libraries
+      - cs.DM: Discrete Mathematics
+      - cs.DS: Data Structures and Algorithms
+      - cs.ET: Emerging Technologies
+      - cs.FL: Formal Languages and Automata Theory
+      - cs.GL: General Literature
+      - cs.GR: Graphics
+      - cs.GT: Computer Science and Game Theory
+      - cs.HC: Human-Computer Interaction
+      - cs.IR: Information Retrieval
+      - cs.IT: Information Theory
+      - cs.LG: Machine Learning
+      - cs.LO: Logic in Computer Science
+      - cs.MA: Multiagent Systems
+      - cs.MM: Multimedia
+      - cs.MS: Mathematical Software
+      - cs.NA: Numerical Analysis
+      - cs.NE: Neural and Evolutionary Computing
+      - cs.NI: Networking and Internet Architecture
+      - cs.OH: Other Computer Science
+      - cs.OS: Operating Systems
+      - cs.PF: Performance
+      - cs.PL: Programming Languages
+      - cs.RO: Robotics
+      - cs.SC: Symbolic Computation
+      - cs.SD: Sound
+      - cs.SE: Software Engineering
+      - cs.SI: Social and Information Networks
+      - cs.SY: Systems and Control
+      - econ.EM: Econometrics
+      - econ.GN: General Economics
+      - econ.TH: Theoretical Economics
+      - eess.AS: Audio and Speech Processing
+      - eess.IV: Image and Video Processing
+      - eess.SP: Signal Processing
+      - eess.SY: Systems and Control
+      - math.AC: Commutative Algebra
+      - math.AG: Algebraic Geometry
+      - math.AP: Analysis of PDEs
+      - math.AT: Algebraic Topology
+      - math.CA: Classical Analysis and ODEs
+      - math.CO: Combinatorics
+      - math.CT: Category Theory
+      - math.CV: Complex Variables
+      - math.DG: Differential Geometry
+      - math.DS: Dynamical Systems
+      - math.FA: Functional Analysis
+      - math.GM: General Mathematics
+      - math.GN: General Topology
+      - math.GR: Group Theory
+      - math.GT: Geometric Topology
+      - math.HO: History and Overview
+      - math.IT: Information Theory
+      - math.KT: K-Theory and Homology
+      - math.LO: Logic
+      - math.MG: Metric Geometry
+      - math.MP: Mathematical Physics
+      - math.NA: Numerical Analysis
+      - math.NT: Number Theory
+      - math.OA: Operator Algebras
+      - math.OC: Optimization and Control
+      - math.PR: Probability
+      - math.QA: Quantum Algebra
+      - math.RA: Rings and Algebras
+      - math.RT: Representation Theory
+      - math.SG: Symplectic Geometry
+      - math.SP: Spectral Theory
+      - math.ST: Statistics Theory
+      - astro-ph.CO: Cosmology and Nongalactic Astrophysics
+      - astro-ph.EP: Earth and Planetary Astrophysics
+      - astro-ph.GA: Astrophysics of Galaxies
+      - astro-ph.HE: High Energy Astrophysical Phenomena
+      - astro-ph.IM: Instrumentation and Methods for Astrophysics
+      - astro-ph.SR: Solar and Stellar Astrophysics
+      - cond-mat.dis-nn: Disordered Systems and Neural Networks
+      - cond-mat.mes-hall: Mesoscale and Nanoscale Physics
+      - cond-mat.mtrl-sci: Materials Science
+      - cond-mat.other: Other Condensed Matter
+      - cond-mat.quant-gas: Quantum Gases
+      - cond-mat.soft: Soft Condensed Matter
+      - cond-mat.stat-mech: Statistical Mechanics
+      - cond-mat.str-el: Strongly Correlated Electrons
+      - cond-mat.supr-con: Superconductivity
+      - gr-qc: General Relativity and Quantum Cosmology
+      - hep-ex: High Energy Physics - Experiment
+      - hep-lat: High Energy Physics - Lattice
+      - hep-ph: High Energy Physics - Phenomenology
+      - hep-th: High Energy Physics - Theory
+      - math-ph: Mathematical Physics
+      - nlin.AO: Adaptation and Self-Organizing Systems
+      - nlin.CD: Chaotic Dynamics
+      - nlin.CG: Cellular Automata and Lattice Gases
+      - nlin.PS: Pattern Formation and Solitons
+      - nlin.SI: Exactly Solvable and Integrable Systems
+      - nucl-ex: Nuclear Experiment
+      - nucl-th: Nuclear Theory
+      - physics.acc-ph: Accelerator Physics
+      - physics.ao-ph: Atmospheric and Oceanic Physics
+      - physics.app-ph: Applied Physics
+      - physics.atm-clus: Atomic and Molecular Clusters
+      - physics.atom-ph: Atomic Physics
+      - physics.bio-ph: Biological Physics
+      - physics.chem-ph: Chemical Physics
+      - physics.class-ph: Classical Physics
+      - physics.comp-ph: Computational Physics
+      - physics.data-an: Data Analysis, Statistics and Probability
+      - physics.ed-ph: Physics Education
+      - physics.flu-dyn: Fluid Dynamics
+      - physics.gen-ph: General Physics
+      - physics.geo-ph: Geophysics
+      - physics.hist-ph: History and Philosophy of Physics
+      - physics.ins-det: Instrumentation and Detectors
+      - physics.med-ph: Medical Physics
+      - physics.optics: Optics
+      - physics.plasm-ph: Plasma Physics
+      - physics.pop-ph: Popular Physics
+      - physics.soc-ph: Physics and Society
+      - physics.space-ph: Space Physics
+      - quant-ph: Quantum Physics
+      - q-bio.BM: Biomolecules
+      - q-bio.CB: Cell Behavior
+      - q-bio.GN: Genomics
+      - q-bio.MN: Molecular Networks
+      - q-bio.NC: Neurons and Cognition
+      - q-bio.OT: Other Quantitative Biology
+      - q-bio.PE: Populations and Evolution
+      - q-bio.QM: Quantitative Methods
+      - q-bio.SC: Subcellular Processes
+      - q-bio.TO: Tissues and Organs
+      - q-fin.CP: Computational Finance
+      - q-fin.EC: Economics
+      - q-fin.GN: General Finance
+      - q-fin.MF: Mathematical Finance
+      - q-fin.PM: Portfolio Management
+      - q-fin.PR: Pricing of Securities
+      - q-fin.RM: Risk Management
+      - q-fin.ST: Statistical Finance
+      - q-fin.TR: Trading and Market Microstructure
+      - stat.AP: Applications
+      - stat.CO: Computation
+      - stat.ME: Methodology
+      - stat.ML: Machine Learning
+      - stat.OT: Other Statistics
+      - stat.TH: Statistics Theory
+    4.2.2. 根据问题所属领域,将问题拆分成多组关键词的组合,同时组合中的关键词个数尽量不要超过3个
+5. Final: 按照下面**回复内容示例**进行回复,注意:
+  - 不要输出思考过程
+  - 可以向多个查询目标分别查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内
+  - 查询搜索引擎时,需要以"internet:"开头
+  - 查询Arxiv论文时,需要以Arxiv的Category值开头,例如"cs.AI:"
+  - 查询Arxiv论文时,优先用英文表述关键词进行搜索
+  - 当用多个关键词查询时,关键词之间用","分隔
+  - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词
+  - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词
+  - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题
+
+### 回复内容示例:
+
+#### 用不同语言查询多次搜索引擎
+internet: 黄金价格走势
+internet: The trend of gold prices
+
+#### 向Arxiv的多个类目查询多次
+cs.AI: attention mechanism
+cs.AI: neuron
+q-bio.NC: brain,attention mechanism
+
+#### 向多个查询目标查询多次
+internet: 中国未来房价趋势
+internet: 最新中国经济政策
+econ.TH: policy, real estate
+
+# 用户发送的消息为:
+{question}

+ 39 - 0
core/relay/plugin/web-search/prompts/chinese-internet.md

@@ -0,0 +1,39 @@
+# 目标
+你需要分析**用户发送的消息**,是否需要查询中文搜索引擎,并按照如下情况回复相应内容:
+
+## 情况一:不需要查询搜索引擎
+### 情况举例:
+1. **用户发送的消息**不是在提问或寻求帮助
+2. **用户发送的消息**是要求翻译文字
+
+### 思考过程
+根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程
+
+### 回复内容示例:
+none
+
+## 情况二:需要查询搜索引擎
+### 情况举例:
+1. 答复**用户发送的消息**,需依赖互联网上最新的资料
+2. 答复**用户发送的消息**,需依赖论文等专业资料
+3. 通过查询资料,可以更好地答复**用户发送的消息**
+
+### 思考过程
+根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程:
+1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料
+2. How: 分析对于要查询的知识和资料,应该提出什么样的问题
+3. Adjust: 明确查询什么问题后,用一句话概括问题,并且针对搜索引擎做问题优化
+4. Final: 按照下面**回复内容示例**进行回复,注意:
+  - 不要输出思考过程
+  - 可以查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内
+  - 需要以"internet:"开头
+  - 即使**用户发送的消息**使用了中文以外的其他语言,也用中文向搜索引擎查询问题,但注意不要翻译专有名词
+
+### 回复内容示例:
+
+#### 查询多次搜索引擎
+internet: 黄金价格走势
+internet: 历史黄金价格高点
+
+# 用户发送的消息为:
+{question}

+ 217 - 0
core/relay/plugin/web-search/prompts/full.md

@@ -0,0 +1,217 @@
+# 目标
+你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing)/论文资料库(Arxiv)/私有知识库,并按照如下情况回复相应内容:
+
+## 情况一:不需要查询搜索引擎/论文资料/私有知识库
+### 情况举例:
+1. **用户发送的消息**不是在提问或寻求帮助
+2. **用户发送的消息**是要求翻译文字
+
+### 思考过程
+根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程
+
+### 回复内容示例:
+none
+
+## 情况二:需要查询搜索引擎/论文资料/私有知识库
+### 情况举例:
+1. 答复**用户发送的消息**,需依赖互联网上最新的资料
+2. 答复**用户发送的消息**,需依赖论文等专业资料
+3. 通过查询资料,可以更好地答复**用户发送的消息**
+
+### 思考过程
+根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程:
+1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料
+2. Where: 判断了解这个知识和资料要向Google等搜索引擎提问,还是向Arxiv论文资料库进行查询,还是向私有知识库进行查询,或者需要同时查询多个地方
+3. How: 分析对于要查询的知识和资料,应该提出什么样的问题
+4. Adjust: 明确要向什么地方查询什么问题后,按下面方式对问题进行调整
+  4.1. 向搜索引擎提问:用一句话概括问题,并且针对搜索引擎做问题优化
+  4.2. 向私有知识库提问:用一句话概括问题,私有知识库不需要对关键词进行拆分
+  4.3. 向Arxiv论文资料库提问:
+    4.3.1. 明确问题所属领域,然后确定Arxiv的Category值,Category可选的枚举如下:
+      - cs.AI: Artificial Intelligence
+      - cs.AR: Hardware Architecture
+      - cs.CC: Computational Complexity
+      - cs.CE: Computational Engineering, Finance, and Science
+      - cs.CG: Computational Geometry
+      - cs.CL: Computation and Language
+      - cs.CR: Cryptography and Security
+      - cs.CV: Computer Vision and Pattern Recognition
+      - cs.CY: Computers and Society
+      - cs.DB: Databases
+      - cs.DC: Distributed, Parallel, and Cluster Computing
+      - cs.DL: Digital Libraries
+      - cs.DM: Discrete Mathematics
+      - cs.DS: Data Structures and Algorithms
+      - cs.ET: Emerging Technologies
+      - cs.FL: Formal Languages and Automata Theory
+      - cs.GL: General Literature
+      - cs.GR: Graphics
+      - cs.GT: Computer Science and Game Theory
+      - cs.HC: Human-Computer Interaction
+      - cs.IR: Information Retrieval
+      - cs.IT: Information Theory
+      - cs.LG: Machine Learning
+      - cs.LO: Logic in Computer Science
+      - cs.MA: Multiagent Systems
+      - cs.MM: Multimedia
+      - cs.MS: Mathematical Software
+      - cs.NA: Numerical Analysis
+      - cs.NE: Neural and Evolutionary Computing
+      - cs.NI: Networking and Internet Architecture
+      - cs.OH: Other Computer Science
+      - cs.OS: Operating Systems
+      - cs.PF: Performance
+      - cs.PL: Programming Languages
+      - cs.RO: Robotics
+      - cs.SC: Symbolic Computation
+      - cs.SD: Sound
+      - cs.SE: Software Engineering
+      - cs.SI: Social and Information Networks
+      - cs.SY: Systems and Control
+      - econ.EM: Econometrics
+      - econ.GN: General Economics
+      - econ.TH: Theoretical Economics
+      - eess.AS: Audio and Speech Processing
+      - eess.IV: Image and Video Processing
+      - eess.SP: Signal Processing
+      - eess.SY: Systems and Control
+      - math.AC: Commutative Algebra
+      - math.AG: Algebraic Geometry
+      - math.AP: Analysis of PDEs
+      - math.AT: Algebraic Topology
+      - math.CA: Classical Analysis and ODEs
+      - math.CO: Combinatorics
+      - math.CT: Category Theory
+      - math.CV: Complex Variables
+      - math.DG: Differential Geometry
+      - math.DS: Dynamical Systems
+      - math.FA: Functional Analysis
+      - math.GM: General Mathematics
+      - math.GN: General Topology
+      - math.GR: Group Theory
+      - math.GT: Geometric Topology
+      - math.HO: History and Overview
+      - math.IT: Information Theory
+      - math.KT: K-Theory and Homology
+      - math.LO: Logic
+      - math.MG: Metric Geometry
+      - math.MP: Mathematical Physics
+      - math.NA: Numerical Analysis
+      - math.NT: Number Theory
+      - math.OA: Operator Algebras
+      - math.OC: Optimization and Control
+      - math.PR: Probability
+      - math.QA: Quantum Algebra
+      - math.RA: Rings and Algebras
+      - math.RT: Representation Theory
+      - math.SG: Symplectic Geometry
+      - math.SP: Spectral Theory
+      - math.ST: Statistics Theory
+      - astro-ph.CO: Cosmology and Nongalactic Astrophysics
+      - astro-ph.EP: Earth and Planetary Astrophysics
+      - astro-ph.GA: Astrophysics of Galaxies
+      - astro-ph.HE: High Energy Astrophysical Phenomena
+      - astro-ph.IM: Instrumentation and Methods for Astrophysics
+      - astro-ph.SR: Solar and Stellar Astrophysics
+      - cond-mat.dis-nn: Disordered Systems and Neural Networks
+      - cond-mat.mes-hall: Mesoscale and Nanoscale Physics
+      - cond-mat.mtrl-sci: Materials Science
+      - cond-mat.other: Other Condensed Matter
+      - cond-mat.quant-gas: Quantum Gases
+      - cond-mat.soft: Soft Condensed Matter
+      - cond-mat.stat-mech: Statistical Mechanics
+      - cond-mat.str-el: Strongly Correlated Electrons
+      - cond-mat.supr-con: Superconductivity
+      - gr-qc: General Relativity and Quantum Cosmology
+      - hep-ex: High Energy Physics - Experiment
+      - hep-lat: High Energy Physics - Lattice
+      - hep-ph: High Energy Physics - Phenomenology
+      - hep-th: High Energy Physics - Theory
+      - math-ph: Mathematical Physics
+      - nlin.AO: Adaptation and Self-Organizing Systems
+      - nlin.CD: Chaotic Dynamics
+      - nlin.CG: Cellular Automata and Lattice Gases
+      - nlin.PS: Pattern Formation and Solitons
+      - nlin.SI: Exactly Solvable and Integrable Systems
+      - nucl-ex: Nuclear Experiment
+      - nucl-th: Nuclear Theory
+      - physics.acc-ph: Accelerator Physics
+      - physics.ao-ph: Atmospheric and Oceanic Physics
+      - physics.app-ph: Applied Physics
+      - physics.atm-clus: Atomic and Molecular Clusters
+      - physics.atom-ph: Atomic Physics
+      - physics.bio-ph: Biological Physics
+      - physics.chem-ph: Chemical Physics
+      - physics.class-ph: Classical Physics
+      - physics.comp-ph: Computational Physics
+      - physics.data-an: Data Analysis, Statistics and Probability
+      - physics.ed-ph: Physics Education
+      - physics.flu-dyn: Fluid Dynamics
+      - physics.gen-ph: General Physics
+      - physics.geo-ph: Geophysics
+      - physics.hist-ph: History and Philosophy of Physics
+      - physics.ins-det: Instrumentation and Detectors
+      - physics.med-ph: Medical Physics
+      - physics.optics: Optics
+      - physics.plasm-ph: Plasma Physics
+      - physics.pop-ph: Popular Physics
+      - physics.soc-ph: Physics and Society
+      - physics.space-ph: Space Physics
+      - quant-ph: Quantum Physics
+      - q-bio.BM: Biomolecules
+      - q-bio.CB: Cell Behavior
+      - q-bio.GN: Genomics
+      - q-bio.MN: Molecular Networks
+      - q-bio.NC: Neurons and Cognition
+      - q-bio.OT: Other Quantitative Biology
+      - q-bio.PE: Populations and Evolution
+      - q-bio.QM: Quantitative Methods
+      - q-bio.SC: Subcellular Processes
+      - q-bio.TO: Tissues and Organs
+      - q-fin.CP: Computational Finance
+      - q-fin.EC: Economics
+      - q-fin.GN: General Finance
+      - q-fin.MF: Mathematical Finance
+      - q-fin.PM: Portfolio Management
+      - q-fin.PR: Pricing of Securities
+      - q-fin.RM: Risk Management
+      - q-fin.ST: Statistical Finance
+      - q-fin.TR: Trading and Market Microstructure
+      - stat.AP: Applications
+      - stat.CO: Computation
+      - stat.ME: Methodology
+      - stat.ML: Machine Learning
+      - stat.OT: Other Statistics
+      - stat.TH: Statistics Theory
+    4.3.2. 根据问题所属领域,将问题拆分成多组关键词的组合,同时组合中的关键词个数尽量不要超过3个
+5. Final: 按照下面**回复内容示例**进行回复,注意:
+  - 不要输出思考过程
+  - 可以向多个查询目标分别查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内
+  - 查询搜索引擎时,需要以"internet:"开头
+  - 查询私有知识库时,需要以"private:"开头
+  - 查询Arxiv论文时,需要以Arxiv的Category值开头,例如"cs.AI:"
+  - 查询Arxiv论文时,优先用英文表述关键词进行搜索
+  - 当用多个关键词查询时,关键词之间用","分隔
+  - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词
+  - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词
+  - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题
+
+### 回复内容示例:
+
+#### 用不同语言查询多次搜索引擎
+internet: 黄金价格走势
+internet: The trend of gold prices
+
+#### 向Arxiv的多个类目查询多次
+cs.AI: attention mechanism
+cs.AI: neuron
+q-bio.NC: brain,attention mechanism
+
+#### 向多个查询目标查询多次
+internet: 中国未来房价趋势
+internet: 最新中国经济政策
+econ.TH: policy, real estate
+private: 财务状况
+
+# 用户发送的消息为:
+{question}

+ 41 - 0
core/relay/plugin/web-search/prompts/internet.md

@@ -0,0 +1,41 @@
+# 目标
+你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing),并按照如下情况回复相应内容:
+
+## 情况一:不需要查询搜索引擎
+### 情况举例:
+1. **用户发送的消息**不是在提问或寻求帮助
+2. **用户发送的消息**是要求翻译文字
+
+### 思考过程
+根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程
+
+### 回复内容示例:
+none
+
+## 情况二:需要查询搜索引擎
+### 情况举例:
+1. 答复**用户发送的消息**,需依赖互联网上最新的资料
+2. 答复**用户发送的消息**,需依赖论文等专业资料
+3. 通过查询资料,可以更好地答复**用户发送的消息**
+
+### 思考过程
+根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程:
+1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料
+2. How: 分析对于要查询的知识和资料,应该提出什么样的问题
+3. Adjust: 明确查询什么问题后,用一句话概括问题,并且针对搜索引擎做问题优化
+4. Final: 按照下面**回复内容示例**进行回复,注意:
+  - 不要输出思考过程
+  - 可以查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内
+  - 需要以"internet:"开头
+  - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词
+  - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词
+  - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题
+
+### 回复内容示例:
+
+#### 用不同语言查询多次搜索引擎
+internet: 黄金价格走势
+internet: The trend of gold prices
+
+# 用户发送的消息为:
+{question}

+ 51 - 0
core/relay/plugin/web-search/prompts/private.md

@@ -0,0 +1,51 @@
+# 目标
+你需要分析**用户发送的消息**,是否需要查询搜索引擎(Google/Bing)/私有知识库,并按照如下情况回复相应内容:
+
+## 情况一:不需要查询搜索引擎/私有知识库
+### 情况举例:
+1. **用户发送的消息**不是在提问或寻求帮助
+2. **用户发送的消息**是要求翻译文字
+
+### 思考过程
+根据上面的**情况举例**,如果符合,则按照下面**回复内容示例**进行回复,注意不要输出思考过程
+
+### 回复内容示例:
+none
+
+## 情况二:需要查询搜索引擎/私有知识库
+### 情况举例:
+1. 答复**用户发送的消息**,需依赖互联网上最新的资料
+2. 答复**用户发送的消息**,需依赖论文等专业资料
+3. 通过查询资料,可以更好地答复**用户发送的消息**
+
+### 思考过程
+根据上面的**情况举例**,以及其他需要查询资料的情况,如果符合,按照以下步骤思考,并按照下面**回复内容示例**进行回复,注意不要输出思考过程:
+1. What: 分析要答复**用户发送的消息**,需要了解什么知识和资料
+2. Where: 判断了解这个知识和资料要向Google等搜索引擎提问,还是向私有知识库进行查询,或者需要同时查询多个地方
+3. How: 分析对于要查询的知识和资料,应该提出什么样的问题
+4. Adjust: 明确要向什么地方查询什么问题后,按下面方式对问题进行调整
+  4.1. 向搜索引擎提问:用一句话概括问题,并且针对搜索引擎做问题优化
+  4.2. 向私有知识库提问:用一句话概括问题,私有知识库不需要对关键词进行拆分
+5. Final: 按照下面**回复内容示例**进行回复,注意:
+  - 不要输出思考过程
+  - 可以向多个查询目标分别查询多次,多个查询用换行分隔,总查询次数控制在{max_count}次以内
+  - 查询搜索引擎时,需要以"internet:"开头
+  - 查询私有知识库时,需要以"private:"开头
+  - 当用多个关键词查询时,关键词之间用","分隔
+  - 尽量满足**用户发送的消息**中的搜索要求,例如用户要求用英文搜索,则需用英文表述问题和关键词
+  - 用户如果没有要求搜索语言,则用和**用户发送的消息**一致的语言表述问题和关键词
+  - 如果**用户发送的消息**使用中文,至少要有一条向搜索引擎查询的中文问题
+
+### 回复内容示例:
+
+#### 用不同语言查询多次搜索引擎
+internet: 黄金价格走势
+internet: The trend of gold prices
+
+#### 向多个查询目标查询多次
+internet: 中国未来房价趋势
+internet: 最新中国经济政策
+private: 财务状况
+
+# 用户发送的消息为:
+{question}

+ 622 - 0
core/relay/plugin/web-search/search.go

@@ -0,0 +1,622 @@
+package websearch
+
+import (
+	"bytes"
+	"context"
+	_ "embed"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/bytedance/sonic"
+	"github.com/bytedance/sonic/ast"
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/common"
+	"github.com/labring/aiproxy/core/middleware"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/adaptors"
+	"github.com/labring/aiproxy/core/relay/controller"
+	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
+	"github.com/labring/aiproxy/core/relay/plugin"
+	"github.com/labring/aiproxy/core/relay/plugin/noop"
+	"github.com/labring/aiproxy/core/relay/utils"
+	"github.com/labring/aiproxy/mcp-servers/web-search/engine"
+)
+
+var _ plugin.Plugin = (*WebSearch)(nil)
+
+type GetChannel func(modelName string) (*model.Channel, error)
+
+// WebSearch implements web search functionality
+type WebSearch struct {
+	noop.Noop
+	GetChannel GetChannel
+}
+
+// NewWebSearchPlugin creates a new web search plugin
+func NewWebSearchPlugin(getChannel GetChannel) *WebSearch {
+	return &WebSearch{
+		GetChannel: getChannel,
+	}
+}
+
+// Configuration structures
+type Config struct {
+	EnablePlugin      bool           `json:"enable_plugin"`
+	DefaultEnable     bool           `json:"default_enable"`
+	MaxResults        int            `json:"max_results"`
+	SearchRewrite     SearchRewrite  `json:"search_rewrite"`
+	NeedReference     bool           `json:"need_reference"`
+	ReferenceLocation string         `json:"reference_location"`
+	ReferenceFormat   string         `json:"reference_format"`
+	DefaultLanguage   string         `json:"default_language"`
+	PromptTemplate    string         `json:"prompt_template"`
+	SearchFrom        []EngineConfig `json:"search_from"`
+}
+
+type SearchRewrite struct {
+	Enable             bool   `json:"enable"`
+	ModelName          string `json:"model_name"`
+	TimeoutMillisecond uint32 `json:"timeout_millisecond"`
+	MaxCount           int    `json:"max_count"`
+}
+
+type EngineConfig struct {
+	Type       string          `json:"type"` // bing, google, arxiv
+	MaxResults int             `json:"max_results"`
+	Spec       json.RawMessage `json:"spec"`
+}
+
+func (e *EngineConfig) SpecExists() bool {
+	return len(e.Spec) > 0
+}
+
+func (e *EngineConfig) LoadSpec(spec any) error {
+	if !e.SpecExists() {
+		return nil
+	}
+	return sonic.Unmarshal(e.Spec, spec)
+}
+
+// Engine-specific configuration structures
+type GoogleSpec struct {
+	APIKey string `json:"api_key"`
+	CX     string `json:"cx"`
+}
+
+type BingSpec struct {
+	APIKey string `json:"api_key"`
+}
+
+type ArxivSpec struct{}
+
+//go:embed prompts/arxiv.md
+var arxivSearchPrompts string
+
+//go:embed prompts/internet.md
+var internetSearchPrompts string
+
+// ConvertRequest intercepts and modifies requests to add web search capabilities
+func (p *WebSearch) ConvertRequest(meta *meta.Meta, req *http.Request, do adaptor.ConvertRequest) (*adaptor.ConvertRequestResult, error) {
+	// Skip if not chat completions mode
+	if meta.Mode != mode.ChatCompletions {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Load plugin configuration
+	pluginConfig := Config{}
+	if err := meta.ModelConfig.LoadPluginConfig("web-search", &pluginConfig); err != nil {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Skip if plugin is disabled
+	if !pluginConfig.EnablePlugin {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Apply default configuration values if needed
+	if err := p.validateAndApplyDefaults(&pluginConfig); err != nil {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Initialize search engines
+	engines, arxivExists, err := p.initializeSearchEngines(pluginConfig.SearchFrom)
+	if err != nil || len(engines) == 0 {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Read and parse request body
+	body, err := common.GetRequestBody(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read request body: %w", err)
+	}
+
+	var chatRequest map[string]any
+	if err := sonic.Unmarshal(body, &chatRequest); err != nil {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Check if web search should be enabled for this request
+	webSearchOptions, hasWebSearchOptions := chatRequest["web_search_options"].(map[string]any)
+	if !pluginConfig.DefaultEnable && !hasWebSearchOptions {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Extract user query from messages
+	messages, ok := chatRequest["messages"].([]any)
+	if !ok || len(messages) == 0 {
+		return do.ConvertRequest(meta, req)
+	}
+
+	queryIndex, query := p.extractUserQuery(messages)
+	if query == "" {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Prepare search rewrite prompt if configured
+	searchRewritePrompt := p.prepareSearchRewritePrompt(pluginConfig.SearchRewrite, arxivExists, webSearchOptions)
+
+	// Generate search contexts
+	searchContexts := p.generateSearchContexts(meta, pluginConfig, query, searchRewritePrompt)
+	if len(searchContexts) == 0 {
+		return nil, errors.New("no valid search contexts found")
+	}
+
+	// Execute searches
+	searchResults, err := p.executeSearches(context.Background(), engines, searchContexts)
+	if err != nil || len(searchResults) == 0 {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Format search results and modify request
+	modifiedRequest, references := p.formatSearchResults(chatRequest, queryIndex, query, searchResults, pluginConfig)
+
+	delete(modifiedRequest, "web_search_options")
+
+	// Create new request body
+	modifiedBody, err := sonic.Marshal(modifiedRequest)
+	if err != nil {
+		return do.ConvertRequest(meta, req)
+	}
+
+	// Update the request
+	common.SetRequestBody(req, modifiedBody)
+	defer common.SetRequestBody(req, body)
+
+	// Store references in context if needed
+	if pluginConfig.NeedReference && references != "" {
+		meta.Set("references", references)
+	}
+
+	return do.ConvertRequest(meta, req)
+}
+
+// validateAndApplyDefaults validates configuration and applies default values
+func (p *WebSearch) validateAndApplyDefaults(config *Config) error {
+	// Set default max results
+	if config.MaxResults == 0 {
+		config.MaxResults = 10
+	}
+
+	// Configure reference settings
+	if config.NeedReference {
+		if config.ReferenceLocation == "" {
+			config.ReferenceLocation = "head"
+		} else if config.ReferenceLocation != "head" && config.ReferenceLocation != "tail" {
+			return errors.New("invalid reference location")
+		}
+
+		if config.ReferenceFormat == "" {
+			config.ReferenceFormat = "**References:**\n%s"
+		} else if !strings.Contains(config.ReferenceFormat, "%s") {
+			return errors.New("invalid reference format")
+		}
+	}
+
+	// Set default prompt template if not provided
+	if config.PromptTemplate == "" {
+		if config.NeedReference {
+			config.PromptTemplate = `# 以下内容是基于用户发送的消息的搜索结果:
+{search_results}
+在我给你的搜索结果中,每个结果都是[webpage X begin]...[webpage X end]格式的,X代表每篇文章的数字索引。请在适当的情况下在句子末尾引用上下文。请按照引用编号[X]的格式在答案中对应部分引用上下文。如果一句话源自多个上下文,请列出所有相关的引用编号,例如[3][5],切记不要将引用集中在最后返回引用编号,而是在答案对应部分列出。
+在回答时,请注意以下几点:
+- 今天是北京时间:{cur_date}。
+- 并非搜索结果的所有内容都与用户的问题密切相关,你需要结合问题,对搜索结果进行甄别、筛选。
+- 对于列举类的问题(如列举所有航班信息),尽量将答案控制在10个要点以内,并告诉用户可以查看搜索来源、获得完整信息。优先提供信息完整、最相关的列举项;如非必要,不要主动告诉用户搜索结果未提供的内容。
+- 对于创作类的问题(如写论文),请务必在正文的段落中引用对应的参考编号,例如[3][5],不能只在文章末尾引用。你需要解读并概括用户的题目要求,选择合适的格式,充分利用搜索结果并抽取重要信息,生成符合用户要求、极具思想深度、富有创造力与专业性的答案。你的创作篇幅需要尽可能延长,对于每一个要点的论述要推测用户的意图,给出尽可能多角度的回答要点,且务必信息量大、论述详尽。
+- 如果回答很长,请尽量结构化、分段落总结。如果需要分点作答,尽量控制在5个点以内,并合并相关的内容。
+- 对于客观类的问答,如果问题的答案非常简短,可以适当补充一到两句相关信息,以丰富内容。
+- 你需要根据用户要求和回答内容选择合适、美观的回答格式,确保可读性强。
+- 你的回答应该综合多个相关网页来回答,不能重复引用一个网页。
+- 除非用户要求,否则你回答的语言需要和用户提问的语言保持一致。
+
+# 用户消息为:
+{question}`
+		} else {
+			config.PromptTemplate = `# 以下内容是基于用户发送的消息的搜索结果:
+{search_results}
+在我给你的搜索结果中,每个结果都是[webpage begin]...[webpage end]格式的。
+在回答时,请注意以下几点:
+- 今天是北京时间:{cur_date}。
+- 并非搜索结果的所有内容都与用户的问题密切相关,你需要结合问题,对搜索结果进行甄别、筛选。
+- 对于列举类的问题(如列举所有航班信息),尽量将答案控制在10个要点以内。如非必要,不要主动告诉用户搜索结果未提供的内容。
+- 对于创作类的问题(如写论文),你需要解读并概括用户的题目要求,选择合适的格式,充分利用搜索结果并抽取重要信息,生成符合用户要求、极具思想深度、富有创造力与专业性的答案。你的创作篇幅需要尽可能延长,对于每一个要点的论述要推测用户的意图,给出尽可能多角度的回答要点,且务必信息量大、论述详尽。
+- 如果回答很长,请尽量结构化、分段落总结。如果需要分点作答,尽量控制在5个点以内,并合并相关的内容。
+- 对于客观类的问答,如果问题的答案非常简短,可以适当补充一到两句相关信息,以丰富内容。
+- 你需要根据用户要求和回答内容选择合适、美观的回答格式,确保可读性强。
+- 你的回答应该综合多个相关网页来回答,但回答中不要给出网页的引用来源。
+- 除非用户要求,否则你回答的语言需要和用户提问的语言保持一致。
+
+# 用户消息为:
+{question}`
+		}
+	}
+
+	// Validate prompt template
+	if !strings.Contains(config.PromptTemplate, "{search_results}") ||
+		!strings.Contains(config.PromptTemplate, "{question}") {
+		return errors.New("invalid prompt template")
+	}
+
+	return nil
+}
+
+// initializeSearchEngines creates search engine instances based on configuration
+func (p *WebSearch) initializeSearchEngines(configs []EngineConfig) ([]engine.Engine, bool, error) {
+	var engines []engine.Engine
+	var arxivExists bool
+
+	for _, e := range configs {
+		switch e.Type {
+		case "bing":
+			var spec BingSpec
+			if err := e.LoadSpec(&spec); err != nil {
+				return nil, false, err
+			}
+			engines = append(engines, engine.NewBingEngine(spec.APIKey))
+		case "google":
+			var spec GoogleSpec
+			if err := e.LoadSpec(&spec); err != nil {
+				return nil, false, err
+			}
+			engines = append(engines, engine.NewGoogleEngine(spec.APIKey, spec.CX))
+		case "arxiv":
+			engines = append(engines, engine.NewArxivEngine())
+			arxivExists = true
+		default:
+			return nil, false, fmt.Errorf("unsupported engine type: %s", e.Type)
+		}
+	}
+
+	return engines, arxivExists, nil
+}
+
+// extractUserQuery finds the last user message in the conversation
+func (p *WebSearch) extractUserQuery(messages []any) (int, string) {
+	for i := len(messages) - 1; i >= 0; i-- {
+		msg, ok := messages[i].(map[string]any)
+		if !ok {
+			continue
+		}
+
+		if role, ok := msg["role"].(string); ok && role == "user" {
+			if content, ok := msg["content"].(string); ok {
+				return i, content
+			}
+			return i, ""
+		}
+	}
+	return -1, ""
+}
+
+// prepareSearchRewritePrompt prepares the prompt for search query rewriting
+func (p *WebSearch) prepareSearchRewritePrompt(searchRewrite SearchRewrite, arxivExists bool, webSearchOptions map[string]any) string {
+	if !searchRewrite.Enable {
+		return ""
+	}
+
+	// Select appropriate prompt template
+	var searchRewritePromptTemplate string
+	if arxivExists {
+		searchRewritePromptTemplate = arxivSearchPrompts
+	} else {
+		searchRewritePromptTemplate = internetSearchPrompts
+	}
+
+	// Adjust max count based on search context size if specified
+	maxCount := searchRewrite.MaxCount
+	if webSearchOptions != nil {
+		if searchContextSize, ok := webSearchOptions["search_context_size"].(string); ok {
+			switch searchContextSize {
+			case "low":
+				maxCount = 1
+			case "medium":
+				maxCount = 3
+			case "high":
+				maxCount = 5
+			}
+		}
+	}
+
+	// Replace placeholder with actual max count
+	return strings.ReplaceAll(searchRewritePromptTemplate, "{max_count}", strconv.Itoa(maxCount))
+}
+
+// generateSearchContexts creates search contexts based on the user query
+func (p *WebSearch) generateSearchContexts(m *meta.Meta, config Config, query string, searchRewritePrompt string) []engine.SearchQuery {
+	if searchRewritePrompt == "" {
+		return []engine.SearchQuery{{
+			Queries:  []string{query},
+			Language: config.DefaultLanguage,
+		}}
+	}
+
+	rewriteBody, err := sonic.Marshal(map[string]any{
+		"stream":     false,
+		"max_tokens": 4096,
+		"model":      config.SearchRewrite.ModelName,
+		"messages": []map[string]any{
+			{
+				"role":    "user",
+				"content": strings.ReplaceAll(searchRewritePrompt, "{question}", query),
+			},
+		},
+	})
+	if err != nil {
+		return nil
+	}
+
+	w := httptest.NewRecorder()
+	newc, _ := gin.CreateTestContext(w)
+	newc.Request = &http.Request{
+		URL:    &url.URL{},
+		Body:   io.NopCloser(bytes.NewReader(rewriteBody)),
+		Header: make(http.Header),
+	}
+	middleware.SetRequestID(newc, "web-search-rewrite")
+
+	modelName := config.SearchRewrite.ModelName
+	if modelName == "" {
+		modelName = m.OriginModel
+	}
+	newMeta := meta.NewMeta(
+		nil,
+		mode.ChatCompletions,
+		modelName,
+		model.ModelConfig{
+			Model: modelName,
+			Type:  mode.ChatCompletions,
+		},
+		meta.WithRequestID("web-search-rewrite"),
+	)
+	if config.SearchRewrite.ModelName == "" {
+		newMeta.CopyChannelFromMeta(m)
+	} else {
+		channel, err := p.GetChannel(config.SearchRewrite.ModelName)
+		if err != nil {
+			return nil
+		}
+		newMeta.SetChannel(channel)
+	}
+	adaptor, ok := adaptors.GetAdaptor(newMeta.Channel.Type)
+	if !ok {
+		return nil
+	}
+	controller.Handle(adaptor, newc, newMeta)
+
+	contentNode, err := sonic.Get(w.Body.Bytes(), "choices", 0, "message", "content")
+	if err != nil {
+		return nil
+	}
+
+	content, err := contentNode.String()
+	if err != nil || content == "" {
+		return nil
+	}
+
+	if strings.Contains(content, "none") {
+		return nil
+	}
+
+	// Parse search queries from LLM response
+	var searchContexts []engine.SearchQuery
+	for _, line := range strings.Split(content, "\n") {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+
+		parts := strings.SplitN(line, ":", 2)
+		if len(parts) != 2 {
+			continue
+		}
+
+		engineType := strings.TrimSpace(parts[0])
+		queryStr := strings.TrimSpace(parts[1])
+
+		var ctx engine.SearchQuery
+		ctx.Language = config.DefaultLanguage
+
+		switch {
+		case engineType == "internet":
+			ctx.Queries = []string{queryStr}
+		default:
+			// Arxiv category
+			ctx.ArxivCategory = engineType
+			ctx.Queries = strings.Split(queryStr, ",")
+			for i := range ctx.Queries {
+				ctx.Queries[i] = strings.TrimSpace(ctx.Queries[i])
+			}
+		}
+
+		if len(ctx.Queries) > 0 {
+			searchContexts = append(searchContexts, ctx)
+			if ctx.ArxivCategory != "" {
+				// Conduct inquiries in all areas to increase recall.
+				backupCtx := ctx
+				backupCtx.ArxivCategory = ""
+				searchContexts = append(searchContexts, backupCtx)
+			}
+		}
+	}
+	return searchContexts
+}
+
+// executeSearches performs searches using all configured engines
+func (p *WebSearch) executeSearches(ctx context.Context, engines []engine.Engine, searchContexts []engine.SearchQuery) ([]engine.SearchResult, error) {
+	var allResults []engine.SearchResult
+	resultsChan := make(chan []engine.SearchResult, len(engines)*len(searchContexts))
+	errorsChan := make(chan error, len(engines)*len(searchContexts))
+
+	searchCount := 0
+	for _, eng := range engines {
+		for _, searchCtx := range searchContexts {
+			searchCount++
+			go func(e engine.Engine, sc engine.SearchQuery) {
+				results, err := e.Search(ctx, engine.SearchQuery{
+					Queries:       sc.Queries,
+					MaxResults:    10,
+					Language:      sc.Language,
+					ArxivCategory: sc.ArxivCategory,
+				})
+				if err != nil {
+					errorsChan <- err
+					return
+				}
+				resultsChan <- results
+			}(eng, searchCtx)
+		}
+	}
+
+	// Collect results with timeout
+	timeout := time.After(10 * time.Second)
+	received := 0
+	for received < searchCount {
+		select {
+		case results := <-resultsChan:
+			allResults = append(allResults, results...)
+			received++
+		case <-errorsChan:
+			received++
+		case <-timeout:
+			return allResults, errors.New("search timeout")
+		}
+	}
+
+	// Deduplicate results by link
+	seen := make(map[string]bool)
+	var uniqueResults []engine.SearchResult
+	for _, result := range allResults {
+		if !seen[result.Link] {
+			seen[result.Link] = true
+			uniqueResults = append(uniqueResults, result)
+		}
+	}
+
+	return uniqueResults, nil
+}
+
+// formatSearchResults formats search results for the prompt
+func (p *WebSearch) formatSearchResults(chatRequest map[string]any, queryIndex int, query string, searchResults []engine.SearchResult, config Config) (map[string]any, string) {
+	var formattedResults []string
+	var formattedReferences []string
+
+	for i, result := range searchResults {
+		if config.NeedReference {
+			formattedResults = append(formattedResults,
+				fmt.Sprintf("[webpage %d begin]\n%s\n[webpage %d end]", i+1, result.Content, i+1))
+			formattedReferences = append(formattedReferences,
+				fmt.Sprintf("[%d] [%s](%s)", i+1, result.Title, result.Link))
+		} else {
+			formattedResults = append(formattedResults,
+				fmt.Sprintf("[webpage begin]\n%s\n[webpage end]", result.Content))
+		}
+	}
+
+	// Fill template
+	curDate := time.Now().In(time.FixedZone("CST", 8*3600)).Format("2006年1月2日")
+	searchResultsStr := strings.Join(formattedResults, "\n")
+
+	prompt := strings.Replace(config.PromptTemplate, "{search_results}", searchResultsStr, 1)
+	prompt = strings.Replace(prompt, "{question}", query, 1)
+	prompt = strings.Replace(prompt, "{cur_date}", curDate, 1)
+
+	// Update message
+	messages := chatRequest["messages"].([]any)
+	messages[queryIndex].(map[string]any)["content"] = prompt
+	chatRequest["messages"] = messages
+
+	references := ""
+	if config.NeedReference {
+		references = strings.Join(formattedReferences, "\n\n")
+	}
+
+	return chatRequest, references
+}
+
+type responseWriter struct {
+	gin.ResponseWriter
+	writed     bool
+	references string
+}
+
+func (rw *responseWriter) Write(b []byte) (int, error) {
+	if rw.writed {
+		return rw.ResponseWriter.Write(b)
+	}
+	node, err := sonic.Get(b)
+	if err != nil {
+		return rw.ResponseWriter.Write(b)
+	}
+	var contentNode *ast.Node
+	if utils.IsStreamResponseWithHeader(rw.ResponseWriter.Header()) {
+		contentNode = node.GetByPath("choices", 0, "delta", "content")
+	} else {
+		contentNode = node.GetByPath("choices", 0, "message", "content")
+	}
+	content, err := contentNode.String()
+	if err != nil {
+		return 0, err
+	}
+	refContent := fmt.Sprintf("%s\n\n%s", rw.references, content)
+	*contentNode = ast.NewString(refContent)
+	b, err = sonic.Marshal(&node)
+	if err != nil {
+		return 0, err
+	}
+	rw.writed = true
+	return rw.ResponseWriter.Write(b)
+}
+
+func (rw *responseWriter) WriteString(s string) (int, error) {
+	if rw.writed {
+		return rw.ResponseWriter.WriteString(s)
+	}
+	return rw.ResponseWriter.WriteString(s)
+}
+
+// DoResponse handles response modification for references
+func (p *WebSearch) DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response, do adaptor.DoResponse) (*model.Usage, adaptor.Error) {
+	references := meta.GetString("references")
+	if references == "" {
+		return do.DoResponse(meta, c, resp)
+	}
+	rw := &responseWriter{
+		ResponseWriter: c.Writer,
+		references:     references,
+	}
+	c.Writer = rw
+	defer func() {
+		c.Writer = rw.ResponseWriter
+	}()
+	return do.DoResponse(meta, c, resp)
+}

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików