Преглед изворни кода

feat: image edit (#159)

* feat: image edit

* fix: output tokens
zijiren пре 8 месеци
родитељ
комит
db6953796f

+ 2 - 7
core/common/consume/consume.go

@@ -143,7 +143,7 @@ func CalculateAmount(
 		Mul(decimal.NewFromFloat(modelPrice.InputPrice)).
 		Div(decimal.NewFromInt(modelPrice.GetInputPriceUnit()))
 
-	inputImageAmount := decimal.NewFromInt(usage.ImageInputTokens).
+	imageInputAmount := decimal.NewFromInt(usage.ImageInputTokens).
 		Mul(decimal.NewFromFloat(modelPrice.ImageInputPrice)).
 		Div(decimal.NewFromInt(modelPrice.GetImageInputPriceUnit()))
 
@@ -163,17 +163,12 @@ func CalculateAmount(
 		Mul(decimal.NewFromFloat(modelPrice.OutputPrice)).
 		Div(decimal.NewFromInt(modelPrice.GetOutputPriceUnit()))
 
-	imageOutputAmount := decimal.NewFromInt(usage.ImageOutputNumbers).
-		Mul(decimal.NewFromFloat(modelPrice.ImageOutputPrice)).
-		Div(decimal.NewFromInt(modelPrice.GetImageOutputPriceUnit()))
-
 	return inputAmount.
-		Add(inputImageAmount).
+		Add(imageInputAmount).
 		Add(cachedAmount).
 		Add(cacheCreationAmount).
 		Add(webSearchAmount).
 		Add(outputAmount).
-		Add(imageOutputAmount).
 		InexactFloat64()
 }
 

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

@@ -53,9 +53,12 @@ func relayController(m mode.Mode) RelayController {
 		Handler: relayHandler,
 	}
 	switch m {
-	case mode.ImagesGenerations, mode.Edits:
-		c.GetRequestPrice = controller.GetImageRequestPrice
-		c.GetRequestUsage = controller.GetImageRequestUsage
+	case mode.ImagesGenerations:
+		c.GetRequestPrice = controller.GetImagesRequestPrice
+		c.GetRequestUsage = controller.GetImagesRequestUsage
+	case mode.ImagesEdits:
+		c.GetRequestPrice = controller.GetImagesEditsRequestPrice
+		c.GetRequestUsage = controller.GetImagesEditsRequestUsage
 	case mode.AudioSpeech:
 		c.GetRequestPrice = controller.GetTTSRequestPrice
 		c.GetRequestUsage = controller.GetTTSRequestUsage

+ 22 - 3
core/controller/relay.go

@@ -105,10 +105,29 @@ func Embeddings() []gin.HandlerFunc {
 	}
 }
 
-func Edits() []gin.HandlerFunc {
+// ImagesEdits godoc
+//
+//	@Summary		ImagesEdits
+//	@Description	ImagesEdits
+//	@Tags			relay
+//	@Produce		json
+//	@Security		ApiKeyAuth
+//	@Param			prompt			formData	string	true	"Prompt"
+//	@Param			model			formData	string	true	"Model"
+//	@Param			image			formData	file	true	"Images"
+//	@Param			Aiproxy-Channel	header		string	false	"Optional Aiproxy-Channel header"
+//	@Success		200				{object}	model.SttJSONResponse
+//	@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/images/edits [post]
+func ImagesEdits() []gin.HandlerFunc {
 	return []gin.HandlerFunc{
-		middleware.NewDistribute(mode.Edits),
-		NewRelay(mode.Edits),
+		middleware.NewDistribute(mode.ImagesEdits),
+		NewRelay(mode.ImagesEdits),
 	}
 }
 

+ 1 - 10
core/docs/docs.go

@@ -7792,9 +7792,6 @@ const docTemplate = `{
                 "image_input_tokens": {
                     "type": "integer"
                 },
-                "image_output_numbers": {
-                    "type": "integer"
-                },
                 "input_tokens": {
                     "type": "integer"
                 },
@@ -7868,7 +7865,7 @@ const docTemplate = `{
                 "Embeddings",
                 "Moderations",
                 "ImagesGenerations",
-                "Edits",
+                "ImagesEdits",
                 "AudioSpeech",
                 "AudioTranscription",
                 "AudioTranslation",
@@ -9141,12 +9138,6 @@ const docTemplate = `{
                 "image_input_price_unit": {
                     "type": "integer"
                 },
-                "image_output_price": {
-                    "type": "number"
-                },
-                "image_output_price_unit": {
-                    "type": "integer"
-                },
                 "input_price": {
                     "type": "number"
                 },

+ 1 - 10
core/docs/swagger.json

@@ -7783,9 +7783,6 @@
                 "image_input_tokens": {
                     "type": "integer"
                 },
-                "image_output_numbers": {
-                    "type": "integer"
-                },
                 "input_tokens": {
                     "type": "integer"
                 },
@@ -7859,7 +7856,7 @@
                 "Embeddings",
                 "Moderations",
                 "ImagesGenerations",
-                "Edits",
+                "ImagesEdits",
                 "AudioSpeech",
                 "AudioTranscription",
                 "AudioTranslation",
@@ -9132,12 +9129,6 @@
                 "image_input_price_unit": {
                     "type": "integer"
                 },
-                "image_output_price": {
-                    "type": "number"
-                },
-                "image_output_price_unit": {
-                    "type": "integer"
-                },
                 "input_price": {
                     "type": "number"
                 },

+ 1 - 7
core/docs/swagger.yaml

@@ -352,8 +352,6 @@ definitions:
         type: integer
       image_input_tokens:
         type: integer
-      image_output_numbers:
-        type: integer
       input_tokens:
         type: integer
       output_tokens:
@@ -409,7 +407,7 @@ definitions:
     - Embeddings
     - Moderations
     - ImagesGenerations
-    - Edits
+    - ImagesEdits
     - AudioSpeech
     - AudioTranscription
     - AudioTranslation
@@ -1303,10 +1301,6 @@ definitions:
         type: number
       image_input_price_unit:
         type: integer
-      image_output_price:
-        type: number
-      image_output_price_unit:
-        type: integer
       input_price:
         type: number
       input_price_unit:

+ 2 - 1
core/middleware/distributor.go

@@ -434,7 +434,8 @@ func getRequestModel(c *gin.Context, m mode.Mode) (string, error) {
 
 		fallthrough
 	case m == mode.AudioTranscription,
-		m == mode.AudioTranslation:
+		m == mode.AudioTranslation,
+		m == mode.ImagesEdits:
 		return c.Request.FormValue("model"), nil
 
 	case strings.HasPrefix(path, "/v1/engines") && strings.HasSuffix(path, "/embeddings"):

+ 0 - 12
core/model/log.go

@@ -50,9 +50,6 @@ type Price struct {
 	OutputPrice     float64 `json:"output_price,omitempty"`
 	OutputPriceUnit int64   `json:"output_price_unit,omitempty"`
 
-	ImageOutputPrice     float64 `json:"image_output_price,omitempty"`
-	ImageOutputPriceUnit int64   `json:"image_output_price_unit,omitempty"`
-
 	CachedPrice     float64 `json:"cached_price,omitempty"`
 	CachedPriceUnit int64   `json:"cached_price_unit,omitempty"`
 
@@ -77,13 +74,6 @@ func (p *Price) GetImageInputPriceUnit() int64 {
 	return PriceUnit
 }
 
-func (p *Price) GetImageOutputPriceUnit() int64 {
-	if p.ImageOutputPriceUnit > 0 {
-		return p.ImageOutputPriceUnit
-	}
-	return PriceUnit
-}
-
 func (p *Price) GetOutputPriceUnit() int64 {
 	if p.OutputPriceUnit > 0 {
 		return p.OutputPriceUnit
@@ -116,7 +106,6 @@ type Usage struct {
 	InputTokens         int64 `json:"input_tokens,omitempty"`
 	ImageInputTokens    int64 `json:"image_input_tokens,omitempty"`
 	OutputTokens        int64 `json:"output_tokens,omitempty"`
-	ImageOutputNumbers  int64 `json:"image_output_numbers,omitempty"`
 	CachedTokens        int64 `json:"cached_tokens,omitempty"`
 	CacheCreationTokens int64 `json:"cache_creation_tokens,omitempty"`
 	TotalTokens         int64 `json:"total_tokens,omitempty"`
@@ -130,7 +119,6 @@ func (u *Usage) Add(other *Usage) {
 	u.InputTokens += other.InputTokens
 	u.ImageInputTokens += other.ImageInputTokens
 	u.OutputTokens += other.OutputTokens
-	u.ImageOutputNumbers += other.ImageOutputNumbers
 	u.CachedTokens += other.CachedTokens
 	u.CacheCreationTokens += other.CacheCreationTokens
 	u.TotalTokens += other.TotalTokens

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

@@ -43,7 +43,7 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
 		path = "/moderations"
 	case mode.ImagesGenerations:
 		path = "/images/generations"
-	case mode.Edits:
+	case mode.ImagesEdits:
 		path = "/images/edits"
 	case mode.AudioSpeech:
 		path = "/audio/speech"
@@ -81,7 +81,9 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (string, http.Header, io
 	case mode.ChatCompletions:
 		return ConvertTextRequest(meta, req, false)
 	case mode.ImagesGenerations:
-		return ConvertImageRequest(meta, req)
+		return ConvertImagesRequest(meta, req)
+	case mode.ImagesEdits:
+		return ConvertImagesEditsRequest(meta, req)
 	case mode.AudioTranscription, mode.AudioTranslation:
 		return ConvertSTTRequest(meta, req)
 	case mode.AudioSpeech:
@@ -95,8 +97,8 @@ func ConvertRequest(meta *meta.Meta, req *http.Request) (string, http.Header, io
 
 func DoResponse(meta *meta.Meta, c *gin.Context, resp *http.Response) (usage *model.Usage, err *relaymodel.ErrorWithStatusCode) {
 	switch meta.Mode {
-	case mode.ImagesGenerations:
-		usage, err = ImageHandler(meta, c, resp)
+	case mode.ImagesGenerations, mode.ImagesEdits:
+		usage, err = ImagesHandler(meta, c, resp)
 	case mode.AudioTranscription, mode.AudioTranslation:
 		usage, err = STTHandler(meta, c, resp)
 	case mode.AudioSpeech:

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

@@ -154,7 +154,7 @@ var ModelList = []*model.ModelConfig{
 	},
 	{
 		Model: "text-davinci-edit-001",
-		Type:  mode.Edits,
+		Type:  mode.ImagesEdits,
 		Owner: model.ModelOwnerOpenAI,
 	},
 	{
@@ -167,6 +167,7 @@ var ModelList = []*model.ModelConfig{
 		Type:  mode.Completions,
 		Owner: model.ModelOwnerOpenAI,
 	},
+
 	{
 		Model: "dall-e-2",
 		Type:  mode.ImagesGenerations,
@@ -177,6 +178,17 @@ var ModelList = []*model.ModelConfig{
 		Type:  mode.ImagesGenerations,
 		Owner: model.ModelOwnerOpenAI,
 	},
+	{
+		Model: "gpt-image-1",
+		Type:  mode.ImagesGenerations,
+		Owner: model.ModelOwnerOpenAI,
+		Price: model.Price{
+			InputPrice:      0.005,
+			ImageInputPrice: 0.01,
+			OutputPrice:     0.04,
+		},
+	},
+
 	{
 		Model: "whisper-1",
 		Type:  mode.AudioTranscription,

+ 65 - 6
core/relay/adaptor/openai/image.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"errors"
 	"io"
+	"mime/multipart"
 	"net/http"
 
 	"github.com/bytedance/sonic"
@@ -17,7 +18,7 @@ import (
 	relaymodel "github.com/labring/aiproxy/core/relay/model"
 )
 
-func ConvertImageRequest(meta *meta.Meta, req *http.Request) (string, http.Header, io.Reader, error) {
+func ConvertImagesRequest(meta *meta.Meta, req *http.Request) (string, http.Header, io.Reader, error) {
 	node, err := common.UnmarshalBody2Node(req)
 	if err != nil {
 		return "", nil, nil, err
@@ -41,7 +42,66 @@ func ConvertImageRequest(meta *meta.Meta, req *http.Request) (string, http.Heade
 	return http.MethodPost, nil, bytes.NewReader(jsonData), nil
 }
 
-func ImageHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *relaymodel.ErrorWithStatusCode) {
+func ConvertImagesEditsRequest(meta *meta.Meta, request *http.Request) (string, http.Header, io.Reader, error) {
+	err := request.ParseMultipartForm(1024 * 1024 * 4)
+	if err != nil {
+		return "", nil, nil, err
+	}
+
+	multipartBody := &bytes.Buffer{}
+	multipartWriter := multipart.NewWriter(multipartBody)
+
+	for key, values := range request.MultipartForm.Value {
+		if len(values) == 0 {
+			continue
+		}
+		value := values[0]
+		if key == "model" {
+			err = multipartWriter.WriteField(key, meta.ActualModel)
+			if err != nil {
+				return "", nil, nil, err
+			}
+			continue
+		}
+		if key == "response_format" {
+			meta.Set(MetaResponseFormat, value)
+			continue
+		}
+		err = multipartWriter.WriteField(key, value)
+		if err != nil {
+			return "", nil, nil, err
+		}
+	}
+
+	for key, files := range request.MultipartForm.File {
+		if len(files) == 0 {
+			continue
+		}
+		fileHeader := files[0]
+		file, err := fileHeader.Open()
+		if err != nil {
+			return "", nil, nil, err
+		}
+		w, err := multipartWriter.CreateFormFile(key, fileHeader.Filename)
+		if err != nil {
+			file.Close()
+			return "", nil, nil, err
+		}
+		_, err = io.Copy(w, file)
+		file.Close()
+		if err != nil {
+			return "", nil, nil, err
+		}
+	}
+
+	multipartWriter.Close()
+	ContentType := multipartWriter.FormDataContentType()
+	return http.MethodPost, http.Header{
+		"Content-Type": {ContentType},
+	}, multipartBody, nil
+}
+
+func ImagesHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.Usage, *relaymodel.ErrorWithStatusCode) {
 	if resp.StatusCode != http.StatusOK {
 		return nil, ErrorHanlder(resp)
 	}
@@ -61,14 +121,13 @@ func ImageHandler(meta *meta.Meta, c *gin.Context, resp *http.Response) (*model.
 	}
 
 	usage := &model.Usage{
-		InputTokens:        meta.RequestUsage.InputTokens,
-		TotalTokens:        meta.RequestUsage.InputTokens,
-		ImageOutputNumbers: meta.RequestUsage.ImageOutputNumbers,
+		InputTokens:  meta.RequestUsage.InputTokens,
+		OutputTokens: meta.RequestUsage.OutputTokens,
+		TotalTokens:  meta.RequestUsage.InputTokens + meta.RequestUsage.OutputTokens,
 	}
 
 	if imageResponse.Usage != nil {
 		usage = imageResponse.Usage.ToModelUsage()
-		usage.ImageOutputNumbers = meta.RequestUsage.ImageOutputNumbers
 	}
 
 	if meta.GetString(MetaResponseFormat) == "b64_json" {

+ 5 - 0
core/relay/controller/dohelper.go

@@ -140,6 +140,8 @@ func getRequestBody(meta *meta.Meta, c *gin.Context, detail *RequestDetail) *rel
 }
 
 func prepareAndDoRequest(a adaptor.Adaptor, c *gin.Context, meta *meta.Meta) (*http.Response, *relaymodel.ErrorWithStatusCode) {
+	log := middleware.GetLogger(c)
+
 	method, header, body, err := a.ConvertRequest(meta, c.Request)
 	if err != nil {
 		return nil, openai.ErrorWrapperWithMessage("convert request failed: "+err.Error(), "convert_request_failed", http.StatusBadRequest)
@@ -259,6 +261,9 @@ func updateUsageMetrics(usage model.Usage, log *log.Entry) {
 	if usage.InputTokens > 0 {
 		log.Data["t_input"] = usage.InputTokens
 	}
+	if usage.ImageInputTokens > 0 {
+		log.Data["t_image_input"] = usage.ImageInputTokens
+	}
 	if usage.OutputTokens > 0 {
 		log.Data["t_output"] = usage.OutputTokens
 	}

+ 54 - 0
core/relay/controller/edits.go

@@ -0,0 +1,54 @@
+package controller
+
+import (
+	"fmt"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+	"github.com/labring/aiproxy/core/model"
+	"github.com/labring/aiproxy/core/relay/adaptor/openai"
+)
+
+func GetImagesEditsRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+	size := c.PostForm("size")
+	quality := c.PostForm("quality")
+
+	imageCostPrice, ok := GetImagesOutputPrice(mc, size, quality)
+	if !ok {
+		return model.Price{}, fmt.Errorf("invalid image size `%s` or quality `%s`", size, quality)
+	}
+
+	return model.Price{
+		PerRequestPrice:     mc.Price.PerRequestPrice,
+		InputPrice:          mc.Price.InputPrice,
+		InputPriceUnit:      mc.Price.InputPriceUnit,
+		ImageInputPrice:     mc.Price.ImageInputPrice,
+		ImageInputPriceUnit: mc.Price.ImageInputPriceUnit,
+		OutputPrice:         imageCostPrice,
+		OutputPriceUnit:     mc.Price.OutputPriceUnit,
+	}, nil
+}
+
+func GetImagesEditsRequestUsage(c *gin.Context, mc *model.ModelConfig) (model.Usage, error) {
+	mutliForms, err := c.MultipartForm()
+	if err != nil {
+		return model.Usage{}, err
+	}
+	images := int64(len(mutliForms.File["image"]))
+
+	prompt := c.PostForm("prompt")
+	nStr := c.PostForm("n")
+	n := 1
+	if nStr != "" {
+		n, err = strconv.Atoi(nStr)
+		if err != nil {
+			return model.Usage{}, err
+		}
+	}
+
+	return model.Usage{
+		InputTokens:      openai.CountTokenInput(prompt, mc.Model),
+		ImageInputTokens: images,
+		OutputTokens:     int64(n),
+	}, nil
+}

+ 16 - 21
core/relay/controller/image.go

@@ -11,7 +11,7 @@ import (
 	"github.com/labring/aiproxy/core/relay/utils"
 )
 
-func getImageRequest(c *gin.Context) (*relaymodel.ImageRequest, error) {
+func getImagesRequest(c *gin.Context) (*relaymodel.ImageRequest, error) {
 	imageRequest, err := utils.UnmarshalImageRequest(c.Request)
 	if err != nil {
 		return nil, err
@@ -19,16 +19,13 @@ func getImageRequest(c *gin.Context) (*relaymodel.ImageRequest, error) {
 	if imageRequest.Prompt == "" {
 		return nil, errors.New("prompt is required")
 	}
-	if imageRequest.Size == "" {
-		return nil, errors.New("size is required")
-	}
 	if imageRequest.N == 0 {
 		imageRequest.N = 1
 	}
 	return imageRequest, nil
 }
 
-func GetImageOutputPrice(modelConfig *model.ModelConfig, size string, quality string) (float64, bool) {
+func GetImagesOutputPrice(modelConfig *model.ModelConfig, size string, quality string) (float64, bool) {
 	switch {
 	case len(modelConfig.ImagePrices) == 0 && len(modelConfig.ImageQualityPrices) == 0:
 		return modelConfig.Price.OutputPrice, true
@@ -43,38 +40,36 @@ func GetImageOutputPrice(modelConfig *model.ModelConfig, size string, quality st
 	}
 }
 
-func GetImageRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price, error) {
-	imageRequest, err := getImageRequest(c)
+func GetImagesRequestPrice(c *gin.Context, mc *model.ModelConfig) (model.Price, error) {
+	imageRequest, err := getImagesRequest(c)
 	if err != nil {
 		return model.Price{}, err
 	}
 
-	imageCostPrice, ok := GetImageOutputPrice(mc, imageRequest.Size, imageRequest.Quality)
+	imageCostPrice, ok := GetImagesOutputPrice(mc, imageRequest.Size, imageRequest.Quality)
 	if !ok {
 		return model.Price{}, fmt.Errorf("invalid image size `%s` or quality `%s`", imageRequest.Size, imageRequest.Quality)
 	}
 
 	return model.Price{
-		PerRequestPrice:      mc.Price.PerRequestPrice,
-		InputPrice:           mc.Price.InputPrice,
-		InputPriceUnit:       mc.Price.InputPriceUnit,
-		ImageInputPrice:      mc.Price.ImageInputPrice,
-		ImageInputPriceUnit:  mc.Price.ImageInputPriceUnit,
-		OutputPrice:          mc.Price.OutputPrice,
-		OutputPriceUnit:      mc.Price.OutputPriceUnit,
-		ImageOutputPrice:     imageCostPrice,
-		ImageOutputPriceUnit: mc.Price.ImageOutputPriceUnit,
+		PerRequestPrice:     mc.Price.PerRequestPrice,
+		InputPrice:          mc.Price.InputPrice,
+		InputPriceUnit:      mc.Price.InputPriceUnit,
+		ImageInputPrice:     mc.Price.ImageInputPrice,
+		ImageInputPriceUnit: mc.Price.ImageInputPriceUnit,
+		OutputPrice:         imageCostPrice,
+		OutputPriceUnit:     mc.Price.OutputPriceUnit,
 	}, nil
 }
 
-func GetImageRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
-	imageRequest, err := getImageRequest(c)
+func GetImagesRequestUsage(c *gin.Context, _ *model.ModelConfig) (model.Usage, error) {
+	imageRequest, err := getImagesRequest(c)
 	if err != nil {
 		return model.Usage{}, err
 	}
 
 	return model.Usage{
-		InputTokens:        openai.CountTokenInput(imageRequest.Prompt, imageRequest.Model),
-		ImageOutputNumbers: int64(imageRequest.N),
+		InputTokens:  openai.CountTokenInput(imageRequest.Prompt, imageRequest.Model),
+		OutputTokens: int64(imageRequest.N),
 	}, nil
 }

+ 3 - 3
core/relay/mode/define.go

@@ -18,8 +18,8 @@ func (m Mode) String() string {
 		return "Moderations"
 	case ImagesGenerations:
 		return "ImagesGenerations"
-	case Edits:
-		return "Edits"
+	case ImagesEdits:
+		return "ImagesEdits"
 	case AudioSpeech:
 		return "AudioSpeech"
 	case AudioTranscription:
@@ -44,7 +44,7 @@ const (
 	Embeddings
 	Moderations
 	ImagesGenerations
-	Edits
+	ImagesEdits
 	AudioSpeech
 	AudioTranscription
 	AudioTranslation

+ 1 - 1
core/relay/model/image.go

@@ -36,7 +36,7 @@ type ImageUsage struct {
 	// The number of tokens (images and text) in the input prompt.
 	InputTokens int64 `json:"input_tokens"`
 	// The number of image tokens in the output image.
-	OutputTokens int64 `jons:"output_tokens"`
+	OutputTokens int64 `json:"output_tokens"`
 	// The total number of tokens (images and text) used for the image generation.
 	TotalTokens int64 `json:"total_tokens"`
 	// The input tokens detailed information for the image generation.

+ 1 - 1
core/relay/utils/testreq.go

@@ -51,7 +51,7 @@ func BuildRequest(modelConfig *model.ModelConfig) (io.Reader, mode.Mode, error)
 			return nil, mode.Unknown, err
 		}
 		return body, mode.ImagesGenerations, nil
-	case mode.Edits:
+	case mode.ImagesEdits:
 		return nil, mode.Unknown, NewErrUnsupportedModelType("edits")
 	case mode.AudioSpeech:
 		body, err := BuildAudioSpeechRequest(modelConfig.Model)

+ 2 - 3
core/router/relay.go

@@ -36,8 +36,8 @@ func SetRelayRouter(router *gin.Engine) {
 			controller.Anthropic()...,
 		)
 		relayRouter.POST(
-			"/edits",
-			controller.Edits()...,
+			"/images/edits",
+			controller.ImagesEdits()...,
 		)
 		relayRouter.POST(
 			"/images/generations",
@@ -76,7 +76,6 @@ func SetRelayRouter(router *gin.Engine) {
 			controller.ParsePdf()...,
 		)
 
-		relayRouter.POST("/images/edits", controller.RelayNotImplemented)
 		relayRouter.POST("/images/variations", controller.RelayNotImplemented)
 		relayRouter.GET("/files", controller.RelayNotImplemented)
 		relayRouter.POST("/files", controller.RelayNotImplemented)