Browse Source

feat: sora support (#230)

* feat: sora support

* fix: get request body check content type prefix is json

* chore: move spliter to plugin

* refactor: adaptor interface

* chore: azure feture info

* refactor: do response usage filed

* feat: add content length header to request backend

* fix: ci lint
zijiren 7 months ago
parent
commit
172908e54b
100 changed files with 2924 additions and 1302 deletions
  1. 19 0
      core/common/consume/consume.go
  2. 2 1
      core/common/gin.go
  3. 1 1
      core/controller/channel-test.go
  4. 3 2
      core/controller/channel.go
  5. 1 1
      core/controller/dashboard.go
  6. 1 1
      core/controller/model.go
  7. 155 13
      core/controller/relay-controller.go
  8. 1 1
      core/controller/relay-dashboard.go
  9. 72 0
      core/controller/relay.go
  10. 333 51
      core/docs/docs.go
  11. 333 51
      core/docs/swagger.json
  12. 224 33
      core/docs/swagger.yaml
  13. 14 32
      core/middleware/auth.go
  14. 3 1
      core/middleware/ctxkey.go
  15. 75 72
      core/middleware/distributor.go
  16. 8 8
      core/middleware/mcp.go
  17. 67 0
      core/model/cache.go
  18. 2 0
      core/model/chtype.go
  19. 5 1
      core/model/log.go
  20. 1 0
      core/model/main.go
  21. 61 0
      core/model/store.go
  22. 6 4
      core/relay/adaptor/ai360/adaptor.go
  23. 102 23
      core/relay/adaptor/ali/adaptor.go
  24. 11 3
      core/relay/adaptor/ali/embeddings.go
  25. 0 14
      core/relay/adaptor/ali/fetures.go
  26. 21 18
      core/relay/adaptor/ali/image.go
  27. 19 16
      core/relay/adaptor/ali/rerank.go
  28. 9 10
      core/relay/adaptor/ali/stt-realtime.go
  29. 7 8
      core/relay/adaptor/ali/tts.go
  30. 35 16
      core/relay/adaptor/anthropic/adaptor.go
  31. 0 11
      core/relay/adaptor/anthropic/fetures.go
  32. 21 25
      core/relay/adaptor/anthropic/main.go
  33. 2 2
      core/relay/adaptor/anthropic/model.go
  34. 46 26
      core/relay/adaptor/anthropic/openai.go
  35. 35 17
      core/relay/adaptor/aws/adaptor.go
  36. 6 5
      core/relay/adaptor/aws/claude/adapter.go
  37. 26 23
      core/relay/adaptor/aws/claude/main.go
  38. 0 4
      core/relay/adaptor/aws/key.go
  39. 6 5
      core/relay/adaptor/aws/llama3/adapter.go
  40. 37 26
      core/relay/adaptor/aws/llama3/main.go
  41. 10 2
      core/relay/adaptor/aws/utils/adaptor.go
  42. 4 8
      core/relay/adaptor/azure/key.go
  43. 116 44
      core/relay/adaptor/azure/main.go
  44. 29 0
      core/relay/adaptor/azure2/main.go
  45. 6 4
      core/relay/adaptor/baichuan/adaptor.go
  46. 27 18
      core/relay/adaptor/baidu/adaptor.go
  47. 6 3
      core/relay/adaptor/baidu/embeddings.go
  48. 7 4
      core/relay/adaptor/baidu/image.go
  49. 0 4
      core/relay/adaptor/baidu/key.go
  50. 18 15
      core/relay/adaptor/baidu/main.go
  51. 7 4
      core/relay/adaptor/baidu/rerank.go
  52. 31 14
      core/relay/adaptor/baiduv2/adaptor.go
  53. 0 4
      core/relay/adaptor/baiduv2/key.go
  54. 24 9
      core/relay/adaptor/cloudflare/adaptor.go
  55. 31 15
      core/relay/adaptor/cohere/adaptor.go
  56. 11 10
      core/relay/adaptor/cohere/main.go
  57. 33 16
      core/relay/adaptor/coze/adaptor.go
  58. 0 4
      core/relay/adaptor/coze/key.go
  59. 16 13
      core/relay/adaptor/coze/main.go
  60. 5 4
      core/relay/adaptor/deepseek/adaptor.go
  61. 24 11
      core/relay/adaptor/doc2x/adaptor.go
  62. 15 14
      core/relay/adaptor/doc2x/pdf.go
  63. 0 12
      core/relay/adaptor/doubao/fetures.go
  64. 41 23
      core/relay/adaptor/doubao/main.go
  65. 0 12
      core/relay/adaptor/doubaoaudio/fetures.go
  66. 0 4
      core/relay/adaptor/doubaoaudio/key.go
  67. 30 12
      core/relay/adaptor/doubaoaudio/main.go
  68. 10 14
      core/relay/adaptor/doubaoaudio/tts.go
  69. 29 11
      core/relay/adaptor/gemini/adaptor.go
  70. 0 2
      core/relay/adaptor/gemini/config.go
  71. 16 19
      core/relay/adaptor/gemini/embeddings.go
  72. 0 12
      core/relay/adaptor/gemini/fetures.go
  73. 24 21
      core/relay/adaptor/gemini/main.go
  74. 0 1
      core/relay/adaptor/gemini/model.go
  75. 10 4
      core/relay/adaptor/geminiopenai/adaptor.go
  76. 0 12
      core/relay/adaptor/geminiopenai/fetures.go
  77. 6 4
      core/relay/adaptor/groq/adaptor.go
  78. 45 37
      core/relay/adaptor/interface.go
  79. 15 7
      core/relay/adaptor/jina/adaptor.go
  80. 7 30
      core/relay/adaptor/jina/embeddings.go
  81. 0 12
      core/relay/adaptor/jina/fetures.go
  82. 13 11
      core/relay/adaptor/jina/rerank.go
  83. 7 5
      core/relay/adaptor/lingyiwanwu/adaptor.go
  84. 37 15
      core/relay/adaptor/minimax/adaptor.go
  85. 0 11
      core/relay/adaptor/minimax/fetures.go
  86. 0 4
      core/relay/adaptor/minimax/key.go
  87. 42 24
      core/relay/adaptor/minimax/tts.go
  88. 6 4
      core/relay/adaptor/mistral/adaptor.go
  89. 6 4
      core/relay/adaptor/moonshot/adaptor.go
  90. 6 4
      core/relay/adaptor/novita/adaptor.go
  91. 36 14
      core/relay/adaptor/ollama/adaptor.go
  92. 0 11
      core/relay/adaptor/ollama/fetures.go
  93. 40 38
      core/relay/adaptor/ollama/main.go
  94. 0 1
      core/relay/adaptor/ollama/model.go
  95. 106 94
      core/relay/adaptor/openai/adaptor.go
  96. 128 25
      core/relay/adaptor/openai/chat.go
  97. 37 17
      core/relay/adaptor/openai/embeddings.go
  98. 29 0
      core/relay/adaptor/openai/error.go
  99. 0 11
      core/relay/adaptor/openai/fetures.go
  100. 8 15
      core/relay/adaptor/openai/helper.go

+ 19 - 0
core/common/consume/consume.go

@@ -10,6 +10,7 @@ import (
 	"github.com/labring/aiproxy/core/common/notify"
 	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/core/relay/meta"
+	"github.com/labring/aiproxy/core/relay/mode"
 	"github.com/shopspring/decimal"
 	log "github.com/sirupsen/logrus"
 )
@@ -37,6 +38,10 @@ func AsyncConsume(
 	channelRate model.RequestRate,
 	groupRate model.RequestRate,
 ) {
+	if !checkNeedRecordConsume(code, meta) {
+		return
+	}
+
 	consumeWaitGroup.Add(1)
 	defer func() {
 		consumeWaitGroup.Done()
@@ -83,6 +88,10 @@ func Consume(
 	channelRate model.RequestRate,
 	groupRate model.RequestRate,
 ) {
+	if !checkNeedRecordConsume(code, meta) {
+		return
+	}
+
 	amount := CalculateAmount(code, usage, modelPrice)
 	amount = consumeAmount(ctx, amount, postGroupConsumer, meta)
 
@@ -109,6 +118,16 @@ func Consume(
 	}
 }
 
+func checkNeedRecordConsume(code int, meta *meta.Meta) bool {
+	switch meta.Mode {
+	case mode.VideoGenerationsGetJobs,
+		mode.VideoGenerationsContent:
+		return code != http.StatusOK
+	default:
+		return true
+	}
+}
+
 func consumeAmount(
 	ctx context.Context,
 	amount float64,

+ 2 - 1
core/common/gin.go

@@ -66,7 +66,8 @@ func GetRequestBody(req *http.Request) ([]byte, error) {
 			req.Body = io.NopCloser(bytes.NewBuffer(buf))
 		}
 	}()
-	if req.ContentLength <= 0 || req.Header.Get("Content-Type") != "application/json" {
+	if req.ContentLength <= 0 ||
+		strings.HasPrefix(contentType, "application/json") {
 		buf, err = io.ReadAll(LimitReader(req.Body, MaxRequestBodySize))
 		if err != nil {
 			if errors.Is(err, ErrLimitedReaderExceeded) {

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

@@ -40,7 +40,7 @@ var (
 func guessModelConfig(modelName string) model.ModelConfig {
 	modelConfigCacheOnce.Do(func() {
 		for _, c := range adaptors.ChannelAdaptor {
-			for _, m := range c.GetModelList() {
+			for _, m := range c.Metadata().Models {
 				if _, ok := modelConfigCache[m.Model]; !ok {
 					modelConfigCache[m.Model] = m
 				}

+ 3 - 2
core/controller/channel.go

@@ -221,10 +221,11 @@ func (r *AddChannelRequest) ToChannel() (*model.Channel, error) {
 	if !ok {
 		return nil, fmt.Errorf("invalid channel type: %d", r.Type)
 	}
+	metadata := a.Metadata()
 	if validator := adaptors.GetKeyValidator(a); validator != nil {
 		err := validator.ValidateKey(r.Key)
 		if err != nil {
-			keyHelp := validator.KeyHelp()
+			keyHelp := metadata.KeyHelp
 			if keyHelp == "" {
 				return nil, fmt.Errorf(
 					"%s [%s(%d)] invalid key: %w",
@@ -245,7 +246,7 @@ func (r *AddChannelRequest) ToChannel() (*model.Channel, error) {
 		}
 	}
 	if r.Config != nil {
-		for key, template := range adaptors.GetConfigTemplates(a) {
+		for key, template := range metadata.Config {
 			v, err := r.Config.Get(key)
 			if err != nil {
 				if errors.Is(err, ast.ErrNotExist) {

+ 1 - 1
core/controller/dashboard.go

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

+ 1 - 1
core/controller/model.go

@@ -87,7 +87,7 @@ func init() {
 	builtinModelsMap = make(map[string]*OpenAIModels)
 	// https://platform.openai.com/docs/models/model-endpoint-compatibility
 	for i, adaptor := range adaptors.ChannelAdaptor {
-		modelNames := adaptor.GetModelList()
+		modelNames := adaptor.Metadata().Models
 		builtinChannelType2Models[i] = make([]BuiltinModelConfig, len(modelNames))
 		for idx, _model := range modelNames {
 			if _model.Owner == "" {

+ 155 - 13
core/controller/relay-controller.go

@@ -108,6 +108,7 @@ func updateChannelModelTokensRequestRate(c *gin.Context, meta *meta.Meta, tpm, t
 
 func (w *wrapAdaptor) DoRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -117,18 +118,16 @@ func (w *wrapAdaptor) DoRequest(
 		meta.OriginModel,
 	)
 	updateChannelModelRequestRate(c, meta, count+overLimitCount, secondCount)
-	return w.Adaptor.DoRequest(meta, c, req)
+	return w.Adaptor.DoRequest(meta, store, c, req)
 }
 
 func (w *wrapAdaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
-	usage, relayErr := w.Adaptor.DoResponse(meta, c, resp)
-	if usage == nil {
-		return nil, relayErr
-	}
+) (model.Usage, adaptor.Error) {
+	usage, relayErr := w.Adaptor.DoResponse(meta, store, c, resp)
 
 	if usage.TotalTokens > 0 {
 		count, overLimitCount, secondCount := reqlimit.PushChannelModelTokensRequest(
@@ -161,6 +160,37 @@ func (w *wrapAdaptor) DoResponse(
 	return usage, relayErr
 }
 
+var adaptorStore adaptor.Store = &storeImpl{}
+
+type storeImpl struct{}
+
+func (s *storeImpl) GetStore(id string) (adaptor.StoreCache, error) {
+	store, err := model.CacheGetStore(id)
+	if err != nil {
+		return adaptor.StoreCache{}, err
+	}
+	return adaptor.StoreCache{
+		ID:        store.ID,
+		GroupID:   store.GroupID,
+		TokenID:   store.TokenID,
+		ChannelID: store.ChannelID,
+		Model:     store.Model,
+		ExpiresAt: store.ExpiresAt,
+	}, nil
+}
+
+func (s *storeImpl) SaveStore(store adaptor.StoreCache) error {
+	_, err := model.SaveStore(&model.Store{
+		ID:        store.ID,
+		GroupID:   store.GroupID,
+		TokenID:   store.TokenID,
+		ChannelID: store.ChannelID,
+		Model:     store.Model,
+		ExpiresAt: store.ExpiresAt,
+	})
+	return err
+}
+
 func relayHandler(c *gin.Context, meta *meta.Meta) *controller.HandleResult {
 	log := middleware.GetLogger(c)
 	middleware.SetLogFieldsFromMeta(meta, log.Data)
@@ -184,7 +214,7 @@ func relayHandler(c *gin.Context, meta *meta.Meta) *controller.HandleResult {
 		thinksplit.NewThinkPlugin(),
 	)
 
-	return controller.Handle(a, c, meta)
+	return controller.Handle(a, c, meta, adaptorStore)
 }
 
 func relayController(m mode.Mode) RelayController {
@@ -222,10 +252,95 @@ func relayController(m mode.Mode) RelayController {
 	case mode.Completions:
 		c.GetRequestPrice = controller.GetCompletionsRequestPrice
 		c.GetRequestUsage = controller.GetCompletionsRequestUsage
+	case mode.VideoGenerationsJobs:
+		c.GetRequestPrice = controller.GetVideoGenerationJobRequestPrice
+		c.GetRequestUsage = controller.GetVideoGenerationJobRequestUsage
 	}
 	return c
 }
 
+const (
+	AIProxyChannelHeader = "Aiproxy-Channel"
+)
+
+func GetChannelFromHeader(
+	header string,
+	mc *model.ModelCaches,
+	availableSet []string,
+	model string,
+) (*model.Channel, error) {
+	channelIDInt, err := strconv.ParseInt(header, 10, 64)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, set := range availableSet {
+		enabledChannels := mc.EnabledModel2ChannelsBySet[set][model]
+		if len(enabledChannels) > 0 {
+			for _, channel := range enabledChannels {
+				if int64(channel.ID) == channelIDInt {
+					return channel, nil
+				}
+			}
+		}
+
+		disabledChannels := mc.DisabledModel2ChannelsBySet[set][model]
+		if len(disabledChannels) > 0 {
+			for _, channel := range disabledChannels {
+				if int64(channel.ID) == channelIDInt {
+					return channel, nil
+				}
+			}
+		}
+	}
+
+	return nil, fmt.Errorf("channel %d not found for model `%s`", channelIDInt, model)
+}
+
+func GetChannelFromRequest(
+	c *gin.Context,
+	mc *model.ModelCaches,
+	availableSet []string,
+	modelName string,
+	m mode.Mode,
+) (*model.Channel, error) {
+	switch m {
+	case mode.VideoGenerationsGetJobs,
+		mode.VideoGenerationsContent:
+		channelID := middleware.GetChannelID(c)
+		if channelID == 0 {
+			return nil, errors.New("channel id is required")
+		}
+		for _, set := range availableSet {
+			enabledChannels := mc.EnabledModel2ChannelsBySet[set][modelName]
+			if len(enabledChannels) > 0 {
+				for _, channel := range enabledChannels {
+					if channel.ID == channelID {
+						return channel, nil
+					}
+				}
+			}
+		}
+		return nil, fmt.Errorf("channel %d not found for model `%s`", channelID, modelName)
+	default:
+		channelID := middleware.GetChannelID(c)
+		if channelID == 0 {
+			return nil, nil
+		}
+		for _, set := range availableSet {
+			enabledChannels := mc.EnabledModel2ChannelsBySet[set][modelName]
+			if len(enabledChannels) > 0 {
+				for _, channel := range enabledChannels {
+					if channel.ID == channelID {
+						return channel, nil
+					}
+				}
+			}
+		}
+	}
+	return nil, nil
+}
+
 func RelayHelper(
 	c *gin.Context,
 	meta *meta.Meta,
@@ -484,7 +599,7 @@ func relay(c *gin.Context, mode mode.Mode, relayController RelayController) {
 	mc := middleware.GetModelConfig(c)
 
 	// Get initial channel
-	initialChannel, err := getInitialChannel(c, requestModel)
+	initialChannel, err := getInitialChannel(c, requestModel, mode)
 	if err != nil || initialChannel == nil || initialChannel.channel == nil {
 		middleware.AbortLogWithMessageWithMode(mode, c,
 			http.StatusServiceUnavailable,
@@ -646,13 +761,43 @@ type initialChannel struct {
 	migratedChannels  []*model.Channel
 }
 
-func getInitialChannel(c *gin.Context, modelName string) (*initialChannel, error) {
+func getInitialChannel(c *gin.Context, modelName string, m mode.Mode) (*initialChannel, error) {
 	log := middleware.GetLogger(c)
-	if channel := middleware.GetChannel(c); channel != nil {
+
+	group := middleware.GetGroup(c)
+	availableSet := group.GetAvailableSets()
+
+	if channelHeader := c.Request.Header.Get(AIProxyChannelHeader); channelHeader != "" {
+		if group.Status != model.GroupStatusInternal {
+			return nil, errors.New("channel header is not allowed in non-internal group")
+		}
+		channel, err := GetChannelFromHeader(
+			channelHeader,
+			middleware.GetModelCaches(c),
+			availableSet,
+			modelName,
+		)
+		if err != nil {
+			return nil, err
+		}
 		log.Data["designated_channel"] = "true"
 		return &initialChannel{channel: channel, designatedChannel: true}, nil
 	}
 
+	channel, err := GetChannelFromRequest(
+		c,
+		middleware.GetModelCaches(c),
+		availableSet,
+		modelName,
+		m,
+	)
+	if err != nil {
+		return nil, err
+	}
+	if channel != nil {
+		return &initialChannel{channel: channel, designatedChannel: true}, nil
+	}
+
 	mc := middleware.GetModelCaches(c)
 
 	ids, err := monitor.GetBannedChannelsWithModel(c.Request.Context(), modelName)
@@ -666,9 +811,6 @@ func getInitialChannel(c *gin.Context, modelName string) (*initialChannel, error
 		log.Errorf("get channel model error rates failed: %+v", err)
 	}
 
-	group := middleware.GetGroup(c)
-	availableSet := group.GetAvailableSets()
-
 	channel, migratedChannels, err := getChannelWithFallback(
 		mc,
 		availableSet,

+ 1 - 1
core/controller/relay-dashboard.go

@@ -23,7 +23,7 @@ import (
 //	@Router			/v1/dashboard/billing/subscription [get]
 func GetSubscription(c *gin.Context) {
 	group := middleware.GetGroup(c)
-	b, _, err := balance.GetGroupRemainBalance(c, *group)
+	b, _, err := balance.GetGroupRemainBalance(c.Request.Context(), group)
 	if err != nil {
 		if errors.Is(err, balance.ErrNoRealNameUsedAmountLimit) {
 			middleware.ErrorResponse(c, http.StatusForbidden, err.Error())

+ 72 - 0
core/controller/relay.go

@@ -283,3 +283,75 @@ func ParsePdf() []gin.HandlerFunc {
 		NewRelay(mode.ParsePdf),
 	}
 }
+
+// VideoGenerationsJobs godoc
+//
+//	@Summary		VideoGenerationsJobs
+//	@Description	VideoGenerationsJobs
+//	@Tags			relay
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			request			body		model.VideoGenerationJobRequest	true	"Request"
+//	@Param			Aiproxy-Channel	header		string							false	"Optional Aiproxy-Channel header"
+//	@Success		200				{object}	model.VideoGenerationJob
+//	@Header			all				{integer}	X-RateLimit-Limit-Requests		"X-RateLimit-Limit-Requests"
+//	@Header			all				{integer}	X-RateLimit-Limit-Tokens		"X-RateLimit-Limit-Tokens"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Requests	"X-RateLimit-Remaining-Requests"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Tokens	"X-RateLimit-Remaining-Tokens"
+//	@Header			all				{string}	X-RateLimit-Reset-Requests		"X-RateLimit-Reset-Requests"
+//	@Header			all				{string}	X-RateLimit-Reset-Tokens		"X-RateLimit-Reset-Tokens"
+//	@Router			/v1/video/generations/jobs [post]
+func VideoGenerationsJobs() []gin.HandlerFunc {
+	return []gin.HandlerFunc{
+		middleware.NewDistribute(mode.VideoGenerationsJobs),
+		NewRelay(mode.VideoGenerationsJobs),
+	}
+}
+
+// VideoGenerationsGetJobs godoc
+//
+//	@Summary		VideoGenerationsGetJobs
+//	@Description	VideoGenerationsGetJobs
+//	@Tags			relay
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			request			body		model.VideoGenerationJobRequest	true	"Request"
+//	@Param			Aiproxy-Channel	header		string							false	"Optional Aiproxy-Channel header"
+//	@Success		200				{object}	model.VideoGenerationJob
+//	@Header			all				{integer}	X-RateLimit-Limit-Requests		"X-RateLimit-Limit-Requests"
+//	@Header			all				{integer}	X-RateLimit-Limit-Tokens		"X-RateLimit-Limit-Tokens"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Requests	"X-RateLimit-Remaining-Requests"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Tokens	"X-RateLimit-Remaining-Tokens"
+//	@Header			all				{string}	X-RateLimit-Reset-Requests		"X-RateLimit-Reset-Requests"
+//	@Header			all				{string}	X-RateLimit-Reset-Tokens		"X-RateLimit-Reset-Tokens"
+//	@Router			/v1/video/generations/jobs/{id} [get]
+func VideoGenerationsGetJobs() []gin.HandlerFunc {
+	return []gin.HandlerFunc{
+		middleware.NewDistribute(mode.VideoGenerationsGetJobs),
+		NewRelay(mode.VideoGenerationsGetJobs),
+	}
+}
+
+// VideoGenerationsContent godoc
+//
+//	@Summary		VideoGenerationsContent
+//	@Description	VideoGenerationsContent
+//	@Tags			relay
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			request			body		model.VideoGenerationJobRequest	true	"Request"
+//	@Param			Aiproxy-Channel	header		string							false	"Optional Aiproxy-Channel header"
+//	@Success		200				{file}		file							"video binary"
+//	@Header			all				{integer}	X-RateLimit-Limit-Requests		"X-RateLimit-Limit-Requests"
+//	@Header			all				{integer}	X-RateLimit-Limit-Tokens		"X-RateLimit-Limit-Tokens"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Requests	"X-RateLimit-Remaining-Requests"
+//	@Header			all				{integer}	X-RateLimit-Remaining-Tokens	"X-RateLimit-Remaining-Tokens"
+//	@Header			all				{string}	X-RateLimit-Reset-Requests		"X-RateLimit-Reset-Requests"
+//	@Header			all				{string}	X-RateLimit-Reset-Tokens		"X-RateLimit-Reset-Tokens"
+//	@Router			/v1/video/generations/{id}/content/video [get]
+func VideoGenerationsContent() []gin.HandlerFunc {
+	return []gin.HandlerFunc{
+		middleware.NewDistribute(mode.VideoGenerationsContent),
+		NewRelay(mode.VideoGenerationsContent),
+	}
+}

+ 333 - 51
core/docs/docs.go

@@ -4619,6 +4619,7 @@ const docTemplate = `{
                         "enum": [
                             1,
                             3,
+                            4,
                             12,
                             13,
                             14,
@@ -7787,6 +7788,210 @@ const docTemplate = `{
                     }
                 }
             }
+        },
+        "/v1/video/generations/jobs": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsJobs",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsJobs",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJob"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/v1/video/generations/jobs/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsGetJobs",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsGetJobs",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJob"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/v1/video/generations/{id}/content/video": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsContent",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsContent",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "video binary",
+                        "schema": {
+                            "type": "file"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
         }
     },
     "definitions": {
@@ -7832,7 +8037,7 @@ const docTemplate = `{
         "adaptors.AdaptorMeta": {
             "type": "object",
             "properties": {
-                "configTemplates": {
+                "config": {
                     "$ref": "#/definitions/adaptor.ConfigTemplates"
                 },
                 "defaultBaseUrl": {
@@ -8555,7 +8760,10 @@ const docTemplate = `{
                 9,
                 10,
                 11,
-                12
+                12,
+                13,
+                14,
+                15
             ],
             "x-enum-varnames": [
                 "Unknown",
@@ -8570,7 +8778,10 @@ const docTemplate = `{
                 "AudioTranslation",
                 "Rerank",
                 "ParsePdf",
-                "Anthropic"
+                "Anthropic",
+                "VideoGenerationsJobs",
+                "VideoGenerationsGetJobs",
+                "VideoGenerationsContent"
             ]
         },
         "model.AnthropicMessageRequest": {
@@ -8587,17 +8798,6 @@ const docTemplate = `{
                 }
             }
         },
-        "model.Audio": {
-            "type": "object",
-            "properties": {
-                "format": {
-                    "type": "string"
-                },
-                "voice": {
-                    "type": "string"
-                }
-            }
-        },
         "model.Channel": {
             "type": "object",
             "properties": {
@@ -8729,6 +8929,7 @@ const docTemplate = `{
             "enum": [
                 1,
                 3,
+                4,
                 12,
                 13,
                 14,
@@ -8766,6 +8967,7 @@ const docTemplate = `{
             "x-enum-varnames": [
                 "ChannelTypeOpenAI",
                 "ChannelTypeAzure",
+                "ChannelTypeAzure2",
                 "ChannelTypeGoogleGeminiOpenAI",
                 "ChannelTypeBaiduV2",
                 "ChannelTypeAnthropic",
@@ -9074,24 +9276,12 @@ const docTemplate = `{
         "model.GeneralOpenAIRequest": {
             "type": "object",
             "properties": {
-                "audio": {
-                    "$ref": "#/definitions/model.Audio"
-                },
-                "dimensions": {
-                    "type": "integer"
-                },
-                "encoding_format": {
-                    "type": "string"
-                },
                 "frequency_penalty": {
                     "type": "number"
                 },
                 "function_call": {},
                 "functions": {},
                 "input": {},
-                "instruction": {
-                    "type": "string"
-                },
                 "logit_bias": {},
                 "logprobs": {
                     "type": "boolean"
@@ -9109,57 +9299,32 @@ const docTemplate = `{
                     }
                 },
                 "metadata": {},
-                "modalities": {
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
                 "model": {
                     "type": "string"
                 },
-                "n": {
-                    "type": "integer"
-                },
                 "num_ctx": {
                     "type": "integer"
                 },
-                "parallel_tool_calls": {
-                    "type": "boolean"
-                },
-                "prediction": {},
                 "presence_penalty": {
                     "type": "number"
                 },
                 "prompt": {},
-                "quality": {
-                    "type": "string"
-                },
                 "response_format": {
                     "$ref": "#/definitions/model.ResponseFormat"
                 },
                 "seed": {
                     "type": "number"
                 },
-                "service_tier": {
-                    "type": "string"
-                },
                 "size": {
                     "type": "string"
                 },
                 "stop": {},
-                "store": {
-                    "type": "boolean"
-                },
                 "stream": {
                     "type": "boolean"
                 },
                 "stream_options": {
                     "$ref": "#/definitions/model.StreamOptions"
                 },
-                "style": {
-                    "type": "string"
-                },
                 "temperature": {
                     "type": "number"
                 },
@@ -10371,6 +10536,123 @@ const docTemplate = `{
                 }
             }
         },
+        "model.VideoGenerationJob": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "integer"
+                },
+                "expires_at": {
+                    "type": "integer"
+                },
+                "finish_reason": {
+                    "type": "string"
+                },
+                "finished_at": {
+                    "type": "integer"
+                },
+                "generations": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.VideoGenerations"
+                    }
+                },
+                "height": {
+                    "type": "integer"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "n_variants": {
+                    "type": "integer"
+                },
+                "object": {
+                    "type": "string"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "status": {
+                    "$ref": "#/definitions/model.VideoGenerationJobStatus"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
+        "model.VideoGenerationJobRequest": {
+            "type": "object",
+            "properties": {
+                "height": {
+                    "type": "integer"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "n_variants": {
+                    "type": "integer"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
+        "model.VideoGenerationJobStatus": {
+            "type": "string",
+            "enum": [
+                "queued",
+                "processing",
+                "running",
+                "succeeded"
+            ],
+            "x-enum-varnames": [
+                "VideoGenerationJobStatusQueued",
+                "VideoGenerationJobStatusProcessing",
+                "VideoGenerationJobStatusRunning",
+                "VideoGenerationJobStatusSucceeded"
+            ]
+        },
+        "model.VideoGenerations": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "integer"
+                },
+                "height": {
+                    "type": "integer"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "job_id": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "object": {
+                    "type": "string"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
         "openai.SubscriptionResponse": {
             "type": "object",
             "properties": {

+ 333 - 51
core/docs/swagger.json

@@ -4610,6 +4610,7 @@
                         "enum": [
                             1,
                             3,
+                            4,
                             12,
                             13,
                             14,
@@ -7778,6 +7779,210 @@
                     }
                 }
             }
+        },
+        "/v1/video/generations/jobs": {
+            "post": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsJobs",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsJobs",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJob"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/v1/video/generations/jobs/{id}": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsGetJobs",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsGetJobs",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "OK",
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJob"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/v1/video/generations/{id}/content/video": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "description": "VideoGenerationsContent",
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "relay"
+                ],
+                "summary": "VideoGenerationsContent",
+                "parameters": [
+                    {
+                        "description": "Request",
+                        "name": "request",
+                        "in": "body",
+                        "required": true,
+                        "schema": {
+                            "$ref": "#/definitions/model.VideoGenerationJobRequest"
+                        }
+                    },
+                    {
+                        "type": "string",
+                        "description": "Optional Aiproxy-Channel header",
+                        "name": "Aiproxy-Channel",
+                        "in": "header"
+                    }
+                ],
+                "responses": {
+                    "200": {
+                        "description": "video binary",
+                        "schema": {
+                            "type": "file"
+                        },
+                        "headers": {
+                            "X-RateLimit-Limit-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Requests"
+                            },
+                            "X-RateLimit-Limit-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Limit-Tokens"
+                            },
+                            "X-RateLimit-Remaining-Requests": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Requests"
+                            },
+                            "X-RateLimit-Remaining-Tokens": {
+                                "type": "integer",
+                                "description": "X-RateLimit-Remaining-Tokens"
+                            },
+                            "X-RateLimit-Reset-Requests": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Requests"
+                            },
+                            "X-RateLimit-Reset-Tokens": {
+                                "type": "string",
+                                "description": "X-RateLimit-Reset-Tokens"
+                            }
+                        }
+                    }
+                }
+            }
         }
     },
     "definitions": {
@@ -7823,7 +8028,7 @@
         "adaptors.AdaptorMeta": {
             "type": "object",
             "properties": {
-                "configTemplates": {
+                "config": {
                     "$ref": "#/definitions/adaptor.ConfigTemplates"
                 },
                 "defaultBaseUrl": {
@@ -8546,7 +8751,10 @@
                 9,
                 10,
                 11,
-                12
+                12,
+                13,
+                14,
+                15
             ],
             "x-enum-varnames": [
                 "Unknown",
@@ -8561,7 +8769,10 @@
                 "AudioTranslation",
                 "Rerank",
                 "ParsePdf",
-                "Anthropic"
+                "Anthropic",
+                "VideoGenerationsJobs",
+                "VideoGenerationsGetJobs",
+                "VideoGenerationsContent"
             ]
         },
         "model.AnthropicMessageRequest": {
@@ -8578,17 +8789,6 @@
                 }
             }
         },
-        "model.Audio": {
-            "type": "object",
-            "properties": {
-                "format": {
-                    "type": "string"
-                },
-                "voice": {
-                    "type": "string"
-                }
-            }
-        },
         "model.Channel": {
             "type": "object",
             "properties": {
@@ -8720,6 +8920,7 @@
             "enum": [
                 1,
                 3,
+                4,
                 12,
                 13,
                 14,
@@ -8757,6 +8958,7 @@
             "x-enum-varnames": [
                 "ChannelTypeOpenAI",
                 "ChannelTypeAzure",
+                "ChannelTypeAzure2",
                 "ChannelTypeGoogleGeminiOpenAI",
                 "ChannelTypeBaiduV2",
                 "ChannelTypeAnthropic",
@@ -9065,24 +9267,12 @@
         "model.GeneralOpenAIRequest": {
             "type": "object",
             "properties": {
-                "audio": {
-                    "$ref": "#/definitions/model.Audio"
-                },
-                "dimensions": {
-                    "type": "integer"
-                },
-                "encoding_format": {
-                    "type": "string"
-                },
                 "frequency_penalty": {
                     "type": "number"
                 },
                 "function_call": {},
                 "functions": {},
                 "input": {},
-                "instruction": {
-                    "type": "string"
-                },
                 "logit_bias": {},
                 "logprobs": {
                     "type": "boolean"
@@ -9100,57 +9290,32 @@
                     }
                 },
                 "metadata": {},
-                "modalities": {
-                    "type": "array",
-                    "items": {
-                        "type": "string"
-                    }
-                },
                 "model": {
                     "type": "string"
                 },
-                "n": {
-                    "type": "integer"
-                },
                 "num_ctx": {
                     "type": "integer"
                 },
-                "parallel_tool_calls": {
-                    "type": "boolean"
-                },
-                "prediction": {},
                 "presence_penalty": {
                     "type": "number"
                 },
                 "prompt": {},
-                "quality": {
-                    "type": "string"
-                },
                 "response_format": {
                     "$ref": "#/definitions/model.ResponseFormat"
                 },
                 "seed": {
                     "type": "number"
                 },
-                "service_tier": {
-                    "type": "string"
-                },
                 "size": {
                     "type": "string"
                 },
                 "stop": {},
-                "store": {
-                    "type": "boolean"
-                },
                 "stream": {
                     "type": "boolean"
                 },
                 "stream_options": {
                     "$ref": "#/definitions/model.StreamOptions"
                 },
-                "style": {
-                    "type": "string"
-                },
                 "temperature": {
                     "type": "number"
                 },
@@ -10362,6 +10527,123 @@
                 }
             }
         },
+        "model.VideoGenerationJob": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "integer"
+                },
+                "expires_at": {
+                    "type": "integer"
+                },
+                "finish_reason": {
+                    "type": "string"
+                },
+                "finished_at": {
+                    "type": "integer"
+                },
+                "generations": {
+                    "type": "array",
+                    "items": {
+                        "$ref": "#/definitions/model.VideoGenerations"
+                    }
+                },
+                "height": {
+                    "type": "integer"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "n_variants": {
+                    "type": "integer"
+                },
+                "object": {
+                    "type": "string"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "status": {
+                    "$ref": "#/definitions/model.VideoGenerationJobStatus"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
+        "model.VideoGenerationJobRequest": {
+            "type": "object",
+            "properties": {
+                "height": {
+                    "type": "integer"
+                },
+                "model": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "n_variants": {
+                    "type": "integer"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
+        "model.VideoGenerationJobStatus": {
+            "type": "string",
+            "enum": [
+                "queued",
+                "processing",
+                "running",
+                "succeeded"
+            ],
+            "x-enum-varnames": [
+                "VideoGenerationJobStatusQueued",
+                "VideoGenerationJobStatusProcessing",
+                "VideoGenerationJobStatusRunning",
+                "VideoGenerationJobStatusSucceeded"
+            ]
+        },
+        "model.VideoGenerations": {
+            "type": "object",
+            "properties": {
+                "created_at": {
+                    "type": "integer"
+                },
+                "height": {
+                    "type": "integer"
+                },
+                "id": {
+                    "type": "string"
+                },
+                "job_id": {
+                    "type": "string"
+                },
+                "n_seconds": {
+                    "type": "integer"
+                },
+                "object": {
+                    "type": "string"
+                },
+                "prompt": {
+                    "type": "string"
+                },
+                "width": {
+                    "type": "integer"
+                }
+            }
+        },
         "openai.SubscriptionResponse": {
             "type": "object",
             "properties": {

+ 224 - 33
core/docs/swagger.yaml

@@ -29,7 +29,7 @@ definitions:
     - ConfigTypeObject
   adaptors.AdaptorMeta:
     properties:
-      configTemplates:
+      config:
         $ref: '#/definitions/adaptor.ConfigTemplates'
       defaultBaseUrl:
         type: string
@@ -508,6 +508,9 @@ definitions:
     - 10
     - 11
     - 12
+    - 13
+    - 14
+    - 15
     type: integer
     x-enum-varnames:
     - Unknown
@@ -523,6 +526,9 @@ definitions:
     - Rerank
     - ParsePdf
     - Anthropic
+    - VideoGenerationsJobs
+    - VideoGenerationsGetJobs
+    - VideoGenerationsContent
   model.AnthropicMessageRequest:
     properties:
       messages:
@@ -532,13 +538,6 @@ definitions:
       model:
         type: string
     type: object
-  model.Audio:
-    properties:
-      format:
-        type: string
-      voice:
-        type: string
-    type: object
   model.Channel:
     properties:
       balance:
@@ -626,6 +625,7 @@ definitions:
     enum:
     - 1
     - 3
+    - 4
     - 12
     - 13
     - 14
@@ -663,6 +663,7 @@ definitions:
     x-enum-varnames:
     - ChannelTypeOpenAI
     - ChannelTypeAzure
+    - ChannelTypeAzure2
     - ChannelTypeGoogleGeminiOpenAI
     - ChannelTypeBaiduV2
     - ChannelTypeAnthropic
@@ -878,19 +879,11 @@ definitions:
     type: object
   model.GeneralOpenAIRequest:
     properties:
-      audio:
-        $ref: '#/definitions/model.Audio'
-      dimensions:
-        type: integer
-      encoding_format:
-        type: string
       frequency_penalty:
         type: number
       function_call: {}
       functions: {}
       input: {}
-      instruction:
-        type: string
       logit_bias: {}
       logprobs:
         type: boolean
@@ -903,41 +896,24 @@ definitions:
           $ref: '#/definitions/model.Message'
         type: array
       metadata: {}
-      modalities:
-        items:
-          type: string
-        type: array
       model:
         type: string
-      "n":
-        type: integer
       num_ctx:
         type: integer
-      parallel_tool_calls:
-        type: boolean
-      prediction: {}
       presence_penalty:
         type: number
       prompt: {}
-      quality:
-        type: string
       response_format:
         $ref: '#/definitions/model.ResponseFormat'
       seed:
         type: number
-      service_tier:
-        type: string
       size:
         type: string
       stop: {}
-      store:
-        type: boolean
       stream:
         type: boolean
       stream_options:
         $ref: '#/definitions/model.StreamOptions'
-      style:
-        type: string
       temperature:
         type: number
       tool_choice: {}
@@ -1767,6 +1743,85 @@ definitions:
       type:
         type: string
     type: object
+  model.VideoGenerationJob:
+    properties:
+      created_at:
+        type: integer
+      expires_at:
+        type: integer
+      finish_reason:
+        type: string
+      finished_at:
+        type: integer
+      generations:
+        items:
+          $ref: '#/definitions/model.VideoGenerations'
+        type: array
+      height:
+        type: integer
+      id:
+        type: string
+      model:
+        type: string
+      n_seconds:
+        type: integer
+      n_variants:
+        type: integer
+      object:
+        type: string
+      prompt:
+        type: string
+      status:
+        $ref: '#/definitions/model.VideoGenerationJobStatus'
+      width:
+        type: integer
+    type: object
+  model.VideoGenerationJobRequest:
+    properties:
+      height:
+        type: integer
+      model:
+        type: string
+      n_seconds:
+        type: integer
+      n_variants:
+        type: integer
+      prompt:
+        type: string
+      width:
+        type: integer
+    type: object
+  model.VideoGenerationJobStatus:
+    enum:
+    - queued
+    - processing
+    - running
+    - succeeded
+    type: string
+    x-enum-varnames:
+    - VideoGenerationJobStatusQueued
+    - VideoGenerationJobStatusProcessing
+    - VideoGenerationJobStatusRunning
+    - VideoGenerationJobStatusSucceeded
+  model.VideoGenerations:
+    properties:
+      created_at:
+        type: integer
+      height:
+        type: integer
+      id:
+        type: string
+      job_id:
+        type: string
+      n_seconds:
+        type: integer
+      object:
+        type: string
+      prompt:
+        type: string
+      width:
+        type: integer
+    type: object
   openai.SubscriptionResponse:
     properties:
       access_until:
@@ -4569,6 +4624,7 @@ paths:
         enum:
         - 1
         - 3
+        - 4
         - 12
         - 13
         - 14
@@ -6535,6 +6591,141 @@ paths:
       summary: Rerank
       tags:
       - relay
+  /v1/video/generations/{id}/content/video:
+    get:
+      description: VideoGenerationsContent
+      parameters:
+      - description: Request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/model.VideoGenerationJobRequest'
+      - description: Optional Aiproxy-Channel header
+        in: header
+        name: Aiproxy-Channel
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: video binary
+          headers:
+            X-RateLimit-Limit-Requests:
+              description: X-RateLimit-Limit-Requests
+              type: integer
+            X-RateLimit-Limit-Tokens:
+              description: X-RateLimit-Limit-Tokens
+              type: integer
+            X-RateLimit-Remaining-Requests:
+              description: X-RateLimit-Remaining-Requests
+              type: integer
+            X-RateLimit-Remaining-Tokens:
+              description: X-RateLimit-Remaining-Tokens
+              type: integer
+            X-RateLimit-Reset-Requests:
+              description: X-RateLimit-Reset-Requests
+              type: string
+            X-RateLimit-Reset-Tokens:
+              description: X-RateLimit-Reset-Tokens
+              type: string
+          schema:
+            type: file
+      security:
+      - ApiKeyAuth: []
+      summary: VideoGenerationsContent
+      tags:
+      - relay
+  /v1/video/generations/jobs:
+    post:
+      description: VideoGenerationsJobs
+      parameters:
+      - description: Request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/model.VideoGenerationJobRequest'
+      - description: Optional Aiproxy-Channel header
+        in: header
+        name: Aiproxy-Channel
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          headers:
+            X-RateLimit-Limit-Requests:
+              description: X-RateLimit-Limit-Requests
+              type: integer
+            X-RateLimit-Limit-Tokens:
+              description: X-RateLimit-Limit-Tokens
+              type: integer
+            X-RateLimit-Remaining-Requests:
+              description: X-RateLimit-Remaining-Requests
+              type: integer
+            X-RateLimit-Remaining-Tokens:
+              description: X-RateLimit-Remaining-Tokens
+              type: integer
+            X-RateLimit-Reset-Requests:
+              description: X-RateLimit-Reset-Requests
+              type: string
+            X-RateLimit-Reset-Tokens:
+              description: X-RateLimit-Reset-Tokens
+              type: string
+          schema:
+            $ref: '#/definitions/model.VideoGenerationJob'
+      security:
+      - ApiKeyAuth: []
+      summary: VideoGenerationsJobs
+      tags:
+      - relay
+  /v1/video/generations/jobs/{id}:
+    get:
+      description: VideoGenerationsGetJobs
+      parameters:
+      - description: Request
+        in: body
+        name: request
+        required: true
+        schema:
+          $ref: '#/definitions/model.VideoGenerationJobRequest'
+      - description: Optional Aiproxy-Channel header
+        in: header
+        name: Aiproxy-Channel
+        type: string
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: OK
+          headers:
+            X-RateLimit-Limit-Requests:
+              description: X-RateLimit-Limit-Requests
+              type: integer
+            X-RateLimit-Limit-Tokens:
+              description: X-RateLimit-Limit-Tokens
+              type: integer
+            X-RateLimit-Remaining-Requests:
+              description: X-RateLimit-Remaining-Requests
+              type: integer
+            X-RateLimit-Remaining-Tokens:
+              description: X-RateLimit-Remaining-Tokens
+              type: integer
+            X-RateLimit-Reset-Requests:
+              description: X-RateLimit-Reset-Requests
+              type: string
+            X-RateLimit-Reset-Tokens:
+              description: X-RateLimit-Reset-Tokens
+              type: string
+          schema:
+            $ref: '#/definitions/model.VideoGenerationJob'
+      security:
+      - ApiKeyAuth: []
+      summary: VideoGenerationsGetJobs
+      tags:
+      - relay
 securityDefinitions:
   ApiKeyAuth:
     in: header

+ 14 - 32
core/middleware/auth.go

@@ -80,21 +80,21 @@ func TokenAuth(c *gin.Context) {
 		"sk-",
 	)
 
-	var token *model.TokenCache
+	var token model.TokenCache
 	var useInternalToken bool
 	if config.AdminKey != "" && config.AdminKey == key ||
 		config.InternalToken != "" && config.InternalToken == key {
-		token = &model.TokenCache{
+		token = model.TokenCache{
 			Key: key,
 		}
 		useInternalToken = true
 	} else {
-		var err error
-		token, err = model.ValidateAndGetToken(key)
+		tokenCache, err := model.ValidateAndGetToken(key)
 		if err != nil {
 			AbortLogWithMessage(c, http.StatusUnauthorized, err.Error(), "invalid_token")
 			return
 		}
+		token = *tokenCache
 	}
 
 	SetLogTokenFields(log.Data, token, useInternalToken)
@@ -118,19 +118,19 @@ func TokenAuth(c *gin.Context) {
 
 	modelCaches := model.LoadModelCaches()
 
-	var group *model.GroupCache
+	var group model.GroupCache
 	if useInternalToken {
-		group = &model.GroupCache{
+		group = model.GroupCache{
 			Status:        model.GroupStatusInternal,
 			AvailableSets: slices.Collect(maps.Keys(modelCaches.EnabledModelsBySet)),
 		}
 	} else {
-		var err error
-		group, err = model.CacheGetGroup(token.Group)
+		groupCache, err := model.CacheGetGroup(token.Group)
 		if err != nil {
 			AbortLogWithMessage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get group: %v", err))
 			return
 		}
+		group = *groupCache
 	}
 	SetLogGroupFields(log.Data, group)
 	if group.Status != model.GroupStatusEnabled && group.Status != model.GroupStatusInternal {
@@ -148,16 +148,16 @@ func TokenAuth(c *gin.Context) {
 	c.Next()
 }
 
-func GetGroup(c *gin.Context) *model.GroupCache {
-	v, ok := c.MustGet(Group).(*model.GroupCache)
+func GetGroup(c *gin.Context) model.GroupCache {
+	v, ok := c.MustGet(Group).(model.GroupCache)
 	if !ok {
 		panic(fmt.Sprintf("group cache type error: %T, %v", v, v))
 	}
 	return v
 }
 
-func GetToken(c *gin.Context) *model.TokenCache {
-	v, ok := c.MustGet(Token).(*model.TokenCache)
+func GetToken(c *gin.Context) model.TokenCache {
+	v, ok := c.MustGet(Token).(model.TokenCache)
 	if !ok {
 		panic(fmt.Sprintf("token cache type error: %T, %v", v, v))
 	}
@@ -172,18 +172,6 @@ func GetModelCaches(c *gin.Context) *model.ModelCaches {
 	return v
 }
 
-func GetChannel(c *gin.Context) *model.Channel {
-	ch, exists := c.Get(Channel)
-	if !exists {
-		return nil
-	}
-	v, ok := ch.(*model.Channel)
-	if !ok {
-		panic(fmt.Sprintf("channel type error: %T, %v", v, v))
-	}
-	return v
-}
-
 func SetLogFieldsFromMeta(m *meta.Meta, fields logrus.Fields) {
 	SetLogRequestIDField(fields, m.RequestID)
 
@@ -225,19 +213,13 @@ func SetLogRequestIDField(fields logrus.Fields, requestID string) {
 	fields["reqid"] = requestID
 }
 
-func SetLogGroupFields(fields logrus.Fields, group *model.GroupCache) {
-	if group == nil {
-		return
-	}
+func SetLogGroupFields(fields logrus.Fields, group model.GroupCache) {
 	if group.ID != "" {
 		fields["gid"] = group.ID
 	}
 }
 
-func SetLogTokenFields(fields logrus.Fields, token *model.TokenCache, internal bool) {
-	if token == nil {
-		return
-	}
+func SetLogTokenFields(fields logrus.Fields, token model.TokenCache, internal bool) {
 	if token.ID > 0 {
 		fields["kid"] = token.ID
 	}

+ 3 - 1
core/middleware/ctxkey.go

@@ -1,7 +1,7 @@
 package middleware
 
 const (
-	Channel            = "channel"
+	ChannelID          = "channel_id"
 	GroupModelTokenRPM = "group_model_token_rpm"
 	GroupModelTokenRPS = "group_model_token_rps"
 	GroupModelTokenTPM = "group_model_token_tpm"
@@ -17,4 +17,6 @@ const (
 	ModelCaches        = "model_caches"
 	ModelConfig        = "model_config"
 	Mode               = "mode"
+	JobID              = "job_id"
+	GenerationID       = "generation_id"
 )

+ 75 - 72
core/middleware/distributor.go

@@ -44,7 +44,7 @@ func calculateGroupConsumeLevelRatio(usedAmount float64) float64 {
 	return groupConsumeLevelRatio
 }
 
-func getGroupPMRatio(group *model.GroupCache) (float64, float64) {
+func getGroupPMRatio(group model.GroupCache) (float64, float64) {
 	groupRPMRatio := group.RPMRatio
 	if groupRPMRatio <= 0 {
 		groupRPMRatio = 1
@@ -56,7 +56,7 @@ func getGroupPMRatio(group *model.GroupCache) (float64, float64) {
 	return groupRPMRatio, groupTPMRatio
 }
 
-func GetGroupAdjustedModelConfig(group *model.GroupCache, mc model.ModelConfig) model.ModelConfig {
+func GetGroupAdjustedModelConfig(group model.GroupCache, mc model.ModelConfig) model.ModelConfig {
 	if groupModelConfig, ok := group.ModelConfigs[mc.Model]; ok {
 		mc = mc.LoadFromGroupModelConfig(groupModelConfig)
 	}
@@ -96,7 +96,7 @@ func setTpmHeaders(c *gin.Context, tpm, remainingRequests int64) {
 	c.Header(XRateLimitResetTokens, "1m0s")
 }
 
-func UpdateGroupModelRequest(c *gin.Context, group *model.GroupCache, rpm, rps int64) {
+func UpdateGroupModelRequest(c *gin.Context, group model.GroupCache, rpm, rps int64) {
 	if group.Status == model.GroupStatusInternal {
 		return
 	}
@@ -106,7 +106,7 @@ func UpdateGroupModelRequest(c *gin.Context, group *model.GroupCache, rpm, rps i
 	log.Data["group_rps"] = strconv.FormatInt(rps, 10)
 }
 
-func UpdateGroupModelTokensRequest(c *gin.Context, group *model.GroupCache, tpm, tps int64) {
+func UpdateGroupModelTokensRequest(c *gin.Context, group model.GroupCache, tpm, tps int64) {
 	if group.Status == model.GroupStatusInternal {
 		return
 	}
@@ -134,7 +134,7 @@ func UpdateGroupModelTokennameTokensRequest(c *gin.Context, tpm, tps int64) {
 
 func checkGroupModelRPMAndTPM(
 	c *gin.Context,
-	group *model.GroupCache,
+	group model.GroupCache,
 	mc model.ModelConfig,
 	tokenName string,
 ) error {
@@ -226,7 +226,7 @@ func GetGroupBalanceConsumerFromContext(c *gin.Context) *GroupBalanceConsumer {
 
 func GetGroupBalanceConsumer(
 	c *gin.Context,
-	group *model.GroupCache,
+	group model.GroupCache,
 ) (*GroupBalanceConsumer, error) {
 	gbc := GetGroupBalanceConsumerFromContext(c)
 	if gbc != nil {
@@ -243,7 +243,7 @@ func GetGroupBalanceConsumer(
 		}
 	} else {
 		log := GetLogger(c)
-		groupBalance, consumer, err := balance.GetGroupRemainBalance(c.Request.Context(), *group)
+		groupBalance, consumer, err := balance.GetGroupRemainBalance(c.Request.Context(), group)
 		if err != nil {
 			return nil, err
 		}
@@ -267,7 +267,7 @@ const (
 	GroupBalanceNotEnough = "group_balance_not_enough"
 )
 
-func checkGroupBalance(c *gin.Context, group *model.GroupCache) bool {
+func checkGroupBalance(c *gin.Context, group model.GroupCache) bool {
 	gbc, err := GetGroupBalanceConsumer(c, group)
 	if err != nil {
 		if errors.Is(err, balance.ErrNoRealNameUsedAmountLimit) {
@@ -327,44 +327,6 @@ func NewDistribute(mode mode.Mode) gin.HandlerFunc {
 	}
 }
 
-const (
-	AIProxyChannelHeader = "Aiproxy-Channel"
-)
-
-func getChannelFromHeader(
-	header string,
-	mc *model.ModelCaches,
-	availableSet []string,
-	model string,
-) (*model.Channel, error) {
-	channelIDInt, err := strconv.ParseInt(header, 10, 64)
-	if err != nil {
-		return nil, err
-	}
-
-	for _, set := range availableSet {
-		enabledChannels := mc.EnabledModel2ChannelsBySet[set][model]
-		if len(enabledChannels) > 0 {
-			for _, channel := range enabledChannels {
-				if int64(channel.ID) == channelIDInt {
-					return channel, nil
-				}
-			}
-		}
-
-		disabledChannels := mc.DisabledModel2ChannelsBySet[set][model]
-		if len(disabledChannels) > 0 {
-			for _, channel := range disabledChannels {
-				if int64(channel.ID) == channelIDInt {
-					return channel, nil
-				}
-			}
-		}
-	}
-
-	return nil, fmt.Errorf("channel %d not found for model `%s`", channelIDInt, model)
-}
-
 func CheckRelayMode(requestMode, modelMode mode.Mode) bool {
 	if modelMode == mode.Unknown {
 		return true
@@ -377,6 +339,10 @@ func CheckRelayMode(requestMode, modelMode mode.Mode) bool {
 	case mode.ImagesGenerations, mode.ImagesEdits:
 		return modelMode == mode.ImagesGenerations ||
 			modelMode == mode.ImagesEdits
+	case mode.VideoGenerationsJobs, mode.VideoGenerationsGetJobs, mode.VideoGenerationsContent:
+		return modelMode == mode.VideoGenerationsJobs ||
+			modelMode == mode.VideoGenerationsGetJobs ||
+			modelMode == mode.VideoGenerationsContent
 	default:
 		return requestMode == modelMode
 	}
@@ -393,12 +359,13 @@ func distribute(c *gin.Context, mode mode.Mode) {
 	log := GetLogger(c)
 
 	group := GetGroup(c)
+	token := GetToken(c)
 
 	if !checkGroupBalance(c, group) {
 		return
 	}
 
-	requestModel, err := getRequestModel(c, mode)
+	requestModel, err := getRequestModel(c, mode, group.ID, token.ID)
 	if err != nil {
 		AbortLogWithMessage(
 			c,
@@ -432,29 +399,17 @@ func distribute(c *gin.Context, mode mode.Mode) {
 	}
 	c.Set(ModelConfig, mc)
 
-	if channelHeader := c.Request.Header.Get(AIProxyChannelHeader); group.Status == model.GroupStatusInternal &&
-		channelHeader != "" {
-		channel, err := getChannelFromHeader(
-			channelHeader,
-			GetModelCaches(c),
-			group.GetAvailableSets(),
-			requestModel,
+	if !token.ContainsModel(requestModel) {
+		AbortLogWithMessage(
+			c,
+			http.StatusNotFound,
+			fmt.Sprintf(
+				"The model `%s` does not exist or you do not have access to it.",
+				requestModel,
+			),
+			"model_not_found",
 		)
-		if err != nil {
-			AbortLogWithMessage(c, http.StatusBadRequest, err.Error())
-			return
-		}
-		c.Set(Channel, channel)
-	} else {
-		token := GetToken(c)
-		if !token.ContainsModel(requestModel) {
-			AbortLogWithMessage(c,
-				http.StatusNotFound,
-				fmt.Sprintf("The model `%s` does not exist or you do not have access to it.", requestModel),
-				"model_not_found",
-			)
-			return
-		}
+		return
 	}
 
 	user, err := getRequestUser(c, mode)
@@ -481,8 +436,6 @@ func distribute(c *gin.Context, mode mode.Mode) {
 	}
 	c.Set(RequestMetadata, metadata)
 
-	token := GetToken(c)
-
 	if err := checkGroupModelRPMAndTPM(c, group, mc, token.Name); err != nil {
 		errMsg := err.Error()
 		consume.AsyncConsume(
@@ -542,6 +495,18 @@ func GetRequestUser(c *gin.Context) string {
 	return c.GetString(RequestUser)
 }
 
+func GetChannelID(c *gin.Context) int {
+	return c.GetInt(ChannelID)
+}
+
+func GetJobID(c *gin.Context) string {
+	return c.GetString(JobID)
+}
+
+func GetGenerationID(c *gin.Context) string {
+	return c.GetString(GenerationID)
+}
+
 func GetRequestMetadata(c *gin.Context) map[string]string {
 	return c.GetStringMapString(RequestMetadata)
 }
@@ -565,6 +530,8 @@ func NewMetaByContext(c *gin.Context,
 	modelName := GetRequestModel(c)
 	modelConfig := GetModelConfig(c)
 	requestAt := GetRequestAt(c)
+	jobID := GetJobID(c)
+	generationID := GetGenerationID(c)
 
 	opts = append(
 		opts,
@@ -573,6 +540,8 @@ func NewMetaByContext(c *gin.Context,
 		meta.WithGroup(group),
 		meta.WithToken(token),
 		meta.WithEndpoint(c.Request.URL.Path),
+		meta.WithJobID(jobID),
+		meta.WithGenerationID(generationID),
 	)
 
 	return meta.NewMeta(
@@ -585,7 +554,7 @@ func NewMetaByContext(c *gin.Context,
 }
 
 // https://platform.openai.com/docs/api-reference/chat
-func getRequestModel(c *gin.Context, m mode.Mode) (string, error) {
+func getRequestModel(c *gin.Context, m mode.Mode, groupID string, tokenID int) (string, error) {
 	path := c.Request.URL.Path
 	switch {
 	case m == mode.ParsePdf:
@@ -605,6 +574,30 @@ func getRequestModel(c *gin.Context, m mode.Mode) (string, error) {
 		// /engines/:model/embeddings
 		return c.Param("model"), nil
 
+	case m == mode.VideoGenerationsGetJobs:
+		jobID := c.Param("id")
+		store, err := model.CacheGetStore(jobID)
+		if err != nil {
+			return "", fmt.Errorf("get request model failed: %w", err)
+		}
+		if err := validateStoreGroupAndToken(store, groupID, tokenID); err != nil {
+			return "", fmt.Errorf("validate store group and token failed: %w", err)
+		}
+		c.Set(JobID, store.ID)
+		c.Set(ChannelID, store.ChannelID)
+		return store.Model, nil
+	case m == mode.VideoGenerationsContent:
+		generationID := c.Param("id")
+		store, err := model.CacheGetStore(generationID)
+		if err != nil {
+			return "", fmt.Errorf("get request model failed: %w", err)
+		}
+		if err := validateStoreGroupAndToken(store, groupID, tokenID); err != nil {
+			return "", fmt.Errorf("validate store group and token failed: %w", err)
+		}
+		c.Set(GenerationID, store.ID)
+		c.Set(ChannelID, store.ChannelID)
+		return store.Model, nil
 	default:
 		body, err := common.GetRequestBody(c.Request)
 		if err != nil {
@@ -614,6 +607,16 @@ func getRequestModel(c *gin.Context, m mode.Mode) (string, error) {
 	}
 }
 
+func validateStoreGroupAndToken(store *model.StoreCache, groupID string, tokenID int) error {
+	if store.GroupID != groupID {
+		return fmt.Errorf("store group id mismatch: %s != %s", store.GroupID, groupID)
+	}
+	if store.TokenID != tokenID {
+		return fmt.Errorf("store token id mismatch: %d != %d", store.TokenID, tokenID)
+	}
+	return nil
+}
+
 func GetModelFromJSON(body []byte) (string, error) {
 	node, err := sonic.GetWithOptions(body, ast.SearchOptions{}, "model")
 	if err != nil {

+ 8 - 8
core/middleware/mcp.go

@@ -22,21 +22,21 @@ func MCPAuth(c *gin.Context) {
 		"sk-",
 	)
 
-	var token *model.TokenCache
+	var token model.TokenCache
 	var useInternalToken bool
 	if config.AdminKey != "" && config.AdminKey == key ||
 		config.InternalToken != "" && config.InternalToken == key {
-		token = &model.TokenCache{
+		token = model.TokenCache{
 			Key: key,
 		}
 		useInternalToken = true
 	} else {
-		var err error
-		token, err = model.ValidateAndGetToken(key)
+		tokenCache, err := model.ValidateAndGetToken(key)
 		if err != nil {
 			AbortLogWithMessage(c, http.StatusUnauthorized, err.Error(), "invalid_token")
 			return
 		}
+		token = *tokenCache
 	}
 
 	SetLogTokenFields(log.Data, token, useInternalToken)
@@ -58,18 +58,18 @@ func MCPAuth(c *gin.Context) {
 		}
 	}
 
-	var group *model.GroupCache
+	var group model.GroupCache
 	if useInternalToken {
-		group = &model.GroupCache{
+		group = model.GroupCache{
 			Status: model.GroupStatusInternal,
 		}
 	} else {
-		var err error
-		group, err = model.CacheGetGroup(token.Group)
+		groupCache, err := model.CacheGetGroup(token.Group)
 		if err != nil {
 			AbortLogWithMessage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get group: %v", err))
 			return
 		}
+		group = *groupCache
 	}
 	SetLogGroupFields(log.Data, group)
 	if group.Status != model.GroupStatusEnabled && group.Status != model.GroupStatusInternal {

+ 67 - 0
core/model/cache.go

@@ -674,6 +674,73 @@ func CacheGetPublicMCPReusingParam(mcpID, groupID string) (*PublicMCPReusingPara
 	return prc, nil
 }
 
+const (
+	StoreCacheKey = "store:%s" // store_id
+)
+
+type StoreCache struct {
+	ID        string    `json:"id"         redis:"i"`
+	GroupID   string    `json:"group_id"   redis:"g"`
+	TokenID   int       `json:"token_id"   redis:"t"`
+	ChannelID int       `json:"channel_id" redis:"c"`
+	Model     string    `json:"model"      redis:"m"`
+	ExpiresAt time.Time `json:"expires_at" redis:"e"`
+}
+
+func (s *Store) ToStoreCache() *StoreCache {
+	return &StoreCache{
+		ID:        s.ID,
+		GroupID:   s.GroupID,
+		TokenID:   s.TokenID,
+		ChannelID: s.ChannelID,
+		Model:     s.Model,
+		ExpiresAt: s.ExpiresAt,
+	}
+}
+
+func CacheSetStore(store *StoreCache) error {
+	if !common.RedisEnabled {
+		return nil
+	}
+	key := fmt.Sprintf(StoreCacheKey, store.ID)
+	pipe := common.RDB.Pipeline()
+	pipe.HSet(context.Background(), key, store)
+	expireTime := SyncFrequency + time.Duration(rand.Int64N(60)-30)*time.Second
+	pipe.Expire(context.Background(), key, expireTime)
+	_, err := pipe.Exec(context.Background())
+	return err
+}
+
+func CacheGetStore(id string) (*StoreCache, error) {
+	if !common.RedisEnabled {
+		store, err := GetStore(id)
+		if err != nil {
+			return nil, err
+		}
+		return store.ToStoreCache(), nil
+	}
+
+	cacheKey := fmt.Sprintf(StoreCacheKey, id)
+	storeCache := &StoreCache{}
+	err := common.RDB.HGetAll(context.Background(), cacheKey).Scan(storeCache)
+	if err == nil && storeCache.ID != "" {
+		return storeCache, nil
+	}
+
+	store, err := GetStore(id)
+	if err != nil {
+		return nil, err
+	}
+
+	sc := store.ToStoreCache()
+
+	if err := CacheSetStore(sc); err != nil {
+		log.Error("redis set store error: " + err.Error())
+	}
+
+	return sc, nil
+}
+
 //nolint:revive
 type ModelConfigCache interface {
 	GetModelConfig(model string) (ModelConfig, bool)

+ 2 - 0
core/model/chtype.go

@@ -14,6 +14,7 @@ func (c ChannelType) String() string {
 const (
 	ChannelTypeOpenAI                  ChannelType = 1
 	ChannelTypeAzure                   ChannelType = 3
+	ChannelTypeAzure2                  ChannelType = 4
 	ChannelTypeGoogleGeminiOpenAI      ChannelType = 12
 	ChannelTypeBaiduV2                 ChannelType = 13
 	ChannelTypeAnthropic               ChannelType = 14
@@ -52,6 +53,7 @@ const (
 var channelTypeNames = map[ChannelType]string{
 	ChannelTypeOpenAI:                  "openai",
 	ChannelTypeAzure:                   "azure",
+	ChannelTypeAzure2:                  "azure (model name support contain '.')",
 	ChannelTypeGoogleGeminiOpenAI:      "google gemini (openai)",
 	ChannelTypeBaiduV2:                 "baidu v2",
 	ChannelTypeAnthropic:               "anthropic",

+ 5 - 1
core/model/log.go

@@ -334,7 +334,11 @@ func cleanLog(batchSize int) error {
 		}
 	}
 
-	return nil
+	return LogDB.
+		Model(&Store{}).
+		Where("expires_at < ?", time.Now()).
+		Delete(&Store{}).
+		Error
 }
 
 func optimizeLog() error {

+ 1 - 0
core/model/main.go

@@ -197,6 +197,7 @@ func migrateLOGDB() error {
 		&GroupSummary{},
 		&Summary{},
 		&ConsumeError{},
+		&Store{},
 	)
 	if err != nil {
 		return err

+ 61 - 0
core/model/store.go

@@ -0,0 +1,61 @@
+package model
+
+import (
+	"errors"
+	"time"
+
+	"github.com/labring/aiproxy/core/common"
+	"gorm.io/gorm"
+)
+
+const (
+	ErrStoreNotFound = "store id"
+)
+
+// Store represents channel-associated data storage for various purposes:
+// - Video generation jobs and their results
+// - File storage with associated metadata
+// - Any other channel-specific data that needs persistence
+type Store struct {
+	ID        string    `gorm:"primaryKey"`
+	CreatedAt time.Time `gorm:"autoCreateTime"`
+	ExpiresAt time.Time
+	GroupID   string
+	TokenID   int
+	ChannelID int
+	Model     string
+}
+
+func (s *Store) BeforeSave(_ *gorm.DB) error {
+	if s.GroupID != "" {
+		if s.TokenID == 0 {
+			return errors.New("token id is required")
+		}
+	}
+	if s.ChannelID == 0 {
+		return errors.New("channel id is required")
+	}
+	if s.ID == "" {
+		s.ID = common.ShortUUID()
+	}
+	if s.CreatedAt.IsZero() {
+		s.CreatedAt = time.Now()
+	}
+	if s.ExpiresAt.IsZero() {
+		s.ExpiresAt = s.CreatedAt.Add(time.Hour * 24 * 30)
+	}
+	return nil
+}
+
+func SaveStore(s *Store) (*Store, error) {
+	if err := LogDB.Save(s).Error; err != nil {
+		return nil, err
+	}
+	return s, nil
+}
+
+func GetStore(id string) (*Store, error) {
+	var s Store
+	err := LogDB.Where("id = ?", id).First(&s).Error
+	return &s, HandleNotFound(err, ErrStoreNotFound)
+}

+ 6 - 4
core/relay/adaptor/ai360/adaptor.go

@@ -1,7 +1,7 @@
 package ai360
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://ai.360.cn/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 102 - 23
core/relay/adaptor/ali/adaptor.go

@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/bytedance/sonic"
 	"github.com/bytedance/sonic/ast"
@@ -24,62 +25,131 @@ type Adaptor struct{}
 
 const baseURL = "https://dashscope.aliyuncs.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 	if u == "" {
 		u = baseURL
 	}
 	switch meta.Mode {
 	case mode.ImagesGenerations:
-		return u + "/api/v1/services/aigc/text2image/image-synthesis", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/v1/services/aigc/text2image/image-synthesis",
+		}, nil
 	case mode.ChatCompletions:
-		return u + "/compatible-mode/v1/chat/completions", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/compatible-mode/v1/chat/completions",
+		}, nil
 	case mode.Completions:
-		return u + "/compatible-mode/v1/completions", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/compatible-mode/v1/completions",
+		}, nil
 	case mode.Embeddings:
-		return u + "/compatible-mode/v1/embeddings", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/compatible-mode/v1/embeddings",
+		}, nil
 	case mode.AudioSpeech, mode.AudioTranscription:
-		return u + "/api-ws/v1/inference", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api-ws/v1/inference",
+		}, nil
 	case mode.Rerank:
-		return u + "/api/v1/services/rerank/text-rerank/text-rerank", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/v1/services/rerank/text-rerank/text-rerank",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 
 	// req.Header.Set("X-Dashscope-Plugin", meta.Channel.Config.Plugin)
 	return nil
 }
 
+// qwen3 enable_thinking must be set to false for non-streaming calls
+func patchQwen3EnableThinking(node *ast.Node) error {
+	streamNode := node.Get("stream")
+	isStreaming := false
+
+	if streamNode.Exists() {
+		streamBool, err := streamNode.Bool()
+		if err != nil {
+			return errors.New("stream is not a boolean")
+		}
+		isStreaming = streamBool
+	}
+
+	// Set enable_thinking to false for non-streaming requests
+	if !isStreaming {
+		_, err := node.Set("enable_thinking", ast.NewBool(false))
+		return err
+	}
+
+	return nil
+}
+
+// qwq only support stream mode
+func patchQwqOnlySupportStream(node *ast.Node) error {
+	_, err := node.Set("stream", ast.NewBool(true))
+	return err
+}
+
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.ImagesGenerations:
 		return ConvertImageRequest(meta, req)
 	case mode.Rerank:
 		return ConvertRerankRequest(meta, req)
-	case mode.ChatCompletions, mode.Completions, mode.Embeddings:
-		return openai.ConvertRequest(meta, req)
+	case mode.ChatCompletions:
+		if strings.HasPrefix(meta.ActualModel, "qwen3-") {
+			return openai.ConvertChatCompletionsRequest(meta, req, patchQwen3EnableThinking, false)
+		}
+		if strings.HasPrefix(meta.ActualModel, "qwq-") {
+			return openai.ConvertChatCompletionsRequest(meta, req, patchQwqOnlySupportStream, false)
+		}
+		return openai.ConvertChatCompletionsRequest(meta, req, nil, false)
+	case mode.Completions:
+		if strings.HasPrefix(meta.ActualModel, "qwen3-") {
+			return openai.ConvertCompletionsRequest(meta, req, patchQwen3EnableThinking)
+		}
+		if strings.HasPrefix(meta.ActualModel, "qwq-") {
+			return openai.ConvertCompletionsRequest(meta, req, patchQwqOnlySupportStream)
+		}
+		return openai.ConvertCompletionsRequest(meta, req, nil)
+	case mode.Embeddings:
+		return openai.ConvertRequest(meta, store, req)
 	case mode.AudioSpeech:
 		return ConvertTTSRequest(meta, req)
 	case mode.AudioTranscription:
 		return ConvertSTTRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -97,18 +167,19 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	switch meta.Mode {
 	case mode.ImagesGenerations:
 		return ImageHandler(meta, c, resp)
 	case mode.Embeddings, mode.Completions:
-		return openai.DoResponse(meta, c, resp)
+		return openai.DoResponse(meta, store, c, resp)
 	case mode.ChatCompletions:
 		reqBody, err := common.GetRequestBody(c.Request)
 		if err != nil {
-			return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+			return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 				fmt.Sprintf("get request body failed: %s", err),
 				"get_request_body_failed",
 				http.StatusInternalServerError,
@@ -116,15 +187,15 @@ func (a *Adaptor) DoResponse(
 		}
 		enableSearch, err := getEnableSearch(reqBody)
 		if err != nil {
-			return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+			return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 				fmt.Sprintf("get enable_search failed: %s", err),
 				"get_enable_search_failed",
 				http.StatusInternalServerError,
 			)
 		}
-		u, e := openai.DoResponse(meta, c, resp)
+		u, e := openai.DoResponse(meta, store, c, resp)
 		if e != nil {
-			return nil, e
+			return model.Usage{}, e
 		}
 		if enableSearch {
 			u.WebSearchCount++
@@ -137,7 +208,7 @@ func (a *Adaptor) DoResponse(
 	case mode.AudioTranscription:
 		return STTDoResponse(meta, c, resp)
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -163,6 +234,14 @@ func getEnableSearch(reqBody []byte) (bool, error) {
 	return enableSearch, nil
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"OpenAI compatibility",
+			"Network search metering support",
+			"Rerank support: https://help.aliyun.com/zh/model-studio/text-rerank-api",
+			"STT support: https://help.aliyun.com/zh/model-studio/sambert-speech-synthesis/",
+		},
+		Models: ModelList,
+	}
 }

+ 11 - 3
core/relay/adaptor/ali/embeddings.go

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -87,19 +88,23 @@ func EmbeddingsHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	log := middleware.GetLogger(c)
 
 	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(err, "read_response_body_failed", resp.StatusCode)
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
+			err,
+			"read_response_body_failed",
+			resp.StatusCode,
+		)
 	}
 	var respBody EmbeddingResponse
 	err = sonic.Unmarshal(responseBody, &respBody)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			resp.StatusCode,
@@ -117,6 +122,9 @@ func EmbeddingsHandler(
 			resp.StatusCode,
 		)
 	}
+
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(data)))
 	_, err = c.Writer.Write(data)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 0 - 14
core/relay/adaptor/ali/fetures.go

@@ -1,14 +0,0 @@
-package ali
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"OpenAI compatibility",
-		"Network search metering support",
-		"Rerank support: https://help.aliyun.com/zh/model-studio/text-rerank-api",
-		"STT support: https://help.aliyun.com/zh/model-studio/sambert-speech-synthesis/",
-	}
-}

+ 21 - 18
core/relay/adaptor/ali/image.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"io"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -27,10 +28,10 @@ const MetaResponseFormat = "response_format"
 func ConvertImageRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalImageRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 
@@ -45,12 +46,13 @@ func ConvertImageRequest(
 
 	data, err := sonic.Marshal(&imageRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
+	return adaptor.ConvertResult{
 		Header: http.Header{
 			"X-Dashscope-Async": {"enable"},
+			"Content-Type":      {"application/json"},
+			"Content-Length":    {strconv.Itoa(len(data))},
 		},
 		Body: bytes.NewReader(data),
 	}, nil
@@ -60,9 +62,9 @@ func ImageHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -74,7 +76,7 @@ func ImageHandler(
 	var aliTaskResponse TaskResponse
 	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"read_response_body_failed",
 			http.StatusInternalServerError,
@@ -82,7 +84,7 @@ func ImageHandler(
 	}
 	err = sonic.Unmarshal(responseBody, &aliTaskResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
@@ -91,7 +93,7 @@ func ImageHandler(
 
 	if aliTaskResponse.Message != "" {
 		log.Error("aliAsyncTask err: " + aliTaskResponse.Message)
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			errors.New(aliTaskResponse.Message),
 			"ali_async_task_failed",
 			http.StatusInternalServerError,
@@ -100,7 +102,7 @@ func ImageHandler(
 
 	aliResponse, err := asyncTaskWait(c, aliTaskResponse.Output.TaskID, meta.Channel.Key)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"ali_async_task_wait_failed",
 			http.StatusInternalServerError,
@@ -108,7 +110,7 @@ func ImageHandler(
 	}
 
 	if aliResponse.Output.TaskStatus != "SUCCEEDED" {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			aliResponse.Output.Message,
 			"ali_error",
 			resp.StatusCode,
@@ -118,19 +120,16 @@ func ImageHandler(
 	fullTextResponse := responseAli2OpenAIImage(c.Request.Context(), aliResponse, responseFormat)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.Usage.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
-	return &model.Usage{
-		OutputTokens: model.ZeroNullInt64(len(jsonResponse)),
-		TotalTokens:  model.ZeroNullInt64(len(jsonResponse)),
-	}, nil
+	return fullTextResponse.Usage.ToModelUsage(), nil
 }
 
 func asyncTask(ctx context.Context, taskID, key string) (*TaskResponse, error) {
@@ -229,5 +228,9 @@ func responseAli2OpenAIImage(
 			RevisedPrompt: "",
 		})
 	}
+	imageResponse.Usage = &relaymodel.ImageUsage{
+		OutputTokens: int64(len(imageResponse.Data)),
+		TotalTokens:  int64(len(imageResponse.Data)),
+	}
 	return &imageResponse
 }

+ 19 - 16
core/relay/adaptor/ali/rerank.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -31,11 +32,11 @@ type RerankUsage struct {
 func ConvertRerankRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	reqMap := make(map[string]any)
 	err := common.UnmarshalBodyReusable(req, &reqMap)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	reqMap["model"] = meta.ActualModel
 	reqMap["input"] = map[string]any{
@@ -55,12 +56,14 @@ func ConvertRerankRequest(
 	reqMap["parameters"] = parameters
 	jsonData, err := sonic.Marshal(reqMap)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(jsonData),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(jsonData))},
+		},
+		Body: bytes.NewReader(jsonData),
 	}, nil
 }
 
@@ -68,9 +71,9 @@ func RerankHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -79,7 +82,7 @@ func RerankHandler(
 
 	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"read_response_body_failed",
 			http.StatusInternalServerError,
@@ -88,15 +91,13 @@ func RerankHandler(
 	var rerankResponse RerankResponse
 	err = sonic.Unmarshal(responseBody, &rerankResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 
-	c.Writer.WriteHeader(resp.StatusCode)
-
 	rerankResp := relaymodel.RerankResponse{
 		Meta: relaymodel.RerankMeta{
 			Tokens: &relaymodel.RerankMetaTokens{
@@ -108,14 +109,14 @@ func RerankHandler(
 		ID:      rerankResponse.RequestID,
 	}
 
-	var usage *model.Usage
+	var usage model.Usage
 	if rerankResponse.Usage == nil {
-		usage = &model.Usage{
+		usage = model.Usage{
 			InputTokens: meta.RequestUsage.InputTokens,
 			TotalTokens: meta.RequestUsage.InputTokens,
 		}
 	} else {
-		usage = &model.Usage{
+		usage = model.Usage{
 			InputTokens: model.ZeroNullInt64(rerankResponse.Usage.TotalTokens),
 			TotalTokens: model.ZeroNullInt64(rerankResponse.Usage.TotalTokens),
 		}
@@ -129,6 +130,8 @@ func RerankHandler(
 			http.StatusInternalServerError,
 		)
 	}
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, err = c.Writer.Write(jsonResponse)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 9 - 10
core/relay/adaptor/ali/stt-realtime.go

@@ -69,18 +69,18 @@ type STTUsage struct {
 func ConvertSTTRequest(
 	meta *meta.Meta,
 	request *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	err := request.ParseMultipartForm(1024 * 1024 * 4)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	audioFile, _, err := request.FormFile("file")
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	audioData, err := io.ReadAll(audioFile)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	format := "mp3"
 	if request.FormValue("format") != "" {
@@ -90,7 +90,7 @@ func ConvertSTTRequest(
 	if request.FormValue("sample_rate") != "" {
 		sampleRate, err = strconv.Atoi(request.FormValue("sample_rate"))
 		if err != nil {
-			return nil, err
+			return adaptor.ConvertResult{}, err
 		}
 	}
 
@@ -115,12 +115,11 @@ func ConvertSTTRequest(
 
 	data, err := sonic.Marshal(sttRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	meta.Set("audio_data", audioData)
 	meta.Set("task_id", sttRequest.Header.TaskID)
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
+	return adaptor.ConvertResult{
 		Header: http.Header{
 			"X-DashScope-DataInspection": {"enable"},
 		},
@@ -158,7 +157,7 @@ func STTDoResponse(
 	meta *meta.Meta,
 	c *gin.Context,
 	_ *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	audioData, ok := meta.MustGet("audio_data").([]byte)
 	if !ok {
 		panic(fmt.Sprintf("audio data type error: %T, %v", audioData, audioData))
@@ -175,7 +174,7 @@ func STTDoResponse(
 
 	output := strings.Builder{}
 
-	usage = &model.Usage{}
+	usage = model.Usage{}
 
 	for {
 		messageType, data, err := conn.ReadMessage()

+ 7 - 8
core/relay/adaptor/ali/tts.go

@@ -94,14 +94,14 @@ var ttsSupportedFormat = map[string]struct{}{
 	"mp3": {},
 }
 
-func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalTTSRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	reqMap, err := utils.UnmarshalMap(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	var sampleRate int
 	sampleRateI, ok := reqMap["sample_rate"].(float64)
@@ -161,10 +161,9 @@ func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequ
 
 	data, err := sonic.Marshal(ttsRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
+	return adaptor.ConvertResult{
 		Header: http.Header{
 			"X-DashScope-DataInspection": {"enable"},
 		},
@@ -203,7 +202,7 @@ func TTSDoResponse(
 	meta *meta.Meta,
 	c *gin.Context,
 	_ *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	log := middleware.GetLogger(c)
 
 	conn, ok := meta.MustGet("ws_conn").(*websocket.Conn)
@@ -212,7 +211,7 @@ func TTSDoResponse(
 	}
 	defer conn.Close()
 
-	usage = &model.Usage{}
+	usage = model.Usage{}
 
 	for {
 		messageType, data, err := conn.ReadMessage()

+ 35 - 16
core/relay/adaptor/anthropic/adaptor.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/bytedance/sonic"
@@ -20,17 +21,25 @@ type Adaptor struct{}
 
 const baseURL = "https://api.anthropic.com/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
-	return meta.Channel.BaseURL + "/messages", nil
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    meta.Channel.BaseURL + "/messages",
+	}, nil
 }
 
 const AnthropicVersion = "2023-06-01"
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	c *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("X-Api-Key", meta.Channel.Key)
 	anthropicVersion := c.Request.Header.Get("Anthropic-Version")
 	if anthropicVersion == "" {
@@ -59,33 +68,37 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.ChatCompletions:
 		data, err := OpenAIConvertRequest(meta, req)
 		if err != nil {
-			return nil, err
+			return adaptor.ConvertResult{}, err
 		}
 
 		data2, err := sonic.Marshal(data)
 		if err != nil {
-			return nil, err
+			return adaptor.ConvertResult{}, err
 		}
-		return &adaptor.ConvertRequestResult{
-			Method: http.MethodPost,
-			Header: nil,
-			Body:   bytes.NewReader(data2),
+		return adaptor.ConvertResult{
+			Header: http.Header{
+				"Content-Type":   {"application/json"},
+				"Content-Length": {strconv.Itoa(len(data2))},
+			},
+			Body: bytes.NewReader(data2),
 		}, nil
 	case mode.Anthropic:
 		return ConvertRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -94,9 +107,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.ChatCompletions:
 		if utils.IsStreamResponse(resp) {
@@ -111,7 +125,7 @@ func (a *Adaptor) DoResponse(
 			usage, err = Handler(meta, c, resp)
 		}
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -120,6 +134,11 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Support native Endpoint: /v1/messages",
+		},
+		Models: ModelList,
+	}
 }

+ 0 - 11
core/relay/adaptor/anthropic/fetures.go

@@ -1,11 +0,0 @@
-package anthropic
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"Support native Endpoint: /v1/messages",
-	}
-}

+ 21 - 25
core/relay/adaptor/anthropic/main.go

@@ -7,10 +7,10 @@ import (
 	"errors"
 	"io"
 	"net/http"
+	"strconv"
 	"strings"
 	"sync"
 
-	"github.com/bytedance/sonic"
 	"github.com/bytedance/sonic/ast"
 	"github.com/gin-gonic/gin"
 	"github.com/labring/aiproxy/core/common"
@@ -25,35 +25,37 @@ import (
 	"golang.org/x/sync/semaphore"
 )
 
-func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	// Parse request body into AST node
 	node, err := common.UnmarshalBody2Node(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	// Set the actual model in the request
 	_, err = node.Set("model", ast.NewString(meta.ActualModel))
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	// Process image content if present
 	err = ConvertImage2Base64(req.Context(), &node)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	// Serialize the modified node
 	newBody, err := node.MarshalJSON()
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(newBody),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(newBody))},
+		},
+		Body: bytes.NewReader(newBody),
 	}, nil
 }
 
@@ -160,9 +162,9 @@ func StreamHandler(
 	m *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHandler(resp)
+		return model.Usage{}, ErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
@@ -240,34 +242,28 @@ func StreamHandler(
 	return usage.ToModelUsage(), nil
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHandler(resp)
+		return model.Usage{}, ErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
 
 	respBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperAnthropicError(
+		return model.Usage{}, relaymodel.WrapperAnthropicError(
 			err,
 			"read_response_failed",
 			http.StatusInternalServerError,
 		)
 	}
 
-	var claudeResponse Response
-	err = sonic.Unmarshal(respBody, &claudeResponse)
-	if err != nil {
-		return nil, relaymodel.WrapperAnthropicError(
-			err,
-			"unmarshal_response_body_failed",
-			http.StatusInternalServerError,
-		)
+	fullTextResponse, adaptorErr := Response2OpenAI(meta, respBody)
+	if adaptorErr != nil {
+		return model.Usage{}, adaptorErr
 	}
-	fullTextResponse := Response2OpenAI(meta, &claudeResponse)
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(respBody)))
 	_, _ = c.Writer.Write(respBody)
 	return fullTextResponse.ToModelUsage(), nil
 }

+ 2 - 2
core/relay/adaptor/anthropic/model.go

@@ -151,8 +151,8 @@ type ServerToolUse struct {
 	ExecutionTimeSeconds float64 `json:"execution_time_seconds,omitempty"`
 }
 
-func (u *Usage) ToOpenAIUsage() *relaymodel.Usage {
-	usage := &relaymodel.Usage{
+func (u *Usage) ToOpenAIUsage() relaymodel.Usage {
+	usage := relaymodel.Usage{
 		PromptTokens:     u.InputTokens + u.CacheReadInputTokens + u.CacheCreationInputTokens,
 		CompletionTokens: u.OutputTokens,
 		PromptTokensDetails: &relaymodel.PromptTokensDetails{

+ 46 - 26
core/relay/adaptor/anthropic/openai.go

@@ -4,7 +4,9 @@ import (
 	"bufio"
 	"context"
 	"errors"
+	"io"
 	"net/http"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -339,10 +341,12 @@ func StreamResponse2OpenAI(
 		if claudeResponse.Message == nil {
 			return nil, nil
 		}
-		usage = claudeResponse.Message.Usage.ToOpenAIUsage()
+		openAIUsage := claudeResponse.Message.Usage.ToOpenAIUsage()
+		usage = &openAIUsage
 	case "message_delta":
 		if claudeResponse.Usage != nil {
-			usage = claudeResponse.Usage.ToOpenAIUsage()
+			openAIUsage := claudeResponse.Usage.ToOpenAIUsage()
+			usage = &openAIUsage
 		}
 		if claudeResponse.Delta != nil && claudeResponse.Delta.StopReason != nil {
 			stopReason = *claudeResponse.Delta.StopReason
@@ -361,7 +365,7 @@ func StreamResponse2OpenAI(
 
 	openaiResponse := relaymodel.ChatCompletionsStreamResponse{
 		ID:      openai.ChatCompletionID(),
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Created: time.Now().Unix(),
 		Model:   meta.OriginModel,
 		Usage:   usage,
@@ -371,7 +375,27 @@ func StreamResponse2OpenAI(
 	return &openaiResponse, nil
 }
 
-func Response2OpenAI(meta *meta.Meta, claudeResponse *Response) *relaymodel.TextResponse {
+func Response2OpenAI(
+	meta *meta.Meta,
+	respData []byte,
+) (*relaymodel.TextResponse, adaptor.Error) {
+	var claudeResponse Response
+	err := sonic.Unmarshal(respData, &claudeResponse)
+	if err != nil {
+		return nil, relaymodel.WrapperOpenAIError(
+			err,
+			"unmarshal_response_body_failed",
+			http.StatusInternalServerError,
+		)
+	}
+
+	if claudeResponse.Type == "error" {
+		return nil, OpenAIErrorHandlerWithBody(
+			http.StatusBadRequest,
+			respData,
+		)
+	}
+
 	var content string
 	var thinking string
 	tools := make([]*relaymodel.Tool, 0)
@@ -412,32 +436,25 @@ func Response2OpenAI(meta *meta.Meta, claudeResponse *Response) *relaymodel.Text
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: []*relaymodel.TextResponseChoice{&choice},
-		Usage: relaymodel.Usage{
-			PromptTokens:     claudeResponse.Usage.InputTokens + claudeResponse.Usage.CacheReadInputTokens + claudeResponse.Usage.CacheCreationInputTokens,
-			CompletionTokens: claudeResponse.Usage.OutputTokens,
-			PromptTokensDetails: &relaymodel.PromptTokensDetails{
-				CachedTokens:        claudeResponse.Usage.CacheReadInputTokens,
-				CacheCreationTokens: claudeResponse.Usage.CacheCreationInputTokens,
-			},
-		},
+		Usage:   claudeResponse.Usage.ToOpenAIUsage(),
 	}
 	if fullTextResponse.PromptTokens == 0 {
 		fullTextResponse.PromptTokens = int64(meta.RequestUsage.InputTokens)
 	}
 	fullTextResponse.TotalTokens = fullTextResponse.PromptTokens + fullTextResponse.CompletionTokens
-	return &fullTextResponse
+	return &fullTextResponse, nil
 }
 
 func OpenAIStreamHandler(
 	m *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, OpenAIErrorHandler(resp)
+		return model.Usage{}, OpenAIErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
@@ -519,7 +536,7 @@ func OpenAIStreamHandler(
 		_ = render.ObjectData(c, &relaymodel.ChatCompletionsStreamResponse{
 			ID:      openai.ChatCompletionID(),
 			Model:   m.OriginModel,
-			Object:  relaymodel.ChatCompletionChunk,
+			Object:  relaymodel.ChatCompletionChunkObject,
 			Created: time.Now().Unix(),
 			Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{},
 			Usage:   usage,
@@ -535,33 +552,36 @@ func OpenAIHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, OpenAIErrorHandler(resp)
+		return model.Usage{}, OpenAIErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
 
-	var claudeResponse Response
-	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&claudeResponse)
+	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
-			"unmarshal_response_body_failed",
+			"read_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
-	fullTextResponse := Response2OpenAI(meta, &claudeResponse)
+	fullTextResponse, adaptorErr := Response2OpenAI(meta, body)
+	if adaptorErr != nil {
+		return model.Usage{}, adaptorErr
+	}
+
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

+ 35 - 17
core/relay/adaptor/aws/adaptor.go

@@ -15,30 +15,32 @@ import (
 
 type Adaptor struct{}
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return ""
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
-	adaptor := GetAdaptor(meta.ActualModel)
-	if adaptor == nil {
-		return nil, errors.New("adaptor not found")
+) (adaptor.ConvertResult, error) {
+	aa := GetAdaptor(meta.ActualModel)
+	if aa == nil {
+		return adaptor.ConvertResult{}, errors.New("adaptor not found")
 	}
-	meta.Set("awsAdapter", adaptor)
-	return adaptor.ConvertRequest(meta, req)
+	meta.Set("awsAdapter", aa)
+	return aa.ConvertRequest(meta, store, req)
 }
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	_ *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	adaptor, ok := meta.Get("awsAdapter")
 	if !ok {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"awsAdapter not found",
 			nil,
 			http.StatusInternalServerError,
@@ -48,25 +50,41 @@ func (a *Adaptor) DoResponse(
 	if !ok {
 		panic(fmt.Sprintf("aws adapter type error: %T, %v", v, v))
 	}
-	return v.DoResponse(meta, c)
+	return v.DoResponse(meta, store, c)
 }
 
-func (a *Adaptor) GetModelList() (models []model.ModelConfig) {
-	models = make([]model.ModelConfig, 0, len(adaptors))
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	models := make([]model.ModelConfig, 0, len(adaptors))
 	for _, model := range adaptors {
 		models = append(models, model.config)
 	}
-	return
+	return adaptor.Metadata{
+		Models:  models,
+		KeyHelp: "region|ak|sk",
+	}
 }
 
-func (a *Adaptor) GetRequestURL(_ *meta.Meta) (string, error) {
-	return "", nil
+func (a *Adaptor) GetRequestURL(_ *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    "",
+	}, nil
 }
 
-func (a *Adaptor) SetupRequestHeader(_ *meta.Meta, _ *gin.Context, _ *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	_ *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	_ *http.Request,
+) error {
 	return nil
 }
 
-func (a *Adaptor) DoRequest(_ *meta.Meta, _ *gin.Context, _ *http.Request) (*http.Response, error) {
+func (a *Adaptor) DoRequest(
+	_ *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	_ *http.Request,
+) (*http.Response, error) {
 	return nil, nil
 }

+ 6 - 5
core/relay/adaptor/aws/claude/adapter.go

@@ -18,16 +18,16 @@ type Adaptor struct{}
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	r, err := anthropic.OpenAIConvertRequest(meta, req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	meta.Set("stream", r.Stream)
 	meta.Set(ConvertedRequest, r)
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
+	return adaptor.ConvertResult{
 		Header: nil,
 		Body:   nil,
 	}, nil
@@ -35,8 +35,9 @@ func (a *Adaptor) ConvertRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	if meta.GetBool("stream") {
 		usage, err = StreamHandler(meta, c)
 	} else {

+ 26 - 23
core/relay/adaptor/aws/claude/main.go

@@ -4,6 +4,7 @@ package aws
 import (
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -93,10 +94,10 @@ func awsModelID(requestModel string) (string, error) {
 	return "", errors.Errorf("model %s not found", requestModel)
 }
 
-func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error) {
 	awsModelID, err := awsModelID(meta.ActualModel)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -111,7 +112,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	convReq, ok := meta.Get(ConvertedRequest)
 	if !ok {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"request not found",
 			nil,
 			http.StatusInternalServerError,
@@ -125,7 +126,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 		AnthropicVersion: "bedrock-2023-05-31",
 	}
 	if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -134,7 +135,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsReq.Body, err = sonic.Marshal(awsClaudeReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -143,7 +144,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsClient, err := utils.AwsClientFromMeta(meta)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -152,33 +153,38 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsResp, err := awsClient.InvokeModel(c.Request.Context(), awsReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
 		)
 	}
 
-	claudeResponse := new(anthropic.Response)
-	err = sonic.Unmarshal(awsResp.Body, claudeResponse)
+	openaiResp, adaptorErr := anthropic.Response2OpenAI(meta, awsResp.Body)
+	if adaptorErr != nil {
+		return model.Usage{}, adaptorErr
+	}
+
+	jsonBody, err := sonic.Marshal(openaiResp)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return openaiResp.ToModelUsage(), relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
 		)
 	}
 
-	openaiResp := anthropic.Response2OpenAI(meta, claudeResponse)
-	c.JSON(http.StatusOK, openaiResp)
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonBody)))
+	_, _ = c.Writer.Write(jsonBody)
 	return openaiResp.ToModelUsage(), nil
 }
 
-func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
+func StreamHandler(m *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error) {
 	log := middleware.GetLogger(c)
 	awsModelID, err := awsModelID(m.ActualModel)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -193,7 +199,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	convReq, ok := m.Get(ConvertedRequest)
 	if !ok {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"request not found",
 			nil,
 			http.StatusInternalServerError,
@@ -207,7 +213,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 		AnthropicVersion: "bedrock-2023-05-31",
 	}
 	if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -215,7 +221,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 	}
 	awsReq.Body, err = sonic.Marshal(awsClaudeReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -224,7 +230,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsClient, err := utils.AwsClientFromMeta(m)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -233,7 +239,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsResp, err := awsClient.InvokeModelWithResponseStream(c.Request.Context(), awsReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -246,9 +252,6 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 	responseText := strings.Builder{}
 	var writed bool
 
-	// c.Stream(func(_ io.Writer) bool {
-
-	// })
 	for event := range stream.Events() {
 		switch v := event.(type) {
 		case *types.ResponseStreamMemberChunk:
@@ -307,7 +310,7 @@ func StreamHandler(m *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 		_ = render.ObjectData(c, &relaymodel.ChatCompletionsStreamResponse{
 			ID:      openai.ChatCompletionID(),
 			Model:   m.OriginModel,
-			Object:  relaymodel.ChatCompletionChunk,
+			Object:  relaymodel.ChatCompletionChunkObject,
 			Created: time.Now().Unix(),
 			Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{},
 			Usage:   usage,

+ 0 - 4
core/relay/adaptor/aws/key.go

@@ -14,7 +14,3 @@ func (a *Adaptor) ValidateKey(key string) error {
 	}
 	return nil
 }
-
-func (a *Adaptor) KeyHelp() string {
-	return "region|ak|sk"
-}

+ 6 - 5
core/relay/adaptor/aws/llama3/adapter.go

@@ -18,18 +18,18 @@ type Adaptor struct{}
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	request, err := relayutils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 	meta.Set("stream", request.Stream)
 	llamaReq := ConvertRequest(request)
 	meta.Set(ConvertedRequest, llamaReq)
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
+	return adaptor.ConvertResult{
 		Header: nil,
 		Body:   nil,
 	}, nil
@@ -37,8 +37,9 @@ func (a *Adaptor) ConvertRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	if meta.GetBool("stream") {
 		usage, err = StreamHandler(meta, c)
 	} else {

+ 37 - 26
core/relay/adaptor/aws/llama3/main.go

@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"io"
 	"net/http"
+	"strconv"
 	"text/template"
 	"time"
 
@@ -90,10 +91,10 @@ func ConvertRequest(textRequest *relaymodel.GeneralOpenAIRequest) *Request {
 	return &llamaRequest
 }
 
-func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error) {
 	awsModelID, err := awsModelID(meta.ActualModel)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -108,7 +109,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	llamaReq, ok := meta.Get(ConvertedRequest)
 	if !ok {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"request not found",
 			nil,
 			http.StatusInternalServerError,
@@ -117,7 +118,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsReq.Body, err = sonic.Marshal(llamaReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -126,7 +127,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsClient, err := utils.AwsClientFromMeta(meta)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -135,7 +136,7 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 
 	awsResp, err := awsClient.InvokeModel(c.Request.Context(), awsReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -145,27 +146,31 @@ func Handler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
 	var llamaResponse Response
 	err = sonic.Unmarshal(awsResp.Body, &llamaResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
 		)
 	}
 
-	openaiResp := ResponseLlama2OpenAI(&llamaResponse)
-	openaiResp.Model = meta.OriginModel
-	usage := relaymodel.Usage{
-		PromptTokens:     llamaResponse.PromptTokenCount,
-		CompletionTokens: llamaResponse.GenerationTokenCount,
-		TotalTokens:      llamaResponse.PromptTokenCount + llamaResponse.GenerationTokenCount,
+	openaiResp := ResponseLlama2OpenAI(meta, llamaResponse)
+
+	jsonData, err := sonic.Marshal(llamaResponse)
+	if err != nil {
+		return openaiResp.ToModelUsage(), relaymodel.WrapperOpenAIErrorWithMessage(
+			err.Error(),
+			nil,
+			http.StatusInternalServerError,
+		)
 	}
-	openaiResp.Usage = usage
 
-	c.JSON(http.StatusOK, openaiResp)
-	return usage.ToModelUsage(), nil
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
+	_, _ = c.Writer.Write(jsonData)
+	return openaiResp.ToModelUsage(), nil
 }
 
-func ResponseLlama2OpenAI(llamaResponse *Response) *relaymodel.TextResponse {
+func ResponseLlama2OpenAI(meta *meta.Meta, llamaResponse Response) relaymodel.TextResponse {
 	var responseText string
 	if len(llamaResponse.Generation) > 0 {
 		responseText = llamaResponse.Generation
@@ -181,20 +186,26 @@ func ResponseLlama2OpenAI(llamaResponse *Response) *relaymodel.TextResponse {
 	}
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: []*relaymodel.TextResponseChoice{&choice},
+		Model:   meta.OriginModel,
+		Usage: relaymodel.Usage{
+			PromptTokens:     llamaResponse.PromptTokenCount,
+			CompletionTokens: llamaResponse.GenerationTokenCount,
+			TotalTokens:      llamaResponse.PromptTokenCount + llamaResponse.GenerationTokenCount,
+		},
 	}
-	return &fullTextResponse
+	return fullTextResponse
 }
 
-func StreamHandler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error) {
+func StreamHandler(meta *meta.Meta, c *gin.Context) (model.Usage, adaptor.Error) {
 	log := middleware.GetLogger(c)
 
 	createdTime := time.Now().Unix()
 	awsModelID, err := awsModelID(meta.ActualModel)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -209,7 +220,7 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error
 
 	llamaReq, ok := meta.Get(ConvertedRequest)
 	if !ok {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"request not found",
 			nil,
 			http.StatusInternalServerError,
@@ -218,7 +229,7 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error
 
 	awsReq.Body, err = sonic.Marshal(llamaReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -227,7 +238,7 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error
 
 	awsClient, err := utils.AwsClientFromMeta(meta)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -236,7 +247,7 @@ func StreamHandler(meta *meta.Meta, c *gin.Context) (*model.Usage, adaptor.Error
 
 	awsResp, err := awsClient.InvokeModelWithResponseStream(c.Request.Context(), awsReq)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -303,7 +314,7 @@ func StreamResponseLlama2OpenAI(
 		choice.FinishReason = finishReason
 	}
 	var openaiResponse relaymodel.ChatCompletionsStreamResponse
-	openaiResponse.Object = relaymodel.ChatCompletionChunk
+	openaiResponse.Object = relaymodel.ChatCompletionChunkObject
 	openaiResponse.Choices = []*relaymodel.ChatCompletionsStreamResponseChoice{&choice}
 	return &openaiResponse
 }

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

@@ -16,8 +16,16 @@ import (
 )
 
 type AwsAdapter interface {
-	ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error)
-	DoResponse(meta *meta.Meta, c *gin.Context) (usage *model.Usage, err adaptor.Error)
+	ConvertRequest(
+		meta *meta.Meta,
+		store adaptor.Store,
+		req *http.Request,
+	) (adaptor.ConvertResult, error)
+	DoResponse(
+		meta *meta.Meta,
+		store adaptor.Store,
+		c *gin.Context,
+	) (usage model.Usage, err adaptor.Error)
 }
 
 type AwsConfig struct {

+ 4 - 8
core/relay/adaptor/azure/key.go

@@ -10,23 +10,19 @@ import (
 var _ adaptor.KeyValidator = (*Adaptor)(nil)
 
 func (a *Adaptor) ValidateKey(key string) error {
-	_, _, err := getTokenAndAPIVersion(key)
+	_, _, err := GetTokenAndAPIVersion(key)
 	if err != nil {
 		return err
 	}
 	return nil
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "key or key|api-version"
-}
-
-const defaultAPIVersion = "2024-12-01-preview"
+const DefaultAPIVersion = "2025-04-01-preview"
 
-func getTokenAndAPIVersion(key string) (string, string, error) {
+func GetTokenAndAPIVersion(key string) (string, string, error) {
 	split := strings.Split(key, "|")
 	if len(split) == 1 {
-		return key, defaultAPIVersion, nil
+		return key, DefaultAPIVersion, nil
 	}
 	if len(split) != 2 {
 		return "", "", errors.New("invalid key format")

+ 116 - 44
core/relay/adaptor/azure/main.go

@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 	"github.com/labring/aiproxy/core/relay/meta"
 	"github.com/labring/aiproxy/core/relay/mode"
@@ -15,74 +16,145 @@ type Adaptor struct {
 	openai.Adaptor
 }
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return "https://{resource_name}.openai.azure.com"
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
-	_, apiVersion, err := getTokenAndAPIVersion(meta.Channel.Key)
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return GetRequestURL(meta, true)
+}
+
+func GetRequestURL(meta *meta.Meta, replaceDot bool) (adaptor.RequestURL, error) {
+	_, apiVersion, err := GetTokenAndAPIVersion(meta.Channel.Key)
 	if err != nil {
-		return "", err
+		return adaptor.RequestURL{}, err
+	}
+	model := meta.ActualModel
+	if replaceDot {
+		model = strings.ReplaceAll(model, ".", "")
 	}
-	model := strings.ReplaceAll(meta.ActualModel, ".", "")
 	switch meta.Mode {
 	case mode.ImagesGenerations:
 		// https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api
 		// https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2024-03-01-preview
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/images/generations?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/images/generations?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
 	case mode.AudioTranscription:
 		// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/audio/transcriptions?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/audio/transcriptions?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
 	case mode.AudioSpeech:
 		// https://learn.microsoft.com/en-us/azure/ai-services/openai/text-to-speech-quickstart?tabs=command-line#rest-api
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/audio/speech?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/audio/speech?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
 	case mode.ChatCompletions:
 		// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/chat/completions?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/chat/completions?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
 	case mode.Completions:
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/completions?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/completions?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
 	case mode.Embeddings:
-		return fmt.Sprintf(
-			"%s/openai/deployments/%s/embeddings?api-version=%s",
-			meta.Channel.BaseURL,
-			model,
-			apiVersion,
-		), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/deployments/%s/embeddings?api-version=%s",
+				meta.Channel.BaseURL,
+				model,
+				apiVersion,
+			),
+		}, nil
+	case mode.VideoGenerationsJobs:
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/v1/video/generations/jobs?api-version=%s",
+				meta.Channel.BaseURL,
+				apiVersion,
+			),
+		}, nil
+	case mode.VideoGenerationsGetJobs:
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/v1/video/generations/jobs/%s?api-version=%s",
+				meta.Channel.BaseURL,
+				meta.JobID,
+				apiVersion,
+			),
+		}, nil
+	case mode.VideoGenerationsContent:
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL: fmt.Sprintf(
+				"%s/openai/v1/video/generations/%s/content/video?api-version=%s",
+				meta.Channel.BaseURL,
+				meta.GenerationID,
+				apiVersion,
+			),
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
-	token, _, err := getTokenAndAPIVersion(meta.Channel.Key)
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
+	token, _, err := GetTokenAndAPIVersion(meta.Channel.Key)
 	if err != nil {
 		return err
 	}
 	req.Header.Set("Api-Key", token)
 	return nil
 }
+
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Model names do not contain '.' character, dots will be removed",
+			"For example: gpt-3.5-turbo becomes gpt-35-turbo",
+			fmt.Sprintf("API version is optional, default is '%s'", DefaultAPIVersion),
+		},
+		KeyHelp: "key or key|api-version",
+		Models:  openai.ModelList,
+	}
+}

+ 29 - 0
core/relay/adaptor/azure2/main.go

@@ -0,0 +1,29 @@
+package azure2
+
+import (
+	"fmt"
+
+	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/adaptor/azure"
+	"github.com/labring/aiproxy/core/relay/adaptor/openai"
+	"github.com/labring/aiproxy/core/relay/meta"
+)
+
+type Adaptor struct {
+	azure.Adaptor
+}
+
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return azure.GetRequestURL(meta, false)
+}
+
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Model names can contain '.' character",
+			fmt.Sprintf("API version is optional, default is '%s'", azure.DefaultAPIVersion),
+		},
+		KeyHelp: "key or key|api-version",
+		Models:  openai.ModelList,
+	}
+}

+ 6 - 4
core/relay/adaptor/baichuan/adaptor.go

@@ -1,7 +1,7 @@
 package baichuan
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.baichuan-ai.com/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 27 - 18
core/relay/adaptor/baidu/adaptor.go

@@ -22,7 +22,7 @@ const (
 	baseURL = "https://aip.baidubce.com"
 )
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
@@ -48,12 +48,7 @@ var modelEndpointMap = map[string]string{
 	"Fuyu-8B":              "fuyu_8b",
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
-	// Build base URL
-	if meta.Channel.BaseURL == "" {
-		meta.Channel.BaseURL = baseURL
-	}
-
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	// Get API path suffix based on mode
 	var pathSuffix string
 	switch meta.Mode {
@@ -76,10 +71,18 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 	fullURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s/%s",
 		meta.Channel.BaseURL, pathSuffix, modelEndpoint)
 
-	return fullURL, nil
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    fullURL,
+	}, nil
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 	accessToken, err := GetAccessToken(context.Background(), meta.Channel.Key)
 	if err != nil {
@@ -91,24 +94,26 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.Embeddings:
-		return openai.ConvertEmbeddingsRequest(meta, req, true)
+		return openai.ConvertEmbeddingsRequest(meta, req, nil, true)
 	case mode.Rerank:
-		return openai.ConvertRequest(meta, req)
+		return openai.ConvertRequest(meta, store, req)
 	case mode.ImagesGenerations:
-		return openai.ConvertRequest(meta, req)
+		return openai.ConvertRequest(meta, store, req)
 	case mode.ChatCompletions:
 		return ConvertRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -117,9 +122,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.Embeddings:
 		usage, err = EmbeddingsHandler(meta, c, resp)
@@ -134,7 +140,7 @@ func (a *Adaptor) DoResponse(
 			usage, err = Handler(meta, c, resp)
 		}
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			nil,
 			http.StatusBadRequest,
@@ -143,6 +149,9 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		KeyHelp: "client_id|client_secret",
+		Models:  ModelList,
+	}
 }

+ 6 - 3
core/relay/adaptor/baidu/embeddings.go

@@ -3,6 +3,7 @@ package baidu
 import (
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -22,14 +23,14 @@ func EmbeddingsHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	log := middleware.GetLogger(c)
 
 	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -38,7 +39,7 @@ func EmbeddingsHandler(
 	var baiduResponse EmbeddingsResponse
 	err = sonic.Unmarshal(body, &baiduResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -68,6 +69,8 @@ func EmbeddingsHandler(
 			http.StatusInternalServerError,
 		)
 	}
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(data)))
 	_, err = c.Writer.Write(data)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 7 - 4
core/relay/adaptor/baidu/image.go

@@ -3,6 +3,7 @@ package baidu
 import (
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -24,14 +25,14 @@ type ImageResponse struct {
 	Created int64        `json:"created"`
 }
 
-func ImageHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func ImageHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	log := middleware.GetLogger(c)
 
 	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
@@ -40,14 +41,14 @@ func ImageHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usa
 	var imageResponse ImageResponse
 	err = sonic.Unmarshal(body, &imageResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			err.Error(),
 			nil,
 			http.StatusInternalServerError,
 		)
 	}
 
-	usage := &model.Usage{
+	usage := model.Usage{
 		InputTokens: model.ZeroNullInt64(len(imageResponse.Data)),
 		TotalTokens: model.ZeroNullInt64(len(imageResponse.Data)),
 	}
@@ -65,6 +66,8 @@ func ImageHandler(_ *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usa
 			http.StatusInternalServerError,
 		)
 	}
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(data)))
 	_, err = c.Writer.Write(data)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 0 - 4
core/relay/adaptor/baidu/key.go

@@ -14,10 +14,6 @@ func (a *Adaptor) ValidateKey(key string) error {
 	return err
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "client_id|client_secret"
-}
-
 // key格式: client_id|client_secret
 func getClientIDAndSecret(key string) (string, string, error) {
 	parts := strings.Split(key, "|")

+ 18 - 15
core/relay/adaptor/baidu/main.go

@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -38,10 +39,10 @@ type ChatRequest struct {
 	EnableCitation  bool                  `json:"enable_citation,omitempty"`
 }
 
-func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 	baiduRequest := ChatRequest{
@@ -78,12 +79,14 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 
 	data, err := sonic.Marshal(baiduRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
@@ -98,7 +101,7 @@ func response2OpenAI(meta *meta.Meta, response *ChatResponse) *relaymodel.TextRe
 	}
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      response.ID,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: response.Created,
 		Model:   meta.OriginModel,
 		Choices: []*relaymodel.TextResponseChoice{&choice},
@@ -120,7 +123,7 @@ func streamResponse2OpenAI(
 	}
 	response := relaymodel.ChatCompletionsStreamResponse{
 		ID:      baiduResponse.ID,
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Created: baiduResponse.Created,
 		Model:   meta.OriginModel,
 		Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{&choice},
@@ -133,7 +136,7 @@ func StreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	log := middleware.GetLogger(c)
@@ -177,32 +180,32 @@ func StreamHandler(
 	return usage.ToModelUsage(), nil
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	var baiduResponse ChatResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&baiduResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	if baiduResponse.Error != nil && baiduResponse.ErrorCode != 0 {
-		return nil, ErrorHandler(baiduResponse.Error)
+		return model.Usage{}, ErrorHandler(baiduResponse.Error)
 	}
 	fullTextResponse := response2OpenAI(meta, &baiduResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

+ 7 - 4
core/relay/adaptor/baidu/rerank.go

@@ -3,6 +3,7 @@ package baidu
 import (
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -22,14 +23,14 @@ func RerankHandler(
 	_ *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	log := middleware.GetLogger(c)
 
 	respBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"read_response_body_failed",
 			http.StatusInternalServerError,
@@ -38,14 +39,14 @@ func RerankHandler(
 	reRankResp := &RerankResponse{}
 	err = sonic.Unmarshal(respBody, reRankResp)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	if reRankResp.Error != nil && reRankResp.Error.ErrorCode != 0 {
-		return nil, ErrorHandler(reRankResp.Error)
+		return model.Usage{}, ErrorHandler(reRankResp.Error)
 	}
 	respMap := make(map[string]any)
 	err = sonic.Unmarshal(respBody, &respMap)
@@ -74,6 +75,8 @@ func RerankHandler(
 			http.StatusInternalServerError,
 		)
 	}
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonData)))
 	_, err = c.Writer.Write(jsonData)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 31 - 14
core/relay/adaptor/baiduv2/adaptor.go

@@ -22,7 +22,7 @@ const (
 	baseURL = "https://qianfan.baidubce.com/v2"
 )
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
@@ -39,18 +39,29 @@ func toV2ModelName(modelName string) string {
 	return strings.ToLower(modelName)
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	switch meta.Mode {
 	case mode.ChatCompletions:
-		return meta.Channel.BaseURL + "/chat/completions", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    meta.Channel.BaseURL + "/chat/completions",
+		}, nil
 	case mode.Rerank:
-		return meta.Channel.BaseURL + "/rerankers", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    meta.Channel.BaseURL + "/rerankers",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	token, err := GetBearerToken(context.Background(), meta.Channel.Key)
 	if err != nil {
 		return err
@@ -61,8 +72,9 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.ChatCompletions, mode.Rerank:
 		actModel := meta.ActualModel
@@ -71,14 +83,15 @@ func (a *Adaptor) ConvertRequest(
 			meta.ActualModel = v2Model
 			defer func() { meta.ActualModel = actModel }()
 		}
-		return openai.ConvertRequest(meta, req)
+		return openai.ConvertRequest(meta, store, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -87,14 +100,15 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.ChatCompletions, mode.Rerank:
-		return openai.DoResponse(meta, c, resp)
+		return openai.DoResponse(meta, store, c, resp)
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			nil,
 			http.StatusBadRequest,
@@ -102,6 +116,9 @@ func (a *Adaptor) DoResponse(
 	}
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		KeyHelp: "ak|sk",
+		Models:  ModelList,
+	}
 }

+ 0 - 4
core/relay/adaptor/baiduv2/key.go

@@ -14,10 +14,6 @@ func (a *Adaptor) ValidateKey(key string) error {
 	return err
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "ak|sk"
-}
-
 // key格式: ak|sk
 func getAKAndSK(key string) (string, string, error) {
 	parts := strings.Split(key, "|")

+ 24 - 9
core/relay/adaptor/cloudflare/adaptor.go

@@ -2,9 +2,10 @@ package cloudflare
 
 import (
 	"fmt"
+	"net/http"
 	"strings"
 
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 	"github.com/labring/aiproxy/core/relay/meta"
 	"github.com/labring/aiproxy/core/relay/mode"
@@ -16,7 +17,7 @@ type Adaptor struct {
 
 const baseURL = "https://api.cloudflare.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
@@ -28,7 +29,7 @@ func isAIGateWay(baseURL string) bool {
 		strings.HasSuffix(baseURL, "/workers-ai")
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 	isAIGateWay := isAIGateWay(u)
 	var urlPrefix string
@@ -40,17 +41,31 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 
 	switch meta.Mode {
 	case mode.ChatCompletions:
-		return urlPrefix + "/v1/chat/completions", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    urlPrefix + "/v1/chat/completions",
+		}, nil
 	case mode.Embeddings:
-		return urlPrefix + "/v1/embeddings", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    urlPrefix + "/v1/embeddings",
+		}, nil
 	default:
 		if isAIGateWay {
-			return fmt.Sprintf("%s/%s", urlPrefix, meta.ActualModel), nil
+			return adaptor.RequestURL{
+				Method: http.MethodPost,
+				URL:    fmt.Sprintf("%s/%s", urlPrefix, meta.ActualModel),
+			}, nil
 		}
-		return fmt.Sprintf("%s/run/%s", urlPrefix, meta.ActualModel), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    fmt.Sprintf("%s/run/%s", urlPrefix, meta.ActualModel),
+		}, nil
 	}
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 31 - 15
core/relay/adaptor/cohere/adaptor.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -19,45 +20,57 @@ type Adaptor struct{}
 
 const baseURL = "https://api.cohere.ai"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
-	return meta.Channel.BaseURL + "/v1/chat", nil
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    meta.Channel.BaseURL + "/v1/chat",
+	}, nil
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 	return nil
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 	requestBody := ConvertRequest(request)
 	if requestBody == nil {
-		return nil, errors.New("request body is nil")
+		return adaptor.ConvertResult{}, errors.New("request body is nil")
 	}
 	data, err := sonic.Marshal(requestBody)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -66,9 +79,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.Rerank:
 		usage, err = openai.RerankHandler(meta, c, resp)
@@ -82,6 +96,8 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 11 - 10
core/relay/adaptor/cohere/main.go

@@ -3,6 +3,7 @@ package cohere
 import (
 	"bufio"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -113,7 +114,7 @@ func StreamResponse2OpenAI(
 		ID:      "chatcmpl-" + cohereResponse.GenerationID,
 		Model:   meta.OriginModel,
 		Created: time.Now().Unix(),
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{&choice},
 	}
 	if response != nil {
@@ -139,7 +140,7 @@ func Response2OpenAI(meta *meta.Meta, cohereResponse *Response) *relaymodel.Text
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: []*relaymodel.TextResponseChoice{&choice},
 		Usage: relaymodel.Usage{
@@ -155,9 +156,9 @@ func StreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -199,9 +200,9 @@ func StreamHandler(
 	return usage.ToModelUsage(), nil
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -209,14 +210,14 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	var cohereResponse Response
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&cohereResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	if cohereResponse.ResponseID == "" {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			cohereResponse.Message,
 			resp.StatusCode,
 			resp.StatusCode,
@@ -225,14 +226,14 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	fullTextResponse := Response2OpenAI(meta, &cohereResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

+ 33 - 16
core/relay/adaptor/coze/adaptor.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/bytedance/sonic"
@@ -19,15 +20,23 @@ type Adaptor struct{}
 
 const baseURL = "https://api.coze.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
-	return meta.Channel.BaseURL + "/open_api/v2/chat", nil
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    meta.Channel.BaseURL + "/open_api/v2/chat",
+	}, nil
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	token, _, err := getTokenAndUserID(meta.Channel.Key)
 	if err != nil {
 		return err
@@ -38,18 +47,19 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	if meta.Mode != mode.ChatCompletions {
-		return nil, errors.New("coze only support chat completions")
+		return adaptor.ConvertResult{}, errors.New("coze only support chat completions")
 	}
 	request, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	_, userID, err := getTokenAndUserID(meta.Channel.Key)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.User = userID
 	request.Model = meta.ActualModel
@@ -71,17 +81,20 @@ func (a *Adaptor) ConvertRequest(
 	}
 	data, err := sonic.Marshal(cozeRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -90,9 +103,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	if utils.IsStreamResponse(resp) {
 		usage, err = StreamHandler(meta, c, resp)
 	} else {
@@ -101,6 +115,9 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		KeyHelp: "token|user_id",
+		Models:  ModelList,
+	}
 }

+ 0 - 4
core/relay/adaptor/coze/key.go

@@ -17,10 +17,6 @@ func (a *Adaptor) ValidateKey(key string) error {
 	return nil
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "token|user_id"
-}
-
 func getTokenAndUserID(key string) (string, string, error) {
 	split := strings.Split(key, "|")
 	if len(split) != 2 {

+ 16 - 13
core/relay/adaptor/coze/main.go

@@ -3,6 +3,7 @@ package coze
 import (
 	"bufio"
 	"net/http"
+	"strconv"
 	"strings"
 	"time"
 
@@ -59,7 +60,7 @@ func StreamResponse2OpenAI(
 		ID:      cozeResponse.ConversationID,
 		Model:   meta.OriginModel,
 		Created: time.Now().Unix(),
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{&choice},
 	}
 	return &openaiResponse
@@ -85,7 +86,7 @@ func Response2OpenAI(meta *meta.Meta, cozeResponse *Response) *relaymodel.TextRe
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: []*relaymodel.TextResponseChoice{&choice},
 	}
@@ -96,9 +97,9 @@ func StreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -151,14 +152,16 @@ func StreamHandler(
 
 	render.Done(c)
 
-	return openai.ResponseText2Usage(responseText.String(), meta.ActualModel, int64(meta.RequestUsage.InputTokens)).
-			ToModelUsage(),
-		nil
+	return openai.ResponseText2Usage(
+		responseText.String(),
+		meta.ActualModel,
+		int64(meta.RequestUsage.InputTokens),
+	).ToModelUsage(), nil
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -168,14 +171,14 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	var cozeResponse Response
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&cozeResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	if cozeResponse.Code != 0 {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			cozeResponse.Msg,
 			cozeResponse.Code,
 			resp.StatusCode,
@@ -184,14 +187,14 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	fullTextResponse := Response2OpenAI(meta, &cozeResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, err = c.Writer.Write(jsonResponse)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 5 - 4
core/relay/adaptor/deepseek/adaptor.go

@@ -1,7 +1,6 @@
 package deepseek
 
 import (
-	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
@@ -14,10 +13,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.deepseek.com/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 24 - 11
core/relay/adaptor/doc2x/adaptor.go

@@ -19,33 +19,38 @@ type Adaptor struct{}
 
 const baseURL = "https://v2.doc2x.noedgeai.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	switch meta.Mode {
 	case mode.ParsePdf:
-		return meta.Channel.BaseURL + "/api/v2/parse/pdf", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    meta.Channel.BaseURL + "/api/v2/parse/pdf",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.ParsePdf:
 		return ConvertParsePdfRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -54,14 +59,15 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	switch meta.Mode {
 	case mode.ParsePdf:
 		return HandleParsePdfResponse(meta, c, resp)
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -69,11 +75,18 @@ func (a *Adaptor) DoResponse(
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 	return nil
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 15 - 14
core/relay/adaptor/doc2x/pdf.go

@@ -26,24 +26,25 @@ import (
 func ConvertParsePdfRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	err := req.ParseMultipartForm(1024 * 1024 * 4)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	file, _, err := req.FormFile("file")
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	responseFormat := req.FormValue("response_format")
 	meta.Set("response_format", responseFormat)
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   file,
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type": {"multipart/form-data"},
+		},
+		Body: file,
 	}, nil
 }
 
@@ -61,11 +62,11 @@ func HandleParsePdfResponse(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	var response ParsePdfResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&response)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"decode response failed: "+err.Error(),
 			"decode_response_failed",
 			http.StatusBadRequest,
@@ -73,7 +74,7 @@ func HandleParsePdfResponse(
 	}
 
 	if response.Code != "success" {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			"parse pdf failed: "+response.Msg,
 			"parse_pdf_failed",
 			http.StatusBadRequest,
@@ -83,7 +84,7 @@ func HandleParsePdfResponse(
 	for {
 		status, err := GetStatus(context.Background(), meta, response.Data.UID)
 		if err != nil {
-			return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+			return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 				"get status failed: "+err.Error(),
 				"get_status_failed",
 				http.StatusInternalServerError,
@@ -96,7 +97,7 @@ func HandleParsePdfResponse(
 		case StatusResponseDataStatusProcessing:
 			time.Sleep(1 * time.Second)
 		case StatusResponseDataStatusFailed:
-			return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+			return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 				"parse pdf failed: "+status.Detail,
 				"parse_pdf_failed",
 				http.StatusBadRequest,
@@ -352,7 +353,7 @@ func handleParsePdfResponse(
 	meta *meta.Meta,
 	c *gin.Context,
 	response *StatusResponseDataResult,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	mds := make([]string, 0, len(response.Pages))
 	totalLength := 0
 	for _, page := range response.Pages {
@@ -383,7 +384,7 @@ func handleParsePdfResponse(
 		})
 	}
 
-	return &model.Usage{
+	return model.Usage{
 		InputTokens: model.ZeroNullInt64(pages),
 		TotalTokens: model.ZeroNullInt64(pages),
 	}, nil

+ 0 - 12
core/relay/adaptor/doubao/fetures.go

@@ -1,12 +0,0 @@
-package doubao
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"Bot support",
-		"Network search metering support",
-	}
-}

+ 41 - 23
core/relay/adaptor/doubao/main.go

@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 
 	"github.com/bytedance/sonic"
@@ -19,18 +20,27 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func GetRequestURL(meta *meta.Meta) (string, error) {
+func GetRequestURL(meta *meta.Meta) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 	switch meta.Mode {
 	case mode.ChatCompletions:
 		if strings.HasPrefix(meta.ActualModel, "bot-") {
-			return u + "/api/v3/bots/chat/completions", nil
+			return adaptor.RequestURL{
+				Method: http.MethodPost,
+				URL:    u + "/api/v3/bots/chat/completions",
+			}, nil
 		}
-		return u + "/api/v3/chat/completions", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/v3/chat/completions",
+		}, nil
 	case mode.Embeddings:
-		return u + "/api/v3/embeddings", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/v3/embeddings",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported relay mode %d for doubao", meta.Mode)
 	}
 }
 
@@ -40,25 +50,32 @@ type Adaptor struct {
 
 const baseURL = "https://ark.cn-beijing.volces.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Bot support",
+			"Network search metering support",
+		},
+		Models: ModelList,
+	}
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	return GetRequestURL(meta)
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
-	result, err := a.Adaptor.ConvertRequest(meta, req)
+) (adaptor.ConvertResult, error) {
+	result, err := a.Adaptor.ConvertRequest(meta, store, req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	if meta.Mode != mode.ChatCompletions || meta.OriginModel != "deepseek-reasoner" {
 		return result, nil
@@ -67,11 +84,11 @@ func (a *Adaptor) ConvertRequest(
 	m := make(map[string]any)
 	err = sonic.ConfigDefault.NewDecoder(result.Body).Decode(&m)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	messages, _ := m["messages"].([]any)
 	if len(messages) == 0 {
-		return nil, errors.New("messages is empty")
+		return adaptor.ConvertResult{}, errors.New("messages is empty")
 	}
 	sysMessage := relaymodel.Message{
 		Role:    "system",
@@ -81,12 +98,14 @@ func (a *Adaptor) ConvertRequest(
 	m["messages"] = messages
 	newBody, err := sonic.Marshal(m)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	return &adaptor.ConvertRequestResult{
-		Method: result.Method,
-		Header: result.Header,
+	header := result.Header
+	header.Set("Content-Length", strconv.Itoa(len(newBody)))
+
+	return adaptor.ConvertResult{
+		Header: header,
 		Body:   bytes.NewReader(newBody),
 	}, nil
 }
@@ -138,9 +157,10 @@ func handlerPreHandler(meta *meta.Meta, node *ast.Node, websearchCount *int64) e
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.ChatCompletions:
 		websearchCount := int64(0)
@@ -149,11 +169,9 @@ func (a *Adaptor) DoResponse(
 		} else {
 			usage, err = openai.Handler(meta, c, resp, newHandlerPreHandler(&websearchCount))
 		}
-		if usage != nil {
-			usage.WebSearchCount += model.ZeroNullInt64(websearchCount)
-		}
+		usage.WebSearchCount += model.ZeroNullInt64(websearchCount)
 	default:
-		return openai.DoResponse(meta, c, resp)
+		return openai.DoResponse(meta, store, c, resp)
 	}
 	return usage, err
 }

+ 0 - 12
core/relay/adaptor/doubaoaudio/fetures.go

@@ -1,12 +0,0 @@
-package doubaoaudio
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"https://www.volcengine.com/docs/6561/1257543",
-		"TTS support",
-	}
-}

+ 0 - 4
core/relay/adaptor/doubaoaudio/key.go

@@ -14,10 +14,6 @@ func (a *Adaptor) ValidateKey(key string) error {
 	return err
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "app_id|app_token"
-}
-
 // key格式: app_id|app_token
 func getAppIDAndToken(key string) (string, string, error) {
 	parts := strings.Split(key, "|")

+ 30 - 12
core/relay/adaptor/doubaoaudio/main.go

@@ -12,13 +12,16 @@ import (
 	relaymodel "github.com/labring/aiproxy/core/relay/model"
 )
 
-func GetRequestURL(meta *meta.Meta) (string, error) {
+func GetRequestURL(meta *meta.Meta) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 	switch meta.Mode {
 	case mode.AudioSpeech:
-		return u + "/api/v1/tts/ws_binary", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/v1/tts/ws_binary",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
@@ -26,31 +29,44 @@ type Adaptor struct{}
 
 const baseURL = "https://openspeech.bytedance.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"https://www.volcengine.com/docs/6561/1257543",
+			"TTS support",
+		},
+		KeyHelp: "app_id|app_token",
+		Models:  ModelList,
+	}
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	return GetRequestURL(meta)
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.AudioSpeech:
 		return ConvertTTSRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	switch meta.Mode {
 	case mode.AudioSpeech:
 		_, token, err := getAppIDAndToken(meta.Channel.Key)
@@ -66,6 +82,7 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 
 func (a *Adaptor) DoRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -79,14 +96,15 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	switch meta.Mode {
 	case mode.AudioSpeech:
 		return TTSDoResponse(meta, c, resp)
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			nil,
 			http.StatusBadRequest,

+ 10 - 14
core/relay/adaptor/doubaoaudio/tts.go

@@ -65,20 +65,20 @@ type RequestConfig struct {
 var defaultHeader = []byte{0x11, 0x10, 0x11, 0x00}
 
 //nolint:gosec
-func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalTTSRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	reqMap, err := utils.UnmarshalMap(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	appID, token, err := getAppIDAndToken(meta.Channel.Key)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	cluster := "volcano_tts"
@@ -129,27 +129,23 @@ func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequ
 
 	data, err := sonic.Marshal(doubaoRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	compressedData, err := gzipCompress(data)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	payloadArr := make([]byte, 4)
 	binary.BigEndian.PutUint32(payloadArr, uint32(len(compressedData)))
 	clientRequest := make([]byte, len(defaultHeader))
 	copy(clientRequest, defaultHeader)
-	//nolint:makezero
 	clientRequest = append(clientRequest, payloadArr...)
-	//nolint:makezero
 	clientRequest = append(clientRequest, compressedData...)
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(clientRequest),
+	return adaptor.ConvertResult{
+		Body: bytes.NewReader(clientRequest),
 	}, nil
 }
 
@@ -184,7 +180,7 @@ func TTSDoResponse(
 	meta *meta.Meta,
 	c *gin.Context,
 	_ *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	log := middleware.GetLogger(c)
 
 	conn, ok := meta.MustGet("ws_conn").(*websocket.Conn)
@@ -193,7 +189,7 @@ func TTSDoResponse(
 	}
 	defer conn.Close()
 
-	usage := &model.Usage{
+	usage := model.Usage{
 		InputTokens: meta.RequestUsage.InputTokens,
 		TotalTokens: meta.RequestUsage.InputTokens,
 	}

+ 29 - 11
core/relay/adaptor/gemini/adaptor.go

@@ -17,13 +17,13 @@ type Adaptor struct{}
 
 const baseURL = "https://generativelanguage.googleapis.com"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
 var v1ModelMap = map[string]struct{}{}
 
-func getRequestURL(meta *meta.Meta, action string) string {
+func getRequestURL(meta *meta.Meta, action string) adaptor.RequestURL {
 	u := meta.Channel.BaseURL
 	if u == "" {
 		u = baseURL
@@ -32,10 +32,13 @@ func getRequestURL(meta *meta.Meta, action string) string {
 	if _, ok := v1ModelMap[meta.ActualModel]; ok {
 		version = "v1"
 	}
-	return fmt.Sprintf("%s/%s/models/%s:%s", u, version, meta.ActualModel, action)
+	return adaptor.RequestURL{
+		Method: http.MethodPost,
+		URL:    fmt.Sprintf("%s/%s/models/%s:%s", u, version, meta.ActualModel, action),
+	}
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	var action string
 	switch meta.Mode {
 	case mode.Embeddings:
@@ -50,27 +53,34 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 	return getRequestURL(meta, action), nil
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("X-Goog-Api-Key", meta.Channel.Key)
 	return nil
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.Embeddings:
 		return ConvertEmbeddingRequest(meta, req)
 	case mode.ChatCompletions:
 		return ConvertRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -79,9 +89,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.Embeddings:
 		usage, err = EmbeddingHandler(meta, c, resp)
@@ -92,7 +103,7 @@ func (a *Adaptor) DoResponse(
 			usage, err = Handler(meta, c, resp)
 		}
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -101,6 +112,13 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"https://ai.google.dev",
+			"Chat、Embeddings、Image generation Support",
+		},
+		Models: ModelList,
+		Config: ConfigTemplates,
+	}
 }

+ 0 - 2
core/relay/adaptor/gemini/config.go

@@ -6,8 +6,6 @@ import (
 	"github.com/labring/aiproxy/core/relay/adaptor"
 )
 
-var _ adaptor.Config = (*Adaptor)(nil)
-
 type Config struct {
 	Safety string `json:"safety"`
 }

+ 16 - 19
core/relay/adaptor/gemini/embeddings.go

@@ -3,6 +3,7 @@ package gemini
 import (
 	"bytes"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
@@ -17,10 +18,10 @@ import (
 func ConvertEmbeddingRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 
@@ -45,12 +46,14 @@ func ConvertEmbeddingRequest(
 		Requests: requests,
 	})
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
@@ -58,9 +61,9 @@ func EmbeddingHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -68,30 +71,24 @@ func EmbeddingHandler(
 	var geminiEmbeddingResponse EmbeddingResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&geminiEmbeddingResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
-	if geminiEmbeddingResponse.Error != nil {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
-			geminiEmbeddingResponse.Error.Message,
-			geminiEmbeddingResponse.Error.Code,
-			resp.StatusCode,
-		)
-	}
+
 	fullTextResponse := embeddingResponse2OpenAI(meta, &geminiEmbeddingResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

+ 0 - 12
core/relay/adaptor/gemini/fetures.go

@@ -1,12 +0,0 @@
-package gemini
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"https://ai.google.dev",
-		"Chat、Embeddings、Image generation Support",
-	}
-}

+ 24 - 21
core/relay/adaptor/gemini/main.go

@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -294,16 +295,16 @@ func processImageTasks(ctx context.Context, imageTasks []*Part) error {
 }
 
 // Setting safety to the lowest possible values since Gemini is already powerless enough
-func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	adaptorConfig := Config{}
 	err := meta.ChannelConfig.SpecConfig(&adaptorConfig)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	textRequest, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	textRequest.Model = meta.ActualModel
@@ -314,13 +315,13 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 	// Process image tasks concurrently
 	if len(imageTasks) > 0 {
 		if err := processImageTasks(req.Context(), imageTasks); err != nil {
-			return nil, err
+			return adaptor.ConvertResult{}, err
 		}
 	}
 
 	config, err := buildGenerationConfig(meta, req, textRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	// Build actual request
@@ -335,13 +336,15 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 
 	data, err := sonic.Marshal(geminiRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
@@ -444,7 +447,7 @@ func responseChat2OpenAI(meta *meta.Meta, response *ChatResponse) *relaymodel.Te
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: make([]*relaymodel.TextResponseChoice, 0, len(response.Candidates)),
 	}
@@ -527,7 +530,7 @@ func streamResponseChat2OpenAI(
 		ID:      openai.ChatCompletionID(),
 		Created: time.Now().Unix(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Choices: make(
 			[]*relaymodel.ChatCompletionsStreamResponseChoice,
 			0,
@@ -635,13 +638,13 @@ func StreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
-	defer resp.Body.Close()
-
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
+	defer resp.Body.Close()
+
 	log := middleware.GetLogger(c)
 
 	responseText := strings.Builder{}
@@ -693,9 +696,9 @@ func StreamHandler(
 	return usage.ToModelUsage(), nil
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -703,7 +706,7 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	var geminiResponse ChatResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&geminiResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
@@ -712,14 +715,14 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	fullTextResponse := responseChat2OpenAI(meta, &geminiResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

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

@@ -31,7 +31,6 @@ type EmbeddingData struct {
 }
 
 type EmbeddingResponse struct {
-	Error      *Error          `json:"error,omitempty"`
 	Embeddings []EmbeddingData `json:"embeddings"`
 }
 

+ 10 - 4
core/relay/adaptor/geminiopenai/adaptor.go

@@ -1,7 +1,7 @@
 package geminiopenai
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/gemini"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
@@ -12,10 +12,16 @@ type Adaptor struct {
 
 const baseURL = "https://generativelanguage.googleapis.com/v1beta/openai"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return gemini.ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"https://ai.google.dev/gemini-api/docs/openai",
+			"OpenAI compatibility",
+		},
+		Models: gemini.ModelList,
+	}
 }

+ 0 - 12
core/relay/adaptor/geminiopenai/fetures.go

@@ -1,12 +0,0 @@
-package geminiopenai
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"https://ai.google.dev/gemini-api/docs/openai",
-		"OpenAI compatibility",
-	}
-}

+ 6 - 4
core/relay/adaptor/groq/adaptor.go

@@ -1,7 +1,7 @@
 package groq
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.groq.com/openai/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 45 - 37
core/relay/adaptor/interface.go

@@ -3,39 +3,74 @@ package adaptor
 import (
 	"encoding/json"
 	"errors"
-	"fmt"
 	"io"
 	"net/http"
+	"time"
 
-	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
 	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/core/relay/meta"
 )
 
+type StoreCache struct {
+	ID        string
+	GroupID   string
+	TokenID   int
+	ChannelID int
+	Model     string
+	ExpiresAt time.Time
+}
+
+type Store interface {
+	GetStore(id string) (StoreCache, error)
+	SaveStore(store StoreCache) error
+}
+
+type Metadata struct {
+	Config   ConfigTemplates
+	KeyHelp  string
+	Features []string
+	Models   []model.ModelConfig
+}
+
+type RequestURL struct {
+	Method string
+	URL    string
+}
+
 type GetRequestURL interface {
-	GetRequestURL(meta *meta.Meta) (string, error)
+	GetRequestURL(meta *meta.Meta, store Store) (RequestURL, error)
 }
 
 type SetupRequestHeader interface {
-	SetupRequestHeader(meta *meta.Meta, c *gin.Context, req *http.Request) error
+	SetupRequestHeader(meta *meta.Meta, store Store, c *gin.Context, req *http.Request) error
 }
 
 type ConvertRequest interface {
-	ConvertRequest(meta *meta.Meta, req *http.Request) (*ConvertRequestResult, error)
+	ConvertRequest(meta *meta.Meta, store Store, req *http.Request) (ConvertResult, error)
 }
 
 type DoRequest interface {
-	DoRequest(meta *meta.Meta, c *gin.Context, req *http.Request) (*http.Response, error)
+	DoRequest(
+		meta *meta.Meta,
+		store Store,
+		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)
+	DoResponse(
+		meta *meta.Meta,
+		store Store,
+		c *gin.Context,
+		resp *http.Response,
+	) (model.Usage, Error)
 }
 
 type Adaptor interface {
-	GetBaseURL() string
-	GetModelList() []model.ModelConfig
+	Metadata() Metadata
+	DefaultBaseURL() string
 	GetRequestURL
 	SetupRequestHeader
 	ConvertRequest
@@ -43,8 +78,7 @@ type Adaptor interface {
 	DoResponse
 }
 
-type ConvertRequestResult struct {
-	Method string
+type ConvertResult struct {
 	Header http.Header
 	Body   io.Reader
 }
@@ -55,23 +89,6 @@ type Error interface {
 	StatusCode() int
 }
 
-type BasicError[T any] struct {
-	error      T
-	statusCode int
-}
-
-func (e BasicError[T]) MarshalJSON() ([]byte, error) {
-	return sonic.Marshal(e.error)
-}
-
-func (e BasicError[T]) StatusCode() int {
-	return e.statusCode
-}
-
-func (e BasicError[T]) Error() string {
-	return fmt.Sprintf("status code: %d, error: %v", e.statusCode, e.error)
-}
-
 var ErrGetBalanceNotImplemented = errors.New("get balance not implemented")
 
 type Balancer interface {
@@ -80,11 +97,6 @@ type Balancer interface {
 
 type KeyValidator interface {
 	ValidateKey(key string) error
-	KeyHelp() string
-}
-
-type Features interface {
-	Features() []string
 }
 
 type ConfigType string
@@ -106,7 +118,3 @@ type ConfigTemplate struct {
 }
 
 type ConfigTemplates = map[string]ConfigTemplate
-
-type Config interface {
-	ConfigTemplates() ConfigTemplates
-}

+ 15 - 7
core/relay/adaptor/jina/adaptor.go

@@ -17,35 +17,43 @@ type Adaptor struct {
 
 const baseURL = "https://api.jina.ai/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.Embeddings:
 		return ConvertEmbeddingsRequest(meta, req)
 	default:
-		return a.Adaptor.ConvertRequest(meta, req)
+		return a.Adaptor.ConvertRequest(meta, store, req)
 	}
 }
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.Rerank:
 		return RerankHandler(meta, c, resp)
 	default:
-		return a.Adaptor.DoResponse(meta, c, resp)
+		return a.Adaptor.DoResponse(meta, store, c, resp)
 	}
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"https://jina.ai",
+			"Embeddings、Rerank Support",
+		},
+		Models: ModelList,
+	}
 }

+ 7 - 30
core/relay/adaptor/jina/embeddings.go

@@ -1,43 +1,20 @@
 package jina
 
 import (
-	"bytes"
 	"net/http"
 
-	"github.com/bytedance/sonic"
-	"github.com/labring/aiproxy/core/common"
+	"github.com/bytedance/sonic/ast"
 	"github.com/labring/aiproxy/core/relay/adaptor"
+	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 	"github.com/labring/aiproxy/core/relay/meta"
 )
 
-//
-//nolint:gocritic
 func ConvertEmbeddingsRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
-	reqMap := make(map[string]any)
-	err := common.UnmarshalBodyReusable(req, &reqMap)
-	if err != nil {
-		return nil, err
-	}
-
-	reqMap["model"] = meta.ActualModel
-
-	switch v := reqMap["input"].(type) {
-	case string:
-		reqMap["input"] = []string{v}
-	}
-
-	delete(reqMap, "encoding_format")
-
-	jsonData, err := sonic.Marshal(reqMap)
-	if err != nil {
-		return nil, err
-	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(jsonData),
-	}, nil
+) (adaptor.ConvertResult, error) {
+	return openai.ConvertEmbeddingsRequest(meta, req, func(node *ast.Node) error {
+		_, err := node.Unset("encoding_format")
+		return err
+	}, true)
 }

+ 0 - 12
core/relay/adaptor/jina/fetures.go

@@ -1,12 +0,0 @@
-package jina
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"https://jina.ai",
-		"Embeddings、Rerank Support",
-	}
-}

+ 13 - 11
core/relay/adaptor/jina/rerank.go

@@ -3,6 +3,7 @@ package jina
 import (
 	"io"
 	"net/http"
+	"strconv"
 
 	"github.com/bytedance/sonic"
 	"github.com/bytedance/sonic/ast"
@@ -18,9 +19,9 @@ func RerankHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHanlder(resp)
+		return model.Usage{}, ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -29,7 +30,7 @@ func RerankHandler(
 
 	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"read_response_body_failed",
 			http.StatusInternalServerError,
@@ -37,7 +38,7 @@ func RerankHandler(
 	}
 	node, err := sonic.Get(responseBody)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
@@ -47,7 +48,7 @@ func RerankHandler(
 	usageNode := node.Get("usage")
 	usageStr, err := usageNode.Raw()
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_usage_failed",
 			http.StatusInternalServerError,
@@ -55,7 +56,7 @@ func RerankHandler(
 	}
 	err = sonic.UnmarshalString(usageStr, &usage)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_usage_failed",
 			http.StatusInternalServerError,
@@ -72,7 +73,7 @@ func RerankHandler(
 		"tokens": modelUsage,
 	})
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return modelUsage, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_usage_failed",
 			http.StatusInternalServerError,
@@ -80,7 +81,7 @@ func RerankHandler(
 	}
 	_, err = node.Unset("usage")
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return modelUsage, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_usage_failed",
 			http.StatusInternalServerError,
@@ -88,21 +89,22 @@ func RerankHandler(
 	}
 	_, err = node.Set("model", ast.NewString(meta.OriginModel))
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return modelUsage, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_usage_failed",
 			http.StatusInternalServerError,
 		)
 	}
-	c.Writer.WriteHeader(resp.StatusCode)
 	respData, err := node.MarshalJSON()
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return modelUsage, relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_failed",
 			http.StatusInternalServerError,
 		)
 	}
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(respData)))
 	_, err = c.Writer.Write(respData)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 7 - 5
core/relay/adaptor/lingyiwanwu/adaptor.go

@@ -12,14 +12,16 @@ type Adaptor struct {
 
 const baseURL = "https://api.lingyiwanwu.com/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
-}
-
 func (a *Adaptor) GetBalance(_ *model.Channel) (float64, error) {
 	return 0, adaptor.ErrGetBalanceNotImplemented
 }
+
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
+}

+ 37 - 15
core/relay/adaptor/minimax/adaptor.go

@@ -18,15 +18,26 @@ type Adaptor struct {
 
 const baseURL = "https://api.minimax.chat/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Chat、Embeddings、TTS(need group id) Support",
+		},
+		KeyHelp: "api_key|group_id",
+		Models:  ModelList,
+	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	apiKey, _, err := GetAPIKeyAndGroupID(meta.Channel.Key)
 	if err != nil {
 		return err
@@ -35,47 +46,58 @@ func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.
 	return nil
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, store adaptor.Store) (adaptor.RequestURL, error) {
 	_, groupID, err := GetAPIKeyAndGroupID(meta.Channel.Key)
 	if err != nil {
-		return "", err
+		return adaptor.RequestURL{}, err
 	}
 	switch meta.Mode {
 	case mode.ChatCompletions:
-		return meta.Channel.BaseURL + "/text/chatcompletion_v2", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    meta.Channel.BaseURL + "/text/chatcompletion_v2",
+		}, nil
 	case mode.Embeddings:
-		return fmt.Sprintf("%s/embeddings?GroupId=%s", meta.Channel.BaseURL, groupID), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    fmt.Sprintf("%s/embeddings?GroupId=%s", meta.Channel.BaseURL, groupID),
+		}, nil
 	case mode.AudioSpeech:
-		return fmt.Sprintf("%s/t2a_v2?GroupId=%s", meta.Channel.BaseURL, groupID), nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    fmt.Sprintf("%s/t2a_v2?GroupId=%s", meta.Channel.BaseURL, groupID),
+		}, nil
 	default:
-		return a.Adaptor.GetRequestURL(meta)
+		return a.Adaptor.GetRequestURL(meta, store)
 	}
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	switch meta.Mode {
 	case mode.ChatCompletions:
-		return openai.ConvertTextRequest(meta, req, true)
+		return openai.ConvertChatCompletionsRequest(meta, req, nil, true)
 	case mode.AudioSpeech:
 		return ConvertTTSRequest(meta, req)
 	default:
-		return a.Adaptor.ConvertRequest(meta, req)
+		return a.Adaptor.ConvertRequest(meta, store, req)
 	}
 }
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.AudioSpeech:
 		return TTSHandler(meta, c, resp)
 	default:
-		return a.Adaptor.DoResponse(meta, c, resp)
+		return a.Adaptor.DoResponse(meta, store, c, resp)
 	}
 }
 

+ 0 - 11
core/relay/adaptor/minimax/fetures.go

@@ -1,11 +0,0 @@
-package minimax
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"Chat、Embeddings、TTS(need group id) Support",
-	}
-}

+ 0 - 4
core/relay/adaptor/minimax/key.go

@@ -17,10 +17,6 @@ func (a *Adaptor) ValidateKey(key string) error {
 	return nil
 }
 
-func (a *Adaptor) KeyHelp() string {
-	return "api_key|group_id"
-}
-
 func GetAPIKeyAndGroupID(key string) (string, string, error) {
 	keys := strings.Split(key, "|")
 	if len(keys) != 2 {

+ 42 - 24
core/relay/adaptor/minimax/tts.go

@@ -20,10 +20,10 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	reqMap, err := utils.UnmarshalMap(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	reqMap["model"] = meta.ActualModel
@@ -80,13 +80,15 @@ func ConvertTTSRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequ
 
 	body, err := sonic.Marshal(reqMap)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(body),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(body))},
+		},
+		Body: bytes.NewReader(body),
 	}, nil
 }
 
@@ -115,9 +117,9 @@ func TTSHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, openai.ErrorHanlder(resp)
+		return model.Usage{}, openai.ErrorHanlder(resp)
 	}
 
 	if !strings.Contains(resp.Header.Get("Content-Type"), "application/json") &&
@@ -131,49 +133,65 @@ func TTSHandler(
 
 	body, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(err, "TTS_ERROR", http.StatusInternalServerError)
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
+			err,
+			"TTS_ERROR",
+			http.StatusInternalServerError,
+		)
 	}
 
 	var result TTSResponse
 	if err := sonic.Unmarshal(body, &result); err != nil {
-		return nil, relaymodel.WrapperOpenAIError(err, "TTS_ERROR", http.StatusInternalServerError)
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
+			err,
+			"TTS_ERROR",
+			http.StatusInternalServerError,
+		)
 	}
 	if result.BaseResp != nil && result.BaseResp.StatusCode != 0 {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			result.BaseResp.StatusMsg,
 			"TTS_ERROR_"+strconv.Itoa(result.BaseResp.StatusCode),
 			http.StatusInternalServerError,
 		)
 	}
 
+	usageCharacters := meta.RequestUsage.InputTokens
+	if result.ExtraInfo.UsageCharacters > 0 {
+		usageCharacters = model.ZeroNullInt64(result.ExtraInfo.UsageCharacters)
+	}
+
+	usage := model.Usage{
+		InputTokens: usageCharacters,
+		TotalTokens: usageCharacters,
+	}
+
 	resp.Header.Set("Content-Type", "audio/"+result.ExtraInfo.AudioFormat)
 
 	audioBytes, err := hex.DecodeString(result.Data.Audio)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(err, "TTS_ERROR", http.StatusInternalServerError)
+		return usage, relaymodel.WrapperOpenAIError(
+			err,
+			"TTS_ERROR",
+			http.StatusInternalServerError,
+		)
 	}
 
+	c.Writer.Header().Set("Content-Type", "audio/"+result.ExtraInfo.AudioFormat)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(audioBytes)))
 	_, err = c.Writer.Write(audioBytes)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)
 	}
 
-	usageCharacters := meta.RequestUsage.InputTokens
-	if result.ExtraInfo.UsageCharacters > 0 {
-		usageCharacters = model.ZeroNullInt64(result.ExtraInfo.UsageCharacters)
-	}
-
-	return &model.Usage{
-		InputTokens: usageCharacters,
-		TotalTokens: usageCharacters,
-	}, nil
+	return usage, nil
 }
 
 func ttsStreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	defer resp.Body.Close()
 
 	resp.Header.Set("Content-Type", "application/octet-stream")
@@ -218,7 +236,7 @@ func ttsStreamHandler(
 		}
 	}
 
-	return &model.Usage{
+	return model.Usage{
 		InputTokens: usageCharacters,
 		TotalTokens: usageCharacters,
 	}, nil

+ 6 - 4
core/relay/adaptor/mistral/adaptor.go

@@ -1,7 +1,7 @@
 package mistral
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.mistral.ai/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 6 - 4
core/relay/adaptor/moonshot/adaptor.go

@@ -1,7 +1,7 @@
 package moonshot
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.moonshot.cn/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 6 - 4
core/relay/adaptor/novita/adaptor.go

@@ -1,7 +1,7 @@
 package novita
 
 import (
-	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/adaptor/openai"
 )
 
@@ -11,10 +11,12 @@ type Adaptor struct {
 
 const baseURL = "https://api.novita.ai/v3/openai"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Models: ModelList,
+	}
 }

+ 36 - 14
core/relay/adaptor/ollama/adaptor.go

@@ -18,36 +18,51 @@ type Adaptor struct{}
 
 const baseURL = "http://localhost:11434"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	// https://github.com/ollama/ollama/blob/main/docs/api.md
 	u := meta.Channel.BaseURL
 	switch meta.Mode {
 	case mode.Embeddings:
-		return u + "/api/embed", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/embed",
+		}, nil
 	case mode.ChatCompletions:
-		return u + "/api/chat", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/chat",
+		}, nil
 	case mode.Completions:
-		return u + "/api/generate", nil
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/api/generate",
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 	return nil
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	request *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	if request == nil {
-		return nil, errors.New("request is nil")
+		return adaptor.ConvertResult{}, errors.New("request is nil")
 	}
 	switch meta.Mode {
 	case mode.Embeddings:
@@ -55,12 +70,13 @@ func (a *Adaptor) ConvertRequest(
 	case mode.ChatCompletions, mode.Completions:
 		return ConvertRequest(meta, request)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -69,9 +85,10 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	_ adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.Embeddings:
 		usage, err = EmbeddingHandler(meta, c, resp)
@@ -82,7 +99,7 @@ func (a *Adaptor) DoResponse(
 			usage, err = Handler(meta, c, resp)
 		}
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -91,6 +108,11 @@ func (a *Adaptor) DoResponse(
 	return
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"Chat、Embeddings Support",
+		},
+		Models: ModelList,
+	}
 }

+ 0 - 11
core/relay/adaptor/ollama/fetures.go

@@ -1,11 +0,0 @@
-package ollama
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"Chat、Embeddings Support",
-	}
-}

+ 40 - 38
core/relay/adaptor/ollama/main.go

@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"net/http"
+	"strconv"
 	"time"
 
 	"github.com/bytedance/sonic"
@@ -20,11 +21,11 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertRequest(meta *meta.Meta, req *http.Request) (adaptor.ConvertResult, error) {
 	var request relaymodel.GeneralOpenAIRequest
 	err := common.UnmarshalBodyReusable(req, &request)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
 	ollamaRequest := ChatRequest{
@@ -63,7 +64,7 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 			case relaymodel.ContentTypeImageURL:
 				_, data, err := image.GetImageFromURL(req.Context(), part.ImageURL.URL)
 				if err != nil {
-					return nil, err
+					return adaptor.ConvertResult{}, err
 				}
 				imageUrls = append(imageUrls, data)
 			}
@@ -105,13 +106,15 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 
 	data, err := sonic.Marshal(ollamaRequest)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
@@ -154,7 +157,7 @@ func response2OpenAI(meta *meta.Meta, response *ChatResponse) *relaymodel.TextRe
 	fullTextResponse := relaymodel.TextResponse{
 		ID:      openai.ChatCompletionID(),
 		Model:   meta.OriginModel,
-		Object:  relaymodel.ChatCompletion,
+		Object:  relaymodel.ChatCompletionObject,
 		Created: time.Now().Unix(),
 		Choices: []*relaymodel.TextResponseChoice{&choice},
 		Usage: relaymodel.Usage{
@@ -185,7 +188,7 @@ func streamResponse2OpenAI(
 	}
 	response := relaymodel.ChatCompletionsStreamResponse{
 		ID:      openai.ChatCompletionID(),
-		Object:  relaymodel.ChatCompletionChunk,
+		Object:  relaymodel.ChatCompletionChunkObject,
 		Created: time.Now().Unix(),
 		Model:   meta.OriginModel,
 		Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{&choice},
@@ -206,9 +209,9 @@ func StreamHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHandler(resp)
+		return model.Usage{}, ErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
@@ -245,16 +248,20 @@ func StreamHandler(
 
 	render.Done(c)
 
+	if usage == nil {
+		return meta.RequestUsage, nil
+	}
+
 	return usage.ToModelUsage(), nil
 }
 
 func ConvertEmbeddingRequest(
 	meta *meta.Meta,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
+) (adaptor.ConvertResult, error) {
 	request, err := utils.UnmarshalGeneralOpenAIRequest(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 	request.Model = meta.ActualModel
 	data, err := sonic.Marshal(&EmbeddingRequest{
@@ -269,12 +276,14 @@ func ConvertEmbeddingRequest(
 		},
 	})
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(data),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(data))},
+		},
+		Body: bytes.NewReader(data),
 	}, nil
 }
 
@@ -282,9 +291,9 @@ func EmbeddingHandler(
 	meta *meta.Meta,
 	c *gin.Context,
 	resp *http.Response,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHandler(resp)
+		return model.Usage{}, ErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
@@ -292,7 +301,7 @@ func EmbeddingHandler(
 	var ollamaResponse EmbeddingResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&ollamaResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
@@ -300,7 +309,7 @@ func EmbeddingHandler(
 	}
 
 	if ollamaResponse.Error != "" {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			ollamaResponse.Error,
 			relaymodel.ErrorTypeUpstream,
 			resp.StatusCode,
@@ -310,14 +319,14 @@ func EmbeddingHandler(
 	fullTextResponse := embeddingResponseOllama2OpenAI(meta, &ollamaResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }
@@ -348,9 +357,9 @@ func embeddingResponseOllama2OpenAI(
 	return &openAIEmbeddingResponse
 }
 
-func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, adaptor.Error) {
+func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHandler(resp)
+		return model.Usage{}, ErrorHandler(resp)
 	}
 
 	defer resp.Body.Close()
@@ -358,31 +367,24 @@ func Handler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage
 	var ollamaResponse ChatResponse
 	err := sonic.ConfigDefault.NewDecoder(resp.Body).Decode(&ollamaResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
-	if ollamaResponse.Error != "" {
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
-			ollamaResponse.Error,
-			relaymodel.ErrorTypeUpstream,
-			resp.StatusCode,
-		)
-	}
-	fullTextResponse := response2OpenAI(meta, &ollamaResponse)
 
+	fullTextResponse := response2OpenAI(meta, &ollamaResponse)
 	jsonResponse, err := sonic.Marshal(fullTextResponse)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return fullTextResponse.ToModelUsage(), relaymodel.WrapperOpenAIError(
 			err,
 			"marshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 	c.Writer.Header().Set("Content-Type", "application/json")
-	c.Writer.WriteHeader(resp.StatusCode)
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(jsonResponse)))
 	_, _ = c.Writer.Write(jsonResponse)
 	return fullTextResponse.ToModelUsage(), nil
 }

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

@@ -47,7 +47,6 @@ type ChatResponse struct {
 	Model           string   `json:"model,omitempty"`
 	CreatedAt       string   `json:"created_at,omitempty"`
 	Response        string   `json:"response,omitempty"`
-	Error           string   `json:"error,omitempty"`
 	Message         *Message `json:"message,omitempty"`
 	TotalDuration   int      `json:"total_duration,omitempty"`
 	LoadDuration    int      `json:"load_duration,omitempty"`

+ 106 - 94
core/relay/adaptor/openai/adaptor.go

@@ -1,14 +1,11 @@
 package openai
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 	"net/http"
 
-	"github.com/bytedance/sonic"
 	"github.com/gin-gonic/gin"
-	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/model"
 	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/meta"
@@ -23,65 +20,119 @@ type Adaptor struct{}
 
 const baseURL = "https://api.openai.com/v1"
 
-func (a *Adaptor) GetBaseURL() string {
+func (a *Adaptor) DefaultBaseURL() string {
 	return baseURL
 }
 
-func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
+func (a *Adaptor) GetRequestURL(meta *meta.Meta, _ adaptor.Store) (adaptor.RequestURL, error) {
 	u := meta.Channel.BaseURL
 
-	var path string
 	switch meta.Mode {
 	case mode.ChatCompletions:
-		path = "/chat/completions"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/chat/completions",
+		}, nil
 	case mode.Completions:
-		path = "/completions"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/completions",
+		}, nil
 	case mode.Embeddings:
-		path = "/embeddings"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/embeddings",
+		}, nil
 	case mode.Moderations:
-		path = "/moderations"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/moderations",
+		}, nil
 	case mode.ImagesGenerations:
-		path = "/images/generations"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/images/generations",
+		}, nil
 	case mode.ImagesEdits:
-		path = "/images/edits"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/images/edits",
+		}, nil
 	case mode.AudioSpeech:
-		path = "/audio/speech"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/audio/speech",
+		}, nil
 	case mode.AudioTranscription:
-		path = "/audio/transcriptions"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/audio/transcriptions",
+		}, nil
 	case mode.AudioTranslation:
-		path = "/audio/translations"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/audio/translations",
+		}, nil
 	case mode.Rerank:
-		path = "/rerank"
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/rerank",
+		}, nil
+	case mode.VideoGenerationsJobs:
+		return adaptor.RequestURL{
+			Method: http.MethodPost,
+			URL:    u + "/video/generations/jobs",
+		}, nil
+	case mode.VideoGenerationsGetJobs:
+		return adaptor.RequestURL{
+			Method: http.MethodGet,
+			URL:    fmt.Sprintf("%s/video/generations/jobs/%s", u, meta.JobID),
+		}, nil
+	case mode.VideoGenerationsContent:
+		return adaptor.RequestURL{
+			Method: http.MethodGet,
+			URL:    fmt.Sprintf("%s/video/generations/%s/content/video", u, meta.GenerationID),
+		}, nil
 	default:
-		return "", fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.RequestURL{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
-
-	return u + path, nil
 }
 
-func (a *Adaptor) SetupRequestHeader(meta *meta.Meta, _ *gin.Context, req *http.Request) error {
+func (a *Adaptor) SetupRequestHeader(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	_ *gin.Context,
+	req *http.Request,
+) error {
 	req.Header.Set("Authorization", "Bearer "+meta.Channel.Key)
 	return nil
 }
 
 func (a *Adaptor) ConvertRequest(
 	meta *meta.Meta,
+	store adaptor.Store,
 	req *http.Request,
-) (*adaptor.ConvertRequestResult, error) {
-	return ConvertRequest(meta, req)
+) (adaptor.ConvertResult, error) {
+	return ConvertRequest(meta, store, req)
 }
 
-func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequestResult, error) {
+func ConvertRequest(
+	meta *meta.Meta,
+	_ adaptor.Store,
+	req *http.Request,
+) (adaptor.ConvertResult, error) {
 	if req == nil {
-		return nil, errors.New("request is nil")
+		return adaptor.ConvertResult{}, errors.New("request is nil")
 	}
 	switch meta.Mode {
 	case mode.Moderations:
-		return ConvertEmbeddingsRequest(meta, req, true)
-	case mode.Embeddings, mode.Completions:
-		return ConvertEmbeddingsRequest(meta, req, false)
+		return ConvertModerationsRequest(meta, req)
+	case mode.Embeddings:
+		return ConvertEmbeddingsRequest(meta, req, nil, false)
+	case mode.Completions:
+		return ConvertCompletionsRequest(meta, req, nil)
 	case mode.ChatCompletions:
-		return ConvertTextRequest(meta, req, false)
+		return ConvertChatCompletionsRequest(meta, req, nil, false)
 	case mode.ImagesGenerations:
 		return ConvertImagesRequest(meta, req)
 	case mode.ImagesEdits:
@@ -92,16 +143,23 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (*adaptor.ConvertRequest
 		return ConvertTTSRequest(meta, req, "")
 	case mode.Rerank:
 		return ConvertRerankRequest(meta, req)
+	case mode.VideoGenerationsJobs:
+		return ConvertVideoRequest(meta, req)
+	case mode.VideoGenerationsGetJobs:
+		return ConvertVideoGetJobsRequest(meta, req)
+	case mode.VideoGenerationsContent:
+		return ConvertVideoGetJobsContentRequest(meta, req)
 	default:
-		return nil, fmt.Errorf("unsupported mode: %s", meta.Mode)
+		return adaptor.ConvertResult{}, fmt.Errorf("unsupported mode: %s", meta.Mode)
 	}
 }
 
 func DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
+) (usage model.Usage, err adaptor.Error) {
 	switch meta.Mode {
 	case mode.ImagesGenerations, mode.ImagesEdits:
 		usage, err = ImagesHandler(meta, c, resp)
@@ -121,8 +179,14 @@ func DoResponse(
 		} else {
 			usage, err = Handler(meta, c, resp, nil)
 		}
+	case mode.VideoGenerationsJobs:
+		usage, err = VideoHandler(meta, store, c, resp)
+	case mode.VideoGenerationsGetJobs:
+		usage, err = VideoGetJobsHandler(meta, store, c, resp)
+	case mode.VideoGenerationsContent:
+		usage, err = VideoGetJobsContentHandler(meta, store, c, resp)
 	default:
-		return nil, relaymodel.WrapperOpenAIErrorWithMessage(
+		return model.Usage{}, relaymodel.WrapperOpenAIErrorWithMessage(
 			fmt.Sprintf("unsupported mode: %s", meta.Mode),
 			"unsupported_mode",
 			http.StatusBadRequest,
@@ -131,69 +195,11 @@ func DoResponse(
 	return usage, err
 }
 
-func ConvertTextRequest(
-	meta *meta.Meta,
-	req *http.Request,
-	doNotPatchStreamOptionsIncludeUsage bool,
-) (*adaptor.ConvertRequestResult, error) {
-	reqMap := make(map[string]any)
-	err := common.UnmarshalBodyReusable(req, &reqMap)
-	if err != nil {
-		return nil, err
-	}
-
-	if !doNotPatchStreamOptionsIncludeUsage {
-		if err := patchStreamOptions(reqMap); err != nil {
-			return nil, err
-		}
-	}
-
-	reqMap["model"] = meta.ActualModel
-	jsonData, err := sonic.Marshal(reqMap)
-	if err != nil {
-		return nil, err
-	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(jsonData),
-	}, nil
-}
-
-func patchStreamOptions(reqMap map[string]any) error {
-	stream, ok := reqMap["stream"]
-	if !ok {
-		return nil
-	}
-
-	streamBool, ok := stream.(bool)
-	if !ok {
-		return errors.New("stream is not a boolean")
-	}
-
-	if !streamBool {
-		return nil
-	}
-
-	streamOptions, ok := reqMap["stream_options"].(map[string]any)
-	if !ok {
-		if reqMap["stream_options"] != nil {
-			return errors.New("stream_options is not a map")
-		}
-		reqMap["stream_options"] = map[string]any{
-			"include_usage": true,
-		}
-		return nil
-	}
-
-	streamOptions["include_usage"] = true
-	return nil
-}
-
 const MetaResponseFormat = "response_format"
 
 func (a *Adaptor) DoRequest(
 	_ *meta.Meta,
+	_ adaptor.Store,
 	_ *gin.Context,
 	req *http.Request,
 ) (*http.Response, error) {
@@ -202,12 +208,18 @@ func (a *Adaptor) DoRequest(
 
 func (a *Adaptor) DoResponse(
 	meta *meta.Meta,
+	store adaptor.Store,
 	c *gin.Context,
 	resp *http.Response,
-) (usage *model.Usage, err adaptor.Error) {
-	return DoResponse(meta, c, resp)
+) (usage model.Usage, err adaptor.Error) {
+	return DoResponse(meta, store, c, resp)
 }
 
-func (a *Adaptor) GetModelList() []model.ModelConfig {
-	return ModelList
+func (a *Adaptor) Metadata() adaptor.Metadata {
+	return adaptor.Metadata{
+		Features: []string{
+			"OpenAI compatibility",
+		},
+		Models: ModelList,
+	}
 }

+ 128 - 25
core/relay/adaptor/openai/main.go → core/relay/adaptor/openai/chat.go

@@ -8,6 +8,7 @@ import (
 	"io"
 	"net/http"
 	"slices"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -15,6 +16,7 @@ import (
 	"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/common/conv"
 	"github.com/labring/aiproxy/core/common/render"
 	"github.com/labring/aiproxy/core/middleware"
@@ -60,24 +62,125 @@ func PutScannerBuffer(buf *[]byte) {
 	scannerBufferPool.Put(buf)
 }
 
+func ConvertCompletionsRequest(
+	meta *meta.Meta,
+	req *http.Request,
+	callback func(node *ast.Node) error,
+) (adaptor.ConvertResult, error) {
+	node, err := common.UnmarshalBody2Node(req)
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+
+	if callback != nil {
+		if err := callback(&node); err != nil {
+			return adaptor.ConvertResult{}, err
+		}
+	}
+
+	_, err = node.Set("model", ast.NewString(meta.ActualModel))
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+
+	jsonData, err := node.MarshalJSON()
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(jsonData))},
+		},
+		Body: bytes.NewReader(jsonData),
+	}, nil
+}
+
+func ConvertChatCompletionsRequest(
+	meta *meta.Meta,
+	req *http.Request,
+	callback func(node *ast.Node) error,
+	doNotPatchStreamOptionsIncludeUsage bool,
+) (adaptor.ConvertResult, error) {
+	node, err := common.UnmarshalBody2Node(req)
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+
+	if callback != nil {
+		if err := callback(&node); err != nil {
+			return adaptor.ConvertResult{}, err
+		}
+	}
+
+	if !doNotPatchStreamOptionsIncludeUsage {
+		if err := patchStreamOptions(&node); err != nil {
+			return adaptor.ConvertResult{}, err
+		}
+	}
+
+	_, err = node.Set("model", ast.NewString(meta.ActualModel))
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+
+	jsonData, err := node.MarshalJSON()
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type":   {"application/json"},
+			"Content-Length": {strconv.Itoa(len(jsonData))},
+		},
+		Body: bytes.NewReader(jsonData),
+	}, nil
+}
+
+func patchStreamOptions(node *ast.Node) error {
+	streamNode := node.Get("stream")
+	if !streamNode.Exists() {
+		return nil
+	}
+	streamBool, err := streamNode.Bool()
+	if err != nil {
+		return errors.New("stream is not a boolean")
+	}
+	if !streamBool {
+		return nil
+	}
+
+	streamOptionsNode := node.Get("stream_options")
+	if !streamOptionsNode.Exists() {
+		_, err = node.SetAny("stream_options", map[string]any{
+			"include_usage": true,
+		})
+		return err
+	}
+
+	if streamOptionsNode.TypeSafe() != ast.V_OBJECT {
+		return errors.New("stream_options is not an object")
+	}
+
+	_, err = streamOptionsNode.Set("include_usage", ast.NewBool(true))
+	return err
+}
+
 func GetUsageOrChatChoicesResponseFromNode(
 	node *ast.Node,
 ) (*relaymodel.Usage, []*relaymodel.ChatCompletionsStreamResponseChoice, error) {
-	var usage *relaymodel.Usage
 	usageNode, err := node.Get("usage").Raw()
 	if err != nil {
 		if !errors.Is(err, ast.ErrNotExist) {
 			return nil, nil, err
 		}
 	} else {
+		var usage relaymodel.Usage
 		err = sonic.UnmarshalString(usageNode, &usage)
 		if err != nil {
 			return nil, nil, err
 		}
-	}
-
-	if usage != nil {
-		return usage, nil, nil
+		return &usage, nil, nil
 	}
 
 	var choices []*relaymodel.ChatCompletionsStreamResponseChoice
@@ -102,9 +205,9 @@ func StreamHandler(
 	c *gin.Context,
 	resp *http.Response,
 	preHandler PreHandler,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHanlder(resp)
+		return model.Usage{}, ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -118,7 +221,7 @@ func StreamHandler(
 	defer PutScannerBuffer(buf)
 	scanner.Buffer(*buf, cap(*buf))
 
-	var usage *relaymodel.Usage
+	var usage relaymodel.Usage
 
 	for scanner.Scan() {
 		data := scanner.Bytes()
@@ -151,11 +254,11 @@ func StreamHandler(
 			continue
 		}
 		if u != nil {
-			usage = u
+			usage = *u
 			responseText.Reset()
 		}
 		for _, choice := range ch {
-			if usage == nil {
+			if usage.TotalTokens == 0 {
 				if choice.Text != "" {
 					responseText.WriteString(choice.Text)
 				} else {
@@ -176,7 +279,7 @@ func StreamHandler(
 		log.Error("error reading stream: " + err.Error())
 	}
 
-	if usage == nil || (usage.TotalTokens == 0 && responseText.Len() > 0) {
+	if usage.TotalTokens == 0 && responseText.Len() > 0 {
 		usage = ResponseText2Usage(
 			responseText.String(),
 			meta.ActualModel,
@@ -185,10 +288,10 @@ func StreamHandler(
 		_ = render.ObjectData(c, &relaymodel.ChatCompletionsStreamResponse{
 			ID:      ChatCompletionID(),
 			Model:   meta.OriginModel,
-			Object:  relaymodel.ChatCompletionChunk,
+			Object:  relaymodel.ChatCompletionChunkObject,
 			Created: time.Now().Unix(),
 			Choices: []*relaymodel.ChatCompletionsStreamResponseChoice{},
-			Usage:   usage,
+			Usage:   &usage,
 		})
 	} else if usage.TotalTokens != 0 && usage.PromptTokens == 0 { // some channels don't return prompt tokens & completion tokens
 		usage.PromptTokens = int64(meta.RequestUsage.InputTokens)
@@ -203,21 +306,18 @@ func StreamHandler(
 func GetUsageOrChoicesResponseFromNode(
 	node *ast.Node,
 ) (*relaymodel.Usage, []*relaymodel.TextResponseChoice, error) {
-	var usage *relaymodel.Usage
 	usageNode, err := node.Get("usage").Raw()
 	if err != nil {
 		if !errors.Is(err, ast.ErrNotExist) {
 			return nil, nil, err
 		}
 	} else {
+		var usage relaymodel.Usage
 		err = sonic.UnmarshalString(usageNode, &usage)
 		if err != nil {
 			return nil, nil, err
 		}
-	}
-
-	if usage != nil {
-		return usage, nil, nil
+		return &usage, nil, nil
 	}
 
 	var choices []*relaymodel.TextResponseChoice
@@ -240,9 +340,9 @@ func Handler(
 	c *gin.Context,
 	resp *http.Response,
 	preHandler PreHandler,
-) (*model.Usage, adaptor.Error) {
+) (model.Usage, adaptor.Error) {
 	if resp.StatusCode != http.StatusOK {
-		return nil, ErrorHanlder(resp)
+		return model.Usage{}, ErrorHanlder(resp)
 	}
 
 	defer resp.Body.Close()
@@ -251,7 +351,7 @@ func Handler(
 
 	responseBody, err := io.ReadAll(resp.Body)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"read_response_body_failed",
 			http.StatusInternalServerError,
@@ -260,7 +360,7 @@ func Handler(
 
 	node, err := sonic.Get(responseBody)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
@@ -269,7 +369,7 @@ func Handler(
 	if preHandler != nil {
 		err := preHandler(meta, &node)
 		if err != nil {
-			return nil, relaymodel.WrapperOpenAIError(
+			return model.Usage{}, relaymodel.WrapperOpenAIError(
 				err,
 				"pre_handler_failed",
 				http.StatusInternalServerError,
@@ -278,14 +378,15 @@ func Handler(
 	}
 	usage, choices, err := GetUsageOrChoicesResponseFromNode(&node)
 	if err != nil {
-		return nil, relaymodel.WrapperOpenAIError(
+		return model.Usage{}, relaymodel.WrapperOpenAIError(
 			err,
 			"unmarshal_response_body_failed",
 			http.StatusInternalServerError,
 		)
 	}
 
-	if usage == nil || usage.TotalTokens == 0 ||
+	if usage == nil ||
+		usage.TotalTokens == 0 ||
 		(usage.PromptTokens == 0 && usage.CompletionTokens == 0) {
 		var completionTokens int64
 		for _, choice := range choices {
@@ -335,6 +436,8 @@ func Handler(
 		)
 	}
 
+	c.Writer.Header().Set("Content-Type", "application/json")
+	c.Writer.Header().Set("Content-Length", strconv.Itoa(len(newData)))
 	_, err = c.Writer.Write(newData)
 	if err != nil {
 		log.Warnf("write response body failed: %v", err)

+ 37 - 17
core/relay/adaptor/openai/embeddings.go

@@ -2,43 +2,63 @@ package openai
 
 import (
 	"bytes"
+	"errors"
 	"net/http"
 
-	"github.com/bytedance/sonic"
+	"github.com/bytedance/sonic/ast"
 	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/relay/adaptor"
 	"github.com/labring/aiproxy/core/relay/meta"
 )
 
-//
-//nolint:gocritic
 func ConvertEmbeddingsRequest(
 	meta *meta.Meta,
 	req *http.Request,
+	callback func(node *ast.Node) error,
 	inputToSlices bool,
-) (*adaptor.ConvertRequestResult, error) {
-	reqMap := make(map[string]any)
-	err := common.UnmarshalBodyReusable(req, &reqMap)
+) (adaptor.ConvertResult, error) {
+	node, err := common.UnmarshalBody2Node(req)
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
 
-	reqMap["model"] = meta.ActualModel
+	if callback != nil {
+		err = callback(&node)
+		if err != nil {
+			return adaptor.ConvertResult{}, err
+		}
+	}
+
+	_, err = node.Set("model", ast.NewString(meta.ActualModel))
+	if err != nil {
+		return adaptor.ConvertResult{}, err
+	}
 
 	if inputToSlices {
-		switch v := reqMap["input"].(type) {
-		case string:
-			reqMap["input"] = []string{v}
+		inputNode := node.Get("input")
+		if inputNode.Exists() {
+			inputString, err := inputNode.String()
+			if err != nil {
+				if !errors.Is(err, ast.ErrUnsupportType) {
+					return adaptor.ConvertResult{}, err
+				}
+			} else {
+				_, err = node.SetAny("input", []string{inputString})
+				if err != nil {
+					return adaptor.ConvertResult{}, err
+				}
+			}
 		}
 	}
 
-	jsonData, err := sonic.Marshal(reqMap)
+	jsonData, err := node.MarshalJSON()
 	if err != nil {
-		return nil, err
+		return adaptor.ConvertResult{}, err
 	}
-	return &adaptor.ConvertRequestResult{
-		Method: http.MethodPost,
-		Header: nil,
-		Body:   bytes.NewReader(jsonData),
+	return adaptor.ConvertResult{
+		Header: http.Header{
+			"Content-Type": {"application/json"},
+		},
+		Body: bytes.NewReader(jsonData),
 	}, nil
 }

+ 29 - 0
core/relay/adaptor/openai/error.go

@@ -81,3 +81,32 @@ func ErrorHanlderWithBody(statusCode int, respBody []byte) adaptor.Error {
 	statusCode, openAIError := GetErrorWithBody(statusCode, respBody)
 	return relaymodel.NewOpenAIError(statusCode, openAIError)
 }
+
+func VideoErrorHanlder(resp *http.Response) adaptor.Error {
+	defer resp.Body.Close()
+
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return relaymodel.NewOpenAIVideoError(resp.StatusCode, relaymodel.OpenAIVideoError{
+			Detail: err.Error(),
+		})
+	}
+
+	return VideoErrorHanlderWithBody(resp.StatusCode, respBody)
+}
+
+func VideoErrorHanlderWithBody(statusCode int, respBody []byte) adaptor.Error {
+	statusCode, openAIError := GetVideoErrorWithBody(statusCode, respBody)
+	return relaymodel.NewOpenAIVideoError(statusCode, openAIError)
+}
+
+func GetVideoErrorWithBody(statusCode int, respBody []byte) (int, relaymodel.OpenAIVideoError) {
+	openAIError := relaymodel.OpenAIVideoError{}
+	err := sonic.Unmarshal(respBody, &openAIError)
+	if err != nil {
+		openAIError.Detail = string(respBody)
+		return statusCode, openAIError
+	}
+
+	return statusCode, openAIError
+}

+ 0 - 11
core/relay/adaptor/openai/fetures.go

@@ -1,11 +0,0 @@
-package openai
-
-import "github.com/labring/aiproxy/core/relay/adaptor"
-
-var _ adaptor.Features = (*Adaptor)(nil)
-
-func (a *Adaptor) Features() []string {
-	return []string{
-		"OpenAI compatibility",
-	}
-}

+ 8 - 15
core/relay/adaptor/openai/helper.go

@@ -1,14 +1,12 @@
 package openai
 
 import (
-	"fmt"
-	"strings"
-
+	"github.com/labring/aiproxy/core/common"
 	"github.com/labring/aiproxy/core/relay/model"
 )
 
-func ResponseText2Usage(responseText, modeName string, promptTokens int64) *model.Usage {
-	usage := &model.Usage{
+func ResponseText2Usage(responseText, modeName string, promptTokens int64) model.Usage {
+	usage := model.Usage{
 		PromptTokens:     promptTokens,
 		CompletionTokens: CountTokenText(responseText, modeName),
 	}
@@ -16,15 +14,10 @@ func ResponseText2Usage(responseText, modeName string, promptTokens int64) *mode
 	return usage
 }
 
-func GetFullRequestURL(baseURL, requestURL string) string {
-	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
+func ChatCompletionID() string {
+	return "chatcmpl-" + common.ShortUUID()
+}
 
-	if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") {
-		fullRequestURL = fmt.Sprintf(
-			"%s%s",
-			baseURL,
-			strings.TrimPrefix(requestURL, "/openai/deployments"),
-		)
-	}
-	return fullRequestURL
+func CallID() string {
+	return "call_" + common.ShortUUID()
 }

Some files were not shown because too many files changed in this diff