Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
apiType = constant.APITypeSub2API
case constant.ChannelTypeNewAPI:
apiType = constant.APITypeNewAPI
case constant.ChannelTypeMinerU:
apiType = constant.APITypeMinerU
}
if apiType == -1 {
// Task plugin channels are served by the task relay and must never
Expand Down
1 change: 1 addition & 0 deletions common/endpoint_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
constant.EndpointTypeImageGeneration: {Path: "/v1/images/generations", Method: "POST"},
constant.EndpointTypeEmbeddings: {Path: "/v1/embeddings", Method: "POST"},
constant.EndpointTypeMinerU: {Path: "/v1/file_parse", Method: "POST"},
}

// GetDefaultEndpointInfo 返回指定端点类型的默认信息以及是否存在
Expand Down
1 change: 1 addition & 0 deletions constant/api_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ const (
APITypeAdvancedCustom
APITypeSub2API
APITypeNewAPI
APITypeMinerU
APITypeDummy // this one is only for count, do not add any channel after this
)
3 changes: 3 additions & 0 deletions constant/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
ChannelTypeTaskPlugin = 61
ChannelTypeVLLM = 62
ChannelTypeSGLang = 63
ChannelTypeMinerU = 64
ChannelTypeDummy // this one is only for count, do not add any channel after this

)
Expand Down Expand Up @@ -132,6 +133,7 @@ var ChannelBaseURLs = []string{
"", //61
"", //62
"", //63
"", //64
}

func GetChannelBaseURL(channelType int) string {
Expand Down Expand Up @@ -202,6 +204,7 @@ var ChannelTypeNames = map[int]string{
ChannelTypeTaskPlugin: "Task Plugin",
ChannelTypeVLLM: "vLLM",
ChannelTypeSGLang: "SGLang",
ChannelTypeMinerU: "MinerU",
}

func GetChannelTypeName(channelType int) string {
Expand Down
1 change: 1 addition & 0 deletions constant/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ const (
EndpointTypeImageGeneration = types.EndpointTypeImageGeneration
EndpointTypeEmbeddings = types.EndpointTypeEmbeddings
EndpointTypeOpenAIVideo = types.EndpointTypeOpenAIVideo
EndpointTypeMinerU = types.EndpointTypeMinerU
)
2 changes: 2 additions & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ func relayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewAPIErro
err = relay.ResponsesHelper(c, info)
case relayconstant.RelayModeAlphaSearch:
err = relay.AlphaSearchHelper(c, info)
case relayconstant.RelayModeMinerU:
err = relay.MinerUHelper(c, info)
default:
err = relay.TextHelper(c, info)
}
Expand Down
7 changes: 7 additions & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,13 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
modelRequest.Model = modelName
}
c.Set("relay_mode", relayMode)
} else if strings.HasPrefix(c.Request.URL.Path, "/v1/file_parse") {
// MinerU document parsing: multipart form, model optional (defaults to mineru)
if req, err := getModelFromRequest(c); err == nil && req.Model != "" {
modelRequest.Model = req.Model
}
modelRequest.Model = common.GetStringIfEmpty(modelRequest.Model, "mineru")
c.Set("relay_mode", relayconstant.RelayModeMinerU)
} else if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") && !strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
req, err := getModelFromRequest(c)
if err != nil {
Expand Down
96 changes: 96 additions & 0 deletions relay/channel/mineru/adaptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package mineru

import (
"errors"
"fmt"
"io"
"net/http"
"strings"

"github.com/QuantumNous/new-api/relay/channel"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"

"github.com/gin-gonic/gin"
)

const ChannelName = "mineru"

var ModelList = []string{"mineru"}

type Adaptor struct {
}

func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
}

// GetRequestURL 返回渠道 base_url + /file_parse。
// 本地 MinerU: base_url = http://mineru-api:8000
// 上游 New-API: base_url = https://api.playground.ai.gcable.cc/v1
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
baseUrl := strings.TrimRight(info.ChannelBaseUrl, "/")
if baseUrl == "" {
return "", errors.New("mineru channel base_url is empty")
}
return fmt.Sprintf("%s/file_parse", baseUrl), nil
}

func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
// multipart 透传:Content-Type(含 boundary)由 DoFormRequest 按入站请求设置
if info.ApiKey != "" {
req.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return nil
}

func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
return channel.DoFormRequest(a, c, info, requestBody)
}

func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
// 响应体已在 MinerUHelper 中原样透传,此处仅返回零 usage(按次计费)
return &dto.Usage{}, nil
}

func (a *Adaptor) GetModelList() []string {
return ModelList
}

func (a *Adaptor) GetChannelName() string {
return ChannelName
}

// 以下为 channel.Adaptor 接口的占位实现(MinerU 转发不涉及)

func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
return nil, errors.New("not implemented")
}

func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) {
return nil, errors.New("not implemented")
}
8 changes: 8 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ func GenRelayInfoOpenAIAudio(c *gin.Context, request dto.Request) *RelayInfo {
return info
}

func GenRelayInfoMinerU(c *gin.Context, request dto.Request) *RelayInfo {
info := genBaseRelayInfo(c, request)
info.RelayFormat = types.RelayFormatMinerU
return info
}

func GenRelayInfoEmbedding(c *gin.Context, request dto.Request) *RelayInfo {
info := genBaseRelayInfo(c, request)
info.RelayFormat = types.RelayFormatEmbedding
Expand Down Expand Up @@ -674,6 +680,8 @@ func GenRelayInfo(c *gin.Context, relayFormat types.RelayFormat, request dto.Req
info = GenRelayInfoOpenAI(c, request)
case types.RelayFormatOpenAIAudio:
info = GenRelayInfoOpenAIAudio(c, request)
case types.RelayFormatMinerU:
info = GenRelayInfoMinerU(c, request)
case types.RelayFormatOpenAIImage:
info = GenRelayInfoImage(c, request)
case types.RelayFormatOpenAIRealtime:
Expand Down
4 changes: 4 additions & 0 deletions relay/constant/relay_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const (
RelayModeResponsesCompact

RelayModeAlphaSearch

RelayModeMinerU
)

func Path2RelayMode(path string) int {
Expand Down Expand Up @@ -87,6 +89,8 @@ func Path2RelayMode(path string) int {
relayMode = RelayModeRealtime
} else if strings.HasPrefix(path, "/v1beta/models") || strings.HasPrefix(path, "/v1/models") {
relayMode = RelayModeGemini
} else if strings.HasPrefix(path, "/v1/file_parse") {
relayMode = RelayModeMinerU
} else if strings.HasPrefix(path, "/mj") {
relayMode = Path2RelayModeMidjourney(path)
}
Expand Down
13 changes: 13 additions & 0 deletions relay/helper/valid_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func GetAndValidateRequest(c *gin.Context, format types.RelayFormat) (request dt
request, err = GetAndValidateRerankRequest(c)
case types.RelayFormatOpenAIAudio:
request, err = GetAndValidAudioRequest(c, relayMode)
case types.RelayFormatMinerU:
request, err = GetAndValidateMinerURequest(c)
case types.RelayFormatOpenAIRealtime:
request = &dto.BaseRequest{}
default:
Expand Down Expand Up @@ -80,6 +82,17 @@ func GetAndValidAudioRequest(c *gin.Context, relayMode int) (*dto.AudioRequest,
return audioRequest, nil
}

func GetAndValidateMinerURequest(c *gin.Context) (*dto.MinerURequest, error) {
request := &dto.MinerURequest{}
if err := common.UnmarshalBodyReusable(c, request); err != nil {
return nil, err
}
if request.Model == "" {
request.Model = "mineru"
}
return request, nil
}

func GetAndValidateRerankRequest(c *gin.Context) (*dto.RerankRequest, error) {
var rerankRequest *dto.RerankRequest
err := common.UnmarshalBodyReusable(c, &rerankRequest)
Expand Down
66 changes: 66 additions & 0 deletions relay/mineru_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package relay

import (
"errors"
"fmt"
"io"
"net/http"

relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/QuantumNous/new-api/service"

"github.com/gin-gonic/gin"
)

// MinerUHelper 将 /v1/file_parse(multipart/form-data)请求转发到
// 渠道 base_url + /file_parse,并将上游响应原样透传给客户端。
// 渠道约定:
// - 本地 MinerU: base_url = http://mineru-api:8000
// - 上游 New-API: base_url = https://api.playground.ai.gcable.cc/v1
func MinerUHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
info.InitChannelMeta(c)

if _, ok := info.Request.(*dto.MinerURequest); !ok {
return types.NewError(errors.New("invalid request type"), types.ErrorCodeInvalidRequest, types.ErrOptionWithSkipRetry())
}

adaptor := GetAdaptor(info.ApiType)
if adaptor == nil {
return types.NewError(fmt.Errorf("invalid api type: %d", info.ApiType), types.ErrorCodeInvalidApiType, types.ErrOptionWithSkipRetry())
}
adaptor.Init(info)

// multipart 请求体已由 controller.Relay 置为可重放的 BodyStorage,直接透传
resp, err := adaptor.DoRequest(c, info, c.Request.Body)
if err != nil {
return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError)
}
httpResp, ok := resp.(*http.Response)
if !ok || httpResp == nil {
return types.NewError(errors.New("invalid upstream response"), types.ErrorCodeDoRequestFailed, types.ErrOptionWithSkipRetry())
}
defer httpResp.Body.Close()

statusCodeMappingStr := c.GetString("status_code_mapping")
if httpResp.StatusCode != http.StatusOK {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
newAPIError = service.RelayErrorHandler(c.Request.Context(), httpResp, false)
service.ResetStatusCode(newAPIError, statusCodeMappingStr)
return newAPIError
}

// 原样回传响应(JSON / zip 均透传)
if contentType := httpResp.Header.Get("Content-Type"); contentType != "" {
c.Writer.Header().Set("Content-Type", contentType)
}
if cd := httpResp.Header.Get("Content-Disposition"); cd != "" {
c.Writer.Header().Set("Content-Disposition", cd)
}
c.Status(http.StatusOK)
_, _ = io.Copy(c.Writer, httpResp.Body)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// 按次计费(mineru 为 quota_type=1 按次价格,usage 置零)
service.PostTextConsumeQuota(c, info, &dto.Usage{}, nil)
return nil
}
3 changes: 3 additions & 0 deletions relay/relay_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/QuantumNous/new-api/relay/channel/jimeng"
"github.com/QuantumNous/new-api/relay/channel/jina"
"github.com/QuantumNous/new-api/relay/channel/minimax"
"github.com/QuantumNous/new-api/relay/channel/mineru"
"github.com/QuantumNous/new-api/relay/channel/mistral"
"github.com/QuantumNous/new-api/relay/channel/mokaai"
"github.com/QuantumNous/new-api/relay/channel/moonshot"
Expand Down Expand Up @@ -123,6 +124,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
return &sub2api.Adaptor{}
case constant.APITypeNewAPI:
return &newapi.Adaptor{}
case constant.APITypeMinerU:
return &mineru.Adaptor{}
}
return nil
}
Expand Down
15 changes: 15 additions & 0 deletions relaykit/dto/mineru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dto

// MinerURequest 表示 /v1/file_parse 的 multipart 表单请求。
// 表单内容(files 及解析参数)原样透传给上游 MinerU 服务,
// 这里只提取渠道选择所需的元信息(model,可省略,默认 mineru)。
type MinerURequest struct {
BaseRequest
Model string `json:"model" form:"model"`
}

func (r *MinerURequest) SetModelName(modelName string) {
if modelName != "" {
r.Model = modelName
}
}
2 changes: 2 additions & 0 deletions relaykit/relayconvert/convmeta/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func GuessRelayFormatFromRequest(req any) (types.RelayFormat, bool) {
return types.RelayFormatOpenAIImage, true
case *dto.AudioRequest, dto.AudioRequest:
return types.RelayFormatOpenAIAudio, true
case *dto.MinerURequest, dto.MinerURequest:
return types.RelayFormatMinerU, true
default:
return "", false
}
Expand Down
1 change: 1 addition & 0 deletions relaykit/types/endpoint_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
EndpointTypeImageGeneration EndpointType = "image-generation"
EndpointTypeEmbeddings EndpointType = "embeddings"
EndpointTypeOpenAIVideo EndpointType = "openai-video"
EndpointTypeMinerU = "MinerU"
)

// Finish reasons shared by the OpenAI-compatible response formats.
Expand Down
1 change: 1 addition & 0 deletions relaykit/types/relay_format.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
RelayFormatRerank = "rerank"
RelayFormatEmbedding = "embedding"

RelayFormatMinerU = "mineru"
RelayFormatTask = "task"
RelayFormatMjProxy = "mj_proxy"
)
5 changes: 5 additions & 0 deletions router/relay-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ func SetRelayRouter(router *gin.Engine) {
controller.Relay(c, types.RelayFormatRerank)
})

// mineru document parsing (file_parse)
httpRouter.POST("/file_parse", func(c *gin.Context) {
controller.Relay(c, types.RelayFormatMinerU)
})

// gemini relay routes
httpRouter.POST("/engines/:model/embeddings", func(c *gin.Context) {
controller.Relay(c, types.RelayFormatGemini)
Expand Down
3 changes: 2 additions & 1 deletion web/src/features/channels/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const CHANNEL_TYPES = {
61: 'Task Plugin',
62: 'vLLM',
63: 'SGLang',
64: 'MinerU',
} as const

export type ChannelProviderPresentation = {
Expand Down Expand Up @@ -185,7 +186,7 @@ export const CHANNEL_PROVIDER_PRESENTATION: Partial<
const CHANNEL_TYPE_DISPLAY_ORDER: number[] = [
1, 14, 24, 33, 43, 3, 41, 17, 45, 25, 26, 23, 48, 60, 58, 59, 61, 42, 34, 20,
4, 62, 40, 27, 15, 46, 18, 31, 35, 49, 19, 47, 37, 38, 39, 11, 8, 57, 22, 21,
44, 2, 5, 36, 50, 51, 52, 53, 54, 55, 56,
44, 2, 5, 36, 50, 51, 52, 53, 54, 55, 56, 64,
]

export const CHANNEL_TYPE_OPTIONS: { value: number; label: string }[] = (() => {
Expand Down