feat(api): add OpenAI compatibility for image models
- Introduced OpenAI-compatible image model support in the API, enabling integration through image generation and editing endpoints. - Added registry type for OpenAIImageModelType to classify and validate compatibility. - Implemented request handling for OpenAI-compatible image models, including JSON and multipart formats. - Enhanced executor methods to support OpenAI-compatible image streaming and non-streaming requests. - Included tests to validate model registration, streaming behavior, and multipart payload formatting.
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
internalconfig "github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/sdk/api/handlers"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -143,7 +145,20 @@ func isSupportedImagesModel(model string) bool {
|
||||
if baseModel == defaultImagesToolModel {
|
||||
return true
|
||||
}
|
||||
return isXAIImagesModel(model)
|
||||
return isXAIImagesModel(model) || isOpenAICompatImagesModel(model)
|
||||
}
|
||||
|
||||
func isDefaultImagesToolModel(model string) bool {
|
||||
return imagesModelBase(model) == defaultImagesToolModel
|
||||
}
|
||||
|
||||
func isOpenAICompatImagesModel(model string) bool {
|
||||
model = strings.TrimSpace(model)
|
||||
if model == "" {
|
||||
return false
|
||||
}
|
||||
info := registry.LookupModelInfo(model)
|
||||
return info != nil && info.Type == registry.OpenAIImageModelType
|
||||
}
|
||||
|
||||
func rejectUnsupportedImagesModel(c *gin.Context, model string) bool {
|
||||
@@ -153,7 +168,7 @@ func rejectUnsupportedImagesModel(c *gin.Context, model string) bool {
|
||||
|
||||
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: fmt.Sprintf("Model %s is not supported on %s or %s. Use %s, %s, or %s.", model, imagesGenerationsPath, imagesEditsPath, defaultImagesToolModel, defaultXAIImagesModel, xaiImagesQualityModel),
|
||||
Message: fmt.Sprintf("Model %s is not supported on %s or %s. Use %s, %s, %s, or a configured openai-compatibility image model.", model, imagesGenerationsPath, imagesEditsPath, defaultImagesToolModel, defaultXAIImagesModel, xaiImagesQualityModel),
|
||||
Type: "invalid_request_error",
|
||||
},
|
||||
})
|
||||
@@ -376,6 +391,90 @@ func multipartFileToDataURL(fileHeader *multipart.FileHeader) (string, error) {
|
||||
return "data:" + mediaType + ";base64," + b64, nil
|
||||
}
|
||||
|
||||
func buildOpenAICompatImagesJSONRequest(rawJSON []byte, imageModel string, stream bool) []byte {
|
||||
payload := rawJSON
|
||||
if model := strings.TrimSpace(imageModel); model != "" {
|
||||
payload, _ = sjson.SetBytes(payload, "model", model)
|
||||
}
|
||||
if stream {
|
||||
payload, _ = sjson.SetBytes(payload, "stream", true)
|
||||
} else {
|
||||
payload, _ = sjson.DeleteBytes(payload, "stream")
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func cloneMIMEHeader(src textproto.MIMEHeader) textproto.MIMEHeader {
|
||||
dst := make(textproto.MIMEHeader, len(src))
|
||||
for key, values := range src {
|
||||
dst[key] = append([]string(nil), values...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func buildOpenAICompatImagesMultipartRequest(form *multipart.Form, imageModel string, stream bool) ([]byte, string, error) {
|
||||
if form == nil {
|
||||
return nil, "", fmt.Errorf("multipart form is nil")
|
||||
}
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
if errWrite := writer.WriteField("model", imageModel); errWrite != nil {
|
||||
return nil, "", fmt.Errorf("write model field failed: %w", errWrite)
|
||||
}
|
||||
if stream {
|
||||
if errWrite := writer.WriteField("stream", "true"); errWrite != nil {
|
||||
return nil, "", fmt.Errorf("write stream field failed: %w", errWrite)
|
||||
}
|
||||
}
|
||||
for key, values := range form.Value {
|
||||
if key == "model" || key == "stream" {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
if errWrite := writer.WriteField(key, value); errWrite != nil {
|
||||
return nil, "", fmt.Errorf("write form field %s failed: %w", key, errWrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for key, files := range form.File {
|
||||
for _, fileHeader := range files {
|
||||
if fileHeader == nil {
|
||||
continue
|
||||
}
|
||||
header := cloneMIMEHeader(fileHeader.Header)
|
||||
header.Set("Content-Disposition", multipart.FileContentDisposition(key, fileHeader.Filename))
|
||||
if header.Get("Content-Type") == "" {
|
||||
header.Set("Content-Type", "application/octet-stream")
|
||||
}
|
||||
part, errCreate := writer.CreatePart(header)
|
||||
if errCreate != nil {
|
||||
return nil, "", fmt.Errorf("create file field %s failed: %w", key, errCreate)
|
||||
}
|
||||
src, errOpen := fileHeader.Open()
|
||||
if errOpen != nil {
|
||||
return nil, "", fmt.Errorf("open upload file failed: %w", errOpen)
|
||||
}
|
||||
_, errCopy := io.Copy(part, src)
|
||||
if errClose := src.Close(); errClose != nil {
|
||||
log.Errorf("openai images: close upload file error: %v", errClose)
|
||||
if errCopy == nil {
|
||||
errCopy = errClose
|
||||
}
|
||||
}
|
||||
if errCopy != nil {
|
||||
return nil, "", fmt.Errorf("copy upload file failed: %w", errCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errClose := writer.Close(); errClose != nil {
|
||||
return nil, "", fmt.Errorf("close multipart writer failed: %w", errClose)
|
||||
}
|
||||
return body.Bytes(), writer.FormDataContentType(), nil
|
||||
}
|
||||
|
||||
func parseIntField(raw string, fallback int64) int64 {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
@@ -454,11 +553,21 @@ func (h *OpenAIAPIHandler) ImagesGenerations(c *gin.Context) {
|
||||
}
|
||||
stream := gjson.GetBytes(rawJSON, "stream").Bool()
|
||||
|
||||
if isDefaultImagesToolModel(imageModel) {
|
||||
imageReq := buildOpenAICompatImagesJSONRequest(rawJSON, imageModel, stream)
|
||||
h.handleRoutedImages(c, imageReq, imageModel, stream)
|
||||
return
|
||||
}
|
||||
if isXAIImagesModel(imageModel) {
|
||||
xaiReq := buildXAIImagesGenerationsRequest(rawJSON, imageModel, responseFormat)
|
||||
h.handleXAIImages(c, xaiReq, responseFormat, "image_generation", stream)
|
||||
return
|
||||
}
|
||||
if isOpenAICompatImagesModel(imageModel) {
|
||||
compatReq := buildOpenAICompatImagesJSONRequest(rawJSON, imageModel, stream)
|
||||
h.handleOpenAICompatImages(c, compatReq, imageModel, responseFormat, "image_generation", stream)
|
||||
return
|
||||
}
|
||||
|
||||
tool := []byte(`{"type":"image_generation","action":"generate"}`)
|
||||
tool, _ = sjson.SetBytes(tool, "model", imageModel)
|
||||
@@ -589,6 +698,21 @@ func (h *OpenAIAPIHandler) imagesEditsFromMultipart(c *gin.Context) {
|
||||
}
|
||||
stream := parseBoolField(c.PostForm("stream"), false)
|
||||
|
||||
if isDefaultImagesToolModel(imageModel) {
|
||||
imageReq, contentType, errBuild := buildOpenAICompatImagesMultipartRequest(form, imageModel, stream)
|
||||
if errBuild != nil {
|
||||
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: fmt.Sprintf("Invalid request: %v", errBuild),
|
||||
Type: "invalid_request_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Request.Header.Set("Content-Type", contentType)
|
||||
h.handleRoutedImages(c, imageReq, imageModel, stream)
|
||||
return
|
||||
}
|
||||
if isXAIImagesModel(imageModel) {
|
||||
aspectRatio := xaiImagesAspectRatio(c.PostForm("aspect_ratio"), "")
|
||||
aspectRatio = xaiImagesAspectRatioFromSize(c.PostForm("size"), aspectRatio)
|
||||
@@ -598,6 +722,21 @@ func (h *OpenAIAPIHandler) imagesEditsFromMultipart(c *gin.Context) {
|
||||
h.handleXAIImages(c, xaiReq, responseFormat, "image_edit", stream)
|
||||
return
|
||||
}
|
||||
if isOpenAICompatImagesModel(imageModel) {
|
||||
compatReq, contentType, errBuild := buildOpenAICompatImagesMultipartRequest(form, imageModel, stream)
|
||||
if errBuild != nil {
|
||||
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: fmt.Sprintf("Invalid request: %v", errBuild),
|
||||
Type: "invalid_request_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Request.Header.Set("Content-Type", contentType)
|
||||
h.handleOpenAICompatImages(c, compatReq, imageModel, responseFormat, "image_edit", stream)
|
||||
return
|
||||
}
|
||||
|
||||
var maskDataURL *string
|
||||
if maskFiles := form.File["mask"]; len(maskFiles) > 0 && maskFiles[0] != nil {
|
||||
@@ -701,6 +840,11 @@ func (h *OpenAIAPIHandler) imagesEditsFromJSON(c *gin.Context) {
|
||||
}
|
||||
stream := gjson.GetBytes(rawJSON, "stream").Bool()
|
||||
|
||||
if isDefaultImagesToolModel(imageModel) {
|
||||
imageReq := buildOpenAICompatImagesJSONRequest(rawJSON, imageModel, stream)
|
||||
h.handleRoutedImages(c, imageReq, imageModel, stream)
|
||||
return
|
||||
}
|
||||
if isXAIImagesModel(imageModel) {
|
||||
images := collectXAIImagesFromJSON(rawJSON)
|
||||
if len(images) == 0 {
|
||||
@@ -717,6 +861,11 @@ func (h *OpenAIAPIHandler) imagesEditsFromJSON(c *gin.Context) {
|
||||
h.handleXAIImages(c, xaiReq, responseFormat, "image_edit", stream)
|
||||
return
|
||||
}
|
||||
if isOpenAICompatImagesModel(imageModel) {
|
||||
compatReq := buildOpenAICompatImagesJSONRequest(rawJSON, imageModel, stream)
|
||||
h.handleOpenAICompatImages(c, compatReq, imageModel, responseFormat, "image_edit", stream)
|
||||
return
|
||||
}
|
||||
|
||||
var images []string
|
||||
imagesResult := gjson.GetBytes(rawJSON, "images")
|
||||
@@ -904,14 +1053,247 @@ func (h *OpenAIAPIHandler) handleXAIImages(c *gin.Context, xaiReq []byte, respon
|
||||
h.collectXAIImages(c, xaiReq, responseFormat)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) handleOpenAICompatImages(c *gin.Context, compatReq []byte, imageModel string, responseFormat string, streamPrefix string, stream bool) {
|
||||
if stream {
|
||||
h.streamOpenAICompatImages(c, compatReq, imageModel)
|
||||
return
|
||||
}
|
||||
h.collectImagesWithModel(c, compatReq, imageModel, responseFormat)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) handleRoutedImages(c *gin.Context, imageReq []byte, imageModel string, stream bool) {
|
||||
if stream {
|
||||
h.streamRoutedImages(c, imageReq, imageModel)
|
||||
return
|
||||
}
|
||||
h.collectRoutedImages(c, imageReq, imageModel)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) collectRoutedImages(c *gin.Context, imageReq []byte, imageModel string) {
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
cliCtx = handlers.WithDisallowFreeAuth(cliCtx)
|
||||
stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
|
||||
|
||||
model := strings.TrimSpace(imageModel)
|
||||
resp, upstreamHeaders, errMsg := h.ExecuteImageWithAuthManager(cliCtx, xaiImagesHandlerType, model, imageReq, "")
|
||||
stopKeepAlive()
|
||||
if errMsg != nil {
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
if errMsg.Error != nil {
|
||||
cliCancel(errMsg.Error)
|
||||
} else {
|
||||
cliCancel(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||
_, _ = c.Writer.Write(resp)
|
||||
cliCancel(nil)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) streamRoutedImages(c *gin.Context, imageReq []byte, imageModel string) {
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: "Streaming not supported",
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
cliCtx = handlers.WithDisallowFreeAuth(cliCtx)
|
||||
model := strings.TrimSpace(imageModel)
|
||||
dataChan, upstreamHeaders, errChan := h.ExecuteImageStreamWithAuthManager(cliCtx, xaiImagesHandlerType, model, imageReq, "")
|
||||
|
||||
setSSEHeaders := func() {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
cliCancel(c.Request.Context().Err())
|
||||
return
|
||||
case errMsg, ok := <-errChan:
|
||||
if !ok {
|
||||
errChan = nil
|
||||
continue
|
||||
}
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
if errMsg != nil {
|
||||
cliCancel(errMsg.Error)
|
||||
} else {
|
||||
cliCancel(nil)
|
||||
}
|
||||
return
|
||||
case chunk, ok := <-dataChan:
|
||||
if !ok {
|
||||
setSSEHeaders()
|
||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||
_, _ = c.Writer.Write([]byte("\n"))
|
||||
flusher.Flush()
|
||||
cliCancel(nil)
|
||||
return
|
||||
}
|
||||
|
||||
setSSEHeaders()
|
||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||
_, _ = c.Writer.Write(chunk)
|
||||
flusher.Flush()
|
||||
h.forwardRawImageStream(cliCtx, c, func(err error) { cliCancel(err) }, dataChan, errChan)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) forwardRawImageStream(ctx context.Context, c *gin.Context, cancel func(error), data <-chan []byte, errs <-chan *interfaces.ErrorMessage) {
|
||||
emitError := func(errMsg *interfaces.ErrorMessage) {
|
||||
if errMsg == nil {
|
||||
return
|
||||
}
|
||||
status := http.StatusInternalServerError
|
||||
if errMsg.StatusCode > 0 {
|
||||
status = errMsg.StatusCode
|
||||
}
|
||||
errText := http.StatusText(status)
|
||||
if errMsg.Error != nil && strings.TrimSpace(errMsg.Error.Error()) != "" {
|
||||
errText = errMsg.Error.Error()
|
||||
}
|
||||
body := handlers.BuildErrorResponseBody(status, errText)
|
||||
_, _ = fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", string(body))
|
||||
if flusher, ok := c.Writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
cancel(c.Request.Context().Err())
|
||||
return
|
||||
case <-ctx.Done():
|
||||
cancel(ctx.Err())
|
||||
return
|
||||
case errMsg, ok := <-errs:
|
||||
if ok && errMsg != nil {
|
||||
emitError(errMsg)
|
||||
cancel(errMsg.Error)
|
||||
return
|
||||
}
|
||||
errs = nil
|
||||
case chunk, ok := <-data:
|
||||
if !ok {
|
||||
cancel(nil)
|
||||
return
|
||||
}
|
||||
_, _ = c.Writer.Write(chunk)
|
||||
if flusher, ok := c.Writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) streamOpenAICompatImages(c *gin.Context, compatReq []byte, imageModel string) {
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{
|
||||
Error: handlers.ErrorDetail{
|
||||
Message: "Streaming not supported",
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
model := strings.TrimSpace(imageModel)
|
||||
dataChan, upstreamHeaders, errChan := h.ExecuteStreamWithAuthManager(cliCtx, xaiImagesHandlerType, model, compatReq, "")
|
||||
|
||||
setSSEHeaders := func() {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
cliCancel(c.Request.Context().Err())
|
||||
return
|
||||
case errMsg, ok := <-errChan:
|
||||
if !ok {
|
||||
errChan = nil
|
||||
continue
|
||||
}
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
if errMsg != nil {
|
||||
cliCancel(errMsg.Error)
|
||||
} else {
|
||||
cliCancel(nil)
|
||||
}
|
||||
return
|
||||
case chunk, ok := <-dataChan:
|
||||
if !ok {
|
||||
setSSEHeaders()
|
||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||
flusher.Flush()
|
||||
cliCancel(nil)
|
||||
return
|
||||
}
|
||||
|
||||
setSSEHeaders()
|
||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||
_, _ = c.Writer.Write(chunk)
|
||||
flusher.Flush()
|
||||
h.ForwardStream(c, flusher, func(err error) { cliCancel(err) }, dataChan, errChan, handlers.StreamForwardOptions{
|
||||
WriteChunk: func(next []byte) {
|
||||
_, _ = c.Writer.Write(next)
|
||||
},
|
||||
WriteTerminalError: func(errMsg *interfaces.ErrorMessage) {
|
||||
if errMsg == nil {
|
||||
return
|
||||
}
|
||||
status := http.StatusInternalServerError
|
||||
if errMsg.StatusCode > 0 {
|
||||
status = errMsg.StatusCode
|
||||
}
|
||||
errText := http.StatusText(status)
|
||||
if errMsg.Error != nil && errMsg.Error.Error() != "" {
|
||||
errText = errMsg.Error.Error()
|
||||
}
|
||||
body := handlers.BuildErrorResponseBody(status, errText)
|
||||
_, _ = fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", string(body))
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) collectXAIImages(c *gin.Context, xaiReq []byte, responseFormat string) {
|
||||
model := strings.TrimSpace(gjson.GetBytes(xaiReq, "model").String())
|
||||
h.collectImagesWithModel(c, xaiReq, model, responseFormat)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) collectImagesWithModel(c *gin.Context, imageReq []byte, model string, responseFormat string) {
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
|
||||
|
||||
model := strings.TrimSpace(gjson.GetBytes(xaiReq, "model").String())
|
||||
resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiImagesHandlerType, model, xaiReq, "")
|
||||
model = strings.TrimSpace(model)
|
||||
resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiImagesHandlerType, model, imageReq, "")
|
||||
stopKeepAlive()
|
||||
if errMsg != nil {
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
@@ -937,6 +1319,11 @@ func (h *OpenAIAPIHandler) collectXAIImages(c *gin.Context, xaiReq []byte, respo
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) streamXAIImages(c *gin.Context, xaiReq []byte, responseFormat string, streamPrefix string) {
|
||||
model := strings.TrimSpace(gjson.GetBytes(xaiReq, "model").String())
|
||||
h.streamImagesWithModel(c, xaiReq, model, responseFormat, streamPrefix)
|
||||
}
|
||||
|
||||
func (h *OpenAIAPIHandler) streamImagesWithModel(c *gin.Context, imageReq []byte, model string, responseFormat string, streamPrefix string) {
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{
|
||||
@@ -949,8 +1336,8 @@ func (h *OpenAIAPIHandler) streamXAIImages(c *gin.Context, xaiReq []byte, respon
|
||||
}
|
||||
|
||||
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||
model := strings.TrimSpace(gjson.GetBytes(xaiReq, "model").String())
|
||||
resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiImagesHandlerType, model, xaiReq, "")
|
||||
model = strings.TrimSpace(model)
|
||||
resp, upstreamHeaders, errMsg := h.ExecuteWithAuthManager(cliCtx, xaiImagesHandlerType, model, imageReq, "")
|
||||
if errMsg != nil {
|
||||
h.WriteErrorResponse(c, errMsg)
|
||||
if errMsg.Error != nil {
|
||||
|
||||
Reference in New Issue
Block a user