From d0c45a01faecf8de5526ef8c2416099bd97aca43 Mon Sep 17 00:00:00 2001 From: Sh1n3zZ Date: Sat, 8 Nov 2025 01:24:45 +0800 Subject: [PATCH] feat: replicate channel flux model --- common/api_type.go | 2 + constant/api_type.go | 1 + constant/channel.go | 3 + relay/channel/replicate/adaptor.go | 530 +++++++++++++++++++++++++ relay/channel/replicate/constants.go | 12 + relay/channel/replicate/dto.go | 19 + relay/image_handler.go | 14 +- relay/relay_adaptor.go | 3 + setting/ratio_setting/model_ratio.go | 51 +-- web/src/constants/channel.constants.js | 5 + web/src/helpers/render.jsx | 3 + 11 files changed, 614 insertions(+), 29 deletions(-) create mode 100644 relay/channel/replicate/adaptor.go create mode 100644 relay/channel/replicate/constants.go create mode 100644 relay/channel/replicate/dto.go diff --git a/common/api_type.go b/common/api_type.go index 8dbf4a900..4f5c1826a 100644 --- a/common/api_type.go +++ b/common/api_type.go @@ -71,6 +71,8 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = constant.APITypeSubmodel case constant.ChannelTypeMiniMax: apiType = constant.APITypeMiniMax + case constant.ChannelTypeReplicate: + apiType = constant.APITypeReplicate } if apiType == -1 { return constant.APITypeOpenAI, false diff --git a/constant/api_type.go b/constant/api_type.go index 156ccc83c..32b48bcd2 100644 --- a/constant/api_type.go +++ b/constant/api_type.go @@ -34,5 +34,6 @@ const ( APITypeMoonshot APITypeSubmodel APITypeMiniMax + APITypeReplicate APITypeDummy // this one is only for count, do not add any channel after this ) diff --git a/constant/channel.go b/constant/channel.go index 426477e13..023faea3d 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -53,6 +53,7 @@ const ( ChannelTypeSubmodel = 53 ChannelTypeDoubaoVideo = 54 ChannelTypeSora = 55 + ChannelTypeReplicate = 56 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -114,6 +115,7 @@ var ChannelBaseURLs = []string{ "https://llm.submodel.ai", //53 "https://ark.cn-beijing.volces.com", //54 "https://api.openai.com", //55 + "https://api.replicate.com", //56 } var ChannelTypeNames = map[int]string{ @@ -169,6 +171,7 @@ var ChannelTypeNames = map[int]string{ ChannelTypeSubmodel: "Submodel", ChannelTypeDoubaoVideo: "DoubaoVideo", ChannelTypeSora: "Sora", + ChannelTypeReplicate: "Replicate", } func GetChannelTypeName(channelType int) string { diff --git a/relay/channel/replicate/adaptor.go b/relay/channel/replicate/adaptor.go new file mode 100644 index 000000000..9ee521615 --- /dev/null +++ b/relay/channel/replicate/adaptor.go @@ -0,0 +1,530 @@ +package replicate + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "strconv" + "strings" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/relay/channel" + relaycommon "github.com/QuantumNous/new-api/relay/common" + relayconstant "github.com/QuantumNous/new-api/relay/constant" + "github.com/QuantumNous/new-api/service" + "github.com/QuantumNous/new-api/types" + + "github.com/gin-gonic/gin" +) + +type Adaptor struct { +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + if info == nil { + return "", errors.New("replicate adaptor: relay info is nil") + } + if info.ChannelBaseUrl == "" { + info.ChannelBaseUrl = constant.ChannelBaseURLs[constant.ChannelTypeReplicate] + } + requestPath := info.RequestURLPath + if requestPath == "" { + return info.ChannelBaseUrl, nil + } + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestPath, info.ChannelType), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + if info == nil { + return errors.New("replicate adaptor: relay info is nil") + } + if info.ApiKey == "" { + return errors.New("replicate adaptor: api key is required") + } + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + req.Set("Prefer", "wait") + if req.Get("Content-Type") == "" { + req.Set("Content-Type", "application/json") + } + if req.Get("Accept") == "" { + req.Set("Accept", "application/json") + } + return nil +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + if info == nil { + return nil, errors.New("replicate adaptor: relay info is nil") + } + if strings.TrimSpace(request.Prompt) == "" { + if v := c.PostForm("prompt"); strings.TrimSpace(v) != "" { + request.Prompt = v + } + } + if strings.TrimSpace(request.Prompt) == "" { + return nil, errors.New("replicate adaptor: prompt is required") + } + + modelName := strings.TrimSpace(info.UpstreamModelName) + if modelName == "" { + modelName = strings.TrimSpace(request.Model) + } + if modelName == "" { + modelName = ModelFlux11Pro + } + info.UpstreamModelName = modelName + + info.RequestURLPath = fmt.Sprintf("/v1/models/%s/predictions", modelName) + + inputPayload := make(map[string]any) + inputPayload["prompt"] = request.Prompt + + if size := strings.TrimSpace(request.Size); size != "" { + if aspect, width, height, ok := mapOpenAISizeToFlux(size); ok { + if aspect != "" { + if aspect == "custom" { + inputPayload["aspect_ratio"] = "custom" + if width > 0 { + inputPayload["width"] = width + } + if height > 0 { + inputPayload["height"] = height + } + } else { + inputPayload["aspect_ratio"] = aspect + } + } + } + } + + if len(request.OutputFormat) > 0 { + var outputFormat string + if err := json.Unmarshal(request.OutputFormat, &outputFormat); err == nil && strings.TrimSpace(outputFormat) != "" { + inputPayload["output_format"] = outputFormat + } + } + + if request.N > 0 { + inputPayload["num_outputs"] = int(request.N) + } + + if strings.EqualFold(request.Quality, "hd") || strings.EqualFold(request.Quality, "high") { + inputPayload["prompt_upsampling"] = true + } + + if info.RelayMode == relayconstant.RelayModeImagesEdits { + imageURL, err := uploadFileFromForm(c, info, "image", "image[]", "image_prompt") + if err != nil { + return nil, err + } + if imageURL == "" { + return nil, errors.New("replicate adaptor: image file is required for edits") + } + inputPayload["image_prompt"] = imageURL + } + + if len(request.ExtraFields) > 0 { + var extra map[string]any + if err := common.Unmarshal(request.ExtraFields, &extra); err != nil { + return nil, fmt.Errorf("replicate adaptor: failed to decode extra_fields: %w", err) + } + for key, val := range extra { + inputPayload[key] = val + } + } + + for key, raw := range request.Extra { + if strings.EqualFold(key, "input") { + var extraInput map[string]any + if err := common.Unmarshal(raw, &extraInput); err != nil { + return nil, fmt.Errorf("replicate adaptor: failed to decode extra input: %w", err) + } + for k, v := range extraInput { + inputPayload[k] = v + } + continue + } + if raw == nil { + continue + } + var val any + if err := common.Unmarshal(raw, &val); err != nil { + return nil, fmt.Errorf("replicate adaptor: failed to decode extra field %s: %w", key, err) + } + inputPayload[key] = val + } + + return map[string]any{ + "input": inputPayload, + }, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (any, *types.NewAPIError) { + if resp == nil { + return nil, types.NewError(errors.New("replicate adaptor: empty response"), types.ErrorCodeBadResponse) + } + + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, types.NewError(err, types.ErrorCodeReadResponseBodyFailed) + } + _ = resp.Body.Close() + + var prediction PredictionResponse + if err := common.Unmarshal(responseBody, &prediction); err != nil { + return nil, types.NewError(fmt.Errorf("replicate adaptor: failed to decode response: %w", err), types.ErrorCodeBadResponseBody) + } + + if prediction.Error != nil { + errMsg := prediction.Error.Message + if errMsg == "" { + errMsg = prediction.Error.Detail + } + if errMsg == "" { + errMsg = prediction.Error.Code + } + if errMsg == "" { + errMsg = "replicate adaptor: prediction error" + } + return nil, types.NewError(errors.New(errMsg), types.ErrorCodeBadResponse) + } + + if prediction.Status != "" && !strings.EqualFold(prediction.Status, "succeeded") { + return nil, types.NewError(fmt.Errorf("replicate adaptor: prediction status %q", prediction.Status), types.ErrorCodeBadResponse) + } + + var urls []string + + appendOutput := func(value string) { + value = strings.TrimSpace(value) + if value == "" { + return + } + urls = append(urls, value) + } + + switch output := prediction.Output.(type) { + case string: + appendOutput(output) + case []any: + for _, item := range output { + if str, ok := item.(string); ok { + appendOutput(str) + } + } + case nil: + // no output + default: + if str, ok := output.(fmt.Stringer); ok { + appendOutput(str.String()) + } + } + + if len(urls) == 0 { + return nil, types.NewError(errors.New("replicate adaptor: empty prediction output"), types.ErrorCodeBadResponseBody) + } + + var imageReq *dto.ImageRequest + if info != nil { + if req, ok := info.Request.(*dto.ImageRequest); ok { + imageReq = req + } + } + + wantsBase64 := imageReq != nil && strings.EqualFold(imageReq.ResponseFormat, "b64_json") + + imageResponse := dto.ImageResponse{ + Created: common.GetTimestamp(), + Data: make([]dto.ImageData, 0), + } + + if wantsBase64 { + converted, convErr := downloadImagesToBase64(urls) + if convErr != nil { + return nil, types.NewError(convErr, types.ErrorCodeBadResponse) + } + for _, content := range converted { + if content == "" { + continue + } + imageResponse.Data = append(imageResponse.Data, dto.ImageData{B64Json: content}) + } + } else { + for _, url := range urls { + if url == "" { + continue + } + imageResponse.Data = append(imageResponse.Data, dto.ImageData{Url: url}) + } + } + + if len(imageResponse.Data) == 0 { + return nil, types.NewError(errors.New("replicate adaptor: no usable image data"), types.ErrorCodeBadResponse) + } + + responseBytes, err := common.Marshal(imageResponse) + if err != nil { + return nil, types.NewError(fmt.Errorf("replicate adaptor: encode response failed: %w", err), types.ErrorCodeBadResponseBody) + } + + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(http.StatusOK) + _, _ = c.Writer.Write(responseBytes) + + usage := &dto.Usage{} + return usage, nil +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} + +func downloadImagesToBase64(urls []string) ([]string, error) { + results := make([]string, 0, len(urls)) + for _, url := range urls { + if strings.TrimSpace(url) == "" { + continue + } + _, data, err := service.GetImageFromUrl(url) + if err != nil { + return nil, fmt.Errorf("replicate adaptor: failed to download image from %s: %w", url, err) + } + results = append(results, data) + } + return results, nil +} + +func mapOpenAISizeToFlux(size string) (aspect string, width int, height int, ok bool) { + parts := strings.Split(size, "x") + if len(parts) != 2 { + return "", 0, 0, false + } + w, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) + h, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) + if err1 != nil || err2 != nil || w <= 0 || h <= 0 { + return "", 0, 0, false + } + + switch { + case w == h: + return "1:1", 0, 0, true + case w == 1792 && h == 1024: + return "16:9", 0, 0, true + case w == 1024 && h == 1792: + return "9:16", 0, 0, true + case w == 1536 && h == 1024: + return "3:2", 0, 0, true + case w == 1024 && h == 1536: + return "2:3", 0, 0, true + } + + rw, rh := reduceRatio(w, h) + ratioStr := fmt.Sprintf("%d:%d", rw, rh) + switch ratioStr { + case "1:1", "16:9", "9:16", "3:2", "2:3", "4:5", "5:4", "3:4", "4:3": + return ratioStr, 0, 0, true + } + + width = normalizeFluxDimension(w) + height = normalizeFluxDimension(h) + return "custom", width, height, true +} + +func reduceRatio(w, h int) (int, int) { + g := gcd(w, h) + if g == 0 { + return w, h + } + return w / g, h / g +} + +func gcd(a, b int) int { + for b != 0 { + a, b = b, a%b + } + if a < 0 { + return -a + } + return a +} + +func normalizeFluxDimension(value int) int { + const ( + minDim = 256 + maxDim = 1440 + step = 32 + ) + if value < minDim { + value = minDim + } + if value > maxDim { + value = maxDim + } + remainder := value % step + if remainder != 0 { + if remainder >= step/2 { + value += step - remainder + } else { + value -= remainder + } + } + if value < minDim { + value = minDim + } + if value > maxDim { + value = maxDim + } + return value +} + +func uploadFileFromForm(c *gin.Context, info *relaycommon.RelayInfo, fieldCandidates ...string) (string, error) { + if info == nil { + return "", errors.New("replicate adaptor: relay info is nil") + } + + mf := c.Request.MultipartForm + if mf == nil { + if _, err := c.MultipartForm(); err != nil { + return "", fmt.Errorf("replicate adaptor: parse multipart form failed: %w", err) + } + mf = c.Request.MultipartForm + } + if mf == nil || len(mf.File) == 0 { + return "", nil + } + + if len(fieldCandidates) == 0 { + fieldCandidates = []string{"image", "image[]", "image_prompt"} + } + + var fileHeader *multipart.FileHeader + for _, key := range fieldCandidates { + if files := mf.File[key]; len(files) > 0 { + fileHeader = files[0] + break + } + } + if fileHeader == nil { + for _, files := range mf.File { + if len(files) > 0 { + fileHeader = files[0] + break + } + } + } + if fileHeader == nil { + return "", nil + } + + file, err := fileHeader.Open() + if err != nil { + return "", fmt.Errorf("replicate adaptor: failed to open image file: %w", err) + } + defer file.Close() + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + hdr := make(textproto.MIMEHeader) + hdr.Set("Content-Disposition", fmt.Sprintf("form-data; name=\"content\"; filename=\"%s\"", fileHeader.Filename)) + contentType := fileHeader.Header.Get("Content-Type") + if contentType == "" { + contentType = "application/octet-stream" + } + hdr.Set("Content-Type", contentType) + + part, err := writer.CreatePart(hdr) + if err != nil { + writer.Close() + return "", fmt.Errorf("replicate adaptor: create upload form failed: %w", err) + } + if _, err := io.Copy(part, file); err != nil { + writer.Close() + return "", fmt.Errorf("replicate adaptor: copy image content failed: %w", err) + } + formContentType := writer.FormDataContentType() + writer.Close() + + baseURL := info.ChannelBaseUrl + if baseURL == "" { + baseURL = constant.ChannelBaseURLs[constant.ChannelTypeReplicate] + } + uploadURL := relaycommon.GetFullRequestURL(baseURL, "/v1/files", info.ChannelType) + + req, err := http.NewRequest(http.MethodPost, uploadURL, &body) + if err != nil { + return "", fmt.Errorf("replicate adaptor: create upload request failed: %w", err) + } + req.Header.Set("Content-Type", formContentType) + req.Header.Set("Authorization", "Bearer "+info.ApiKey) + + resp, err := service.GetHttpClient().Do(req) + if err != nil { + return "", fmt.Errorf("replicate adaptor: upload image failed: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("replicate adaptor: read upload response failed: %w", err) + } + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return "", fmt.Errorf("replicate adaptor: upload image failed with status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + + var uploadResp FileUploadResponse + if err := common.Unmarshal(respBody, &uploadResp); err != nil { + return "", fmt.Errorf("replicate adaptor: decode upload response failed: %w", err) + } + if uploadResp.Urls.Get == "" { + return "", errors.New("replicate adaptor: upload response missing url") + } + return uploadResp.Urls.Get, nil +} + +func (a *Adaptor) ConvertOpenAIRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeneralOpenAIRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertOpenAIRequest is not implemented") +} + +func (a *Adaptor) ConvertRerankRequest(*gin.Context, int, dto.RerankRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertRerankRequest is not implemented") +} + +func (a *Adaptor) ConvertEmbeddingRequest(*gin.Context, *relaycommon.RelayInfo, dto.EmbeddingRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertEmbeddingRequest is not implemented") +} + +func (a *Adaptor) ConvertAudioRequest(*gin.Context, *relaycommon.RelayInfo, dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("replicate adaptor: ConvertAudioRequest is not implemented") +} + +func (a *Adaptor) ConvertOpenAIResponsesRequest(*gin.Context, *relaycommon.RelayInfo, dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertOpenAIResponsesRequest is not implemented") +} + +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertClaudeRequest is not implemented") +} + +func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("replicate adaptor: ConvertGeminiRequest is not implemented") +} diff --git a/relay/channel/replicate/constants.go b/relay/channel/replicate/constants.go new file mode 100644 index 000000000..b047bcfad --- /dev/null +++ b/relay/channel/replicate/constants.go @@ -0,0 +1,12 @@ +package replicate + +const ( + // ChannelName identifies the replicate channel. + ChannelName = "replicate" + // ModelFlux11Pro is the default image generation model supported by this channel. + ModelFlux11Pro = "black-forest-labs/flux-1.1-pro" +) + +var ModelList = []string{ + ModelFlux11Pro, +} diff --git a/relay/channel/replicate/dto.go b/relay/channel/replicate/dto.go new file mode 100644 index 000000000..2ff06dcab --- /dev/null +++ b/relay/channel/replicate/dto.go @@ -0,0 +1,19 @@ +package replicate + +type PredictionResponse struct { + Status string `json:"status"` + Output any `json:"output"` + Error *PredictionError `json:"error"` +} + +type PredictionError struct { + Code string `json:"code"` + Message string `json:"message"` + Detail string `json:"detail"` +} + +type FileUploadResponse struct { + Urls struct { + Get string `json:"get"` + } `json:"urls"` +} diff --git a/relay/image_handler.go b/relay/image_handler.go index afb9446a1..101447c13 100644 --- a/relay/image_handler.go +++ b/relay/image_handler.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" "github.com/QuantumNous/new-api/logger" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -92,10 +93,15 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type httpResp = resp.(*http.Response) info.IsStream = info.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream") if httpResp.StatusCode != http.StatusOK { - newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false) - // reset status code 重置状态码 - service.ResetStatusCode(newAPIError, statusCodeMappingStr) - return newAPIError + if httpResp.StatusCode == http.StatusCreated && info.ApiType == constant.APITypeReplicate { + // replicate channel returns 201 Created when using Prefer: wait, treat it as success. + httpResp.StatusCode = http.StatusOK + } else { + newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false) + // reset status code 重置状态码 + service.ResetStatusCode(newAPIError, statusCodeMappingStr) + return newAPIError + } } } diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 85a0b6396..55afea179 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -26,6 +26,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel/openai" "github.com/QuantumNous/new-api/relay/channel/palm" "github.com/QuantumNous/new-api/relay/channel/perplexity" + "github.com/QuantumNous/new-api/relay/channel/replicate" "github.com/QuantumNous/new-api/relay/channel/siliconflow" "github.com/QuantumNous/new-api/relay/channel/submodel" taskali "github.com/QuantumNous/new-api/relay/channel/task/ali" @@ -113,6 +114,8 @@ func GetAdaptor(apiType int) channel.Adaptor { return &submodel.Adaptor{} case constant.APITypeMiniMax: return &minimax.Adaptor{} + case constant.APITypeReplicate: + return &replicate.Adaptor{} } return nil } diff --git a/setting/ratio_setting/model_ratio.go b/setting/ratio_setting/model_ratio.go index 0ff4035bd..416822393 100644 --- a/setting/ratio_setting/model_ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -268,31 +268,32 @@ var defaultModelRatio = map[string]float64{ } var defaultModelPrice = map[string]float64{ - "suno_music": 0.1, - "suno_lyrics": 0.01, - "dall-e-3": 0.04, - "imagen-3.0-generate-002": 0.03, - "gpt-4-gizmo-*": 0.1, - "mj_video": 0.8, - "mj_imagine": 0.1, - "mj_edits": 0.1, - "mj_variation": 0.1, - "mj_reroll": 0.1, - "mj_blend": 0.1, - "mj_modal": 0.1, - "mj_zoom": 0.1, - "mj_shorten": 0.1, - "mj_high_variation": 0.1, - "mj_low_variation": 0.1, - "mj_pan": 0.1, - "mj_inpaint": 0, - "mj_custom_zoom": 0, - "mj_describe": 0.05, - "mj_upscale": 0.05, - "swap_face": 0.05, - "mj_upload": 0.05, - "sora-2": 0.3, - "sora-2-pro": 0.5, + "suno_music": 0.1, + "suno_lyrics": 0.01, + "dall-e-3": 0.04, + "imagen-3.0-generate-002": 0.03, + "black-forest-labs/flux-1.1-pro": 0.04, + "gpt-4-gizmo-*": 0.1, + "mj_video": 0.8, + "mj_imagine": 0.1, + "mj_edits": 0.1, + "mj_variation": 0.1, + "mj_reroll": 0.1, + "mj_blend": 0.1, + "mj_modal": 0.1, + "mj_zoom": 0.1, + "mj_shorten": 0.1, + "mj_high_variation": 0.1, + "mj_low_variation": 0.1, + "mj_pan": 0.1, + "mj_inpaint": 0, + "mj_custom_zoom": 0, + "mj_describe": 0.05, + "mj_upscale": 0.05, + "swap_face": 0.05, + "mj_upload": 0.05, + "sora-2": 0.3, + "sora-2-pro": 0.5, } var defaultAudioRatio = map[string]float64{ diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index e79e19f6a..0d487958e 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -179,6 +179,11 @@ export const CHANNEL_OPTIONS = [ color: 'green', label: 'Sora', }, + { + value: 56, + color: 'blue', + label: 'Replicate', + }, ]; export const MODEL_TABLE_PAGE_SIZE = 10; diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 241830ab6..e0b362740 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -55,6 +55,7 @@ import { Kling, Jimeng, Perplexity, + Replicate, } from '@lobehub/icons'; import { @@ -342,6 +343,8 @@ export function getChannelIcon(channelType) { return ; case 54: // 豆包视频 Doubao Video return ; + case 56: // Replicate + return ; case 8: // 自定义渠道 case 22: // 知识库:FastGPT return ;