package advancedcustom import ( "errors" "fmt" "io" "net/http" "net/url" "strings" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/relay/channel" "github.com/QuantumNous/new-api/relay/channel/claude" "github.com/QuantumNous/new-api/relay/channel/gemini" "github.com/QuantumNous/new-api/relay/channel/openai" relaycommon "github.com/QuantumNous/new-api/relay/common" relayconstant "github.com/QuantumNous/new-api/relay/constant" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/relayconvert" "github.com/QuantumNous/new-api/relaykit/types" "github.com/QuantumNous/new-api/service" "github.com/gin-gonic/gin" "github.com/samber/lo" ) const ChannelName = "advanced_custom" const advancedCustomModelPlaceholder = "{model}" type Adaptor struct { openaiAdaptor openai.Adaptor claudeAdaptor claude.Adaptor geminiAdaptor gemini.Adaptor resolved bool converted bool route dto.AdvancedCustomRoute converter string } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { a.openaiAdaptor.Init(info) a.claudeAdaptor.Init(info) a.geminiAdaptor.Init(info) } func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } if converter == relayconvert.ConverterNone { return a.convertOpenAICompatibleRequest(c, info, request) } switch converter { case relayconvert.ConverterOpenAIChatToClaudeMessages, relayconvert.ConverterOpenAIChatToOpenAIResponses, relayconvert.ConverterOpenAIChatToGeminiContent: result, err := service.ConvertRequestByID(c, info, converter, request) if err != nil { return nil, err } return result.Value, nil default: return nil, fmt.Errorf("converter %q does not support OpenAI chat completions requests", converter) } } func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } switch converter { case relayconvert.ConverterNone: return a.claudeAdaptor.ConvertClaudeRequest(c, info, request) case relayconvert.ConverterClaudeMessagesToOpenAIChat: result, err := service.ConvertRequestByID(c, info, converter, request) if err != nil { return nil, err } chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest) if !ok { return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value) } return a.convertOpenAICompatibleRequest(c, info, chatRequest) default: return nil, fmt.Errorf("converter %q does not support Anthropic Messages requests", converter) } } func (a *Adaptor) ConvertGeminiRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeminiChatRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } switch converter { case relayconvert.ConverterNone: return a.geminiAdaptor.ConvertGeminiRequest(c, info, request) case relayconvert.ConverterGeminiContentToOpenAIChat: result, err := service.ConvertRequestByID(c, info, converter, request) if err != nil { return nil, err } chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest) if !ok { return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value) } return a.convertOpenAICompatibleRequest(c, info, chatRequest) default: return nil, fmt.Errorf("converter %q does not support Gemini generateContent requests", converter) } } func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } switch converter { case relayconvert.ConverterNone: return a.convertOpenAICompatibleResponsesRequest(c, info, request) case relayconvert.ConverterOpenAIResponsesToOpenAIChat: result, err := service.ConvertRequestByID(c, info, converter, request) if err != nil { return nil, err } chatRequest, ok := result.Value.(*dto.GeneralOpenAIRequest) if !ok { return nil, fmt.Errorf("expected OpenAI chat completions request, got %T", result.Value) } return a.convertOpenAICompatibleRequest(c, info, chatRequest) case relayconvert.ConverterOpenAIResponsesToGemini: result, err := service.ConvertRequestByID(c, info, converter, request) if err != nil { return nil, err } geminiRequest, ok := result.Value.(*dto.GeminiChatRequest) if !ok { return nil, fmt.Errorf("expected Gemini generateContent request, got %T", result.Value) } return geminiRequest, nil default: return nil, fmt.Errorf("converter %q does not support OpenAI Responses requests", converter) } } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } if converter != relayconvert.ConverterNone { return nil, fmt.Errorf("converter %q does not support embedding requests", converter) } return a.convertOpenAICompatibleEmbeddingRequest(c, info, request) } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } if converter != relayconvert.ConverterNone { return nil, fmt.Errorf("converter %q does not support audio requests", converter) } return a.convertOpenAICompatibleAudioRequest(c, info, request) } func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { converter, err := a.resolveForConversion(c, info) if err != nil { return nil, err } if converter != relayconvert.ConverterNone { return nil, fmt.Errorf("converter %q does not support image requests", converter) } return a.convertOpenAICompatibleImageRequest(c, info, request) } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { a.converted = true return a.openaiAdaptor.ConvertRerankRequest(c, relayMode, request) } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if err := a.resolve(nil, info); err != nil { return "", err } return a.routeURL(info) } func (a *Adaptor) BuildModelListRequest(info *relaycommon.RelayInfo) (string, http.Header, error) { if info == nil { return "", nil, errors.New("missing relay info") } config := info.ChannelOtherSettings.AdvancedCustom if config == nil { return "", nil, errors.New("advanced_custom is required") } if err := config.Validate(); err != nil { return "", nil, err } route, ok := config.ModelListRoute() if !ok { return "", nil, errors.New("advanced custom channel does not configure a /v1/models route") } converter := strings.TrimSpace(route.Converter) if converter == "" { converter = relayconvert.ConverterNone } if converter != relayconvert.ConverterNone { return "", nil, fmt.Errorf("converter %q does not support model list requests", converter) } requestURL, err := buildRouteURL(route, converter, info) if err != nil { return "", nil, err } header := http.Header{} auth := route.Auth if auth == nil { header.Set("Authorization", "Bearer "+info.ApiKey) return requestURL, header, nil } switch strings.TrimSpace(auth.Type) { case dto.AdvancedCustomAuthTypeNone, dto.AdvancedCustomAuthTypeQuery: case dto.AdvancedCustomAuthTypeHeader: header.Set(strings.TrimSpace(auth.Name), applyAuthTemplate(auth.Value, info.ApiKey)) default: return "", nil, fmt.Errorf("invalid advanced custom auth type: %s", auth.Type) } return requestURL, header, nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error { if err := a.resolve(c, info); err != nil { return err } channel.SetupApiRequestHeader(info, c, header) auth := a.route.Auth if auth == nil { header.Set("Authorization", "Bearer "+info.ApiKey) } else { switch strings.TrimSpace(auth.Type) { case dto.AdvancedCustomAuthTypeNone: case dto.AdvancedCustomAuthTypeHeader: header.Set(strings.TrimSpace(auth.Name), applyAuthTemplate(auth.Value, info.ApiKey)) case dto.AdvancedCustomAuthTypeQuery: default: return fmt.Errorf("invalid advanced custom auth type: %s", auth.Type) } } if shouldApplyClaudeHeaders(a.converter, info) { applyClaudeHeaders(c, header, info) } return nil } func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { if err := a.resolve(c, info); err != nil { return nil, err } if !a.converted && a.converter != relayconvert.ConverterNone { return nil, errors.New("advanced custom converter routes cannot be used with pass-through request body") } if info.RelayMode == relayconstant.RelayModeAudioTranscription || info.RelayMode == relayconstant.RelayModeAudioTranslation || (info.RelayMode == relayconstant.RelayModeImagesEdits && !isJSONRequest(c)) { return channel.DoFormRequest(a, c, info, requestBody) } if info.RelayMode == relayconstant.RelayModeRealtime { return channel.DoWssRequest(a, c, info, requestBody) } return channel.DoApiRequest(a, c, info, requestBody) } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) { if err := a.resolve(c, info); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry()) } switch a.converter { case relayconvert.ConverterNone: return a.doNativeResponse(c, resp, info) case relayconvert.ConverterClaudeMessagesToOpenAIChat, relayconvert.ConverterGeminiContentToOpenAIChat: return a.openaiAdaptor.DoResponse(c, resp, info) case relayconvert.ConverterOpenAIChatToClaudeMessages: return a.claudeAdaptor.DoResponse(c, resp, info) case relayconvert.ConverterOpenAIChatToGeminiContent: return a.geminiAdaptor.DoResponse(c, resp, info) case relayconvert.ConverterOpenAIResponsesToGemini: return a.geminiAdaptor.DoResponse(c, resp, info) case relayconvert.ConverterOpenAIChatToOpenAIResponses: if info.IsStream { return openai.OaiResponsesToChatStreamHandler(c, info, resp) } return openai.OaiResponsesToChatHandler(c, info, resp) case relayconvert.ConverterOpenAIResponsesToOpenAIChat: if info.IsStream { return openai.OaiChatToResponsesStreamHandler(c, info, resp) } return openai.OaiChatToResponsesHandler(c, info, resp) default: return nil, types.NewOpenAIError(fmt.Errorf("unsupported advanced custom converter: %s", a.converter), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry()) } } func (a *Adaptor) GetModelList() []string { models := make([]string, 0, len(openai.ModelList)+len(claude.ModelList)+len(gemini.ModelList)) models = append(models, openai.ModelList...) models = append(models, claude.ModelList...) models = append(models, gemini.ModelList...) return lo.Uniq(models) } func (a *Adaptor) GetChannelName() string { return ChannelName } func (a *Adaptor) doNativeResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (any, *types.NewAPIError) { switch info.RelayFormat { case types.RelayFormatClaude: return a.claudeAdaptor.DoResponse(c, resp, info) case types.RelayFormatGemini: return a.geminiAdaptor.DoResponse(c, resp, info) default: return a.openaiAdaptor.DoResponse(c, resp, info) } } func (a *Adaptor) resolveForConversion(c *gin.Context, info *relaycommon.RelayInfo) (string, error) { if err := a.resolve(c, info); err != nil { return "", err } a.converted = true return a.converter, nil } func (a *Adaptor) resolve(c *gin.Context, info *relaycommon.RelayInfo) error { if a.resolved { return nil } if info == nil { return errors.New("missing relay info") } config := info.ChannelOtherSettings.AdvancedCustom if config == nil { return errors.New("advanced_custom is required") } if err := config.Validate(); err != nil { return err } incomingPath := incomingRequestPath(c, info) route, ok := config.MatchPathForModel(incomingPath, info.OriginModelName) if ok { route.Converter = strings.TrimSpace(route.Converter) if route.Converter == "" { route.Converter = relayconvert.ConverterNone } a.route = route a.converter = route.Converter a.resolved = true return nil } return fmt.Errorf("advanced custom channel does not support request path %s for model %s", incomingPath, info.OriginModelName) } func incomingRequestPath(c *gin.Context, info *relaycommon.RelayInfo) string { if c != nil && c.Request != nil && c.Request.URL != nil { return c.Request.URL.Path } if info == nil { return "" } return strings.Split(info.RequestURLPath, "?")[0] } func (a *Adaptor) routeURL(info *relaycommon.RelayInfo) (string, error) { return buildRouteURL(a.route, a.converter, info) } func buildRouteURL(route dto.AdvancedCustomRoute, converter string, info *relaycommon.RelayInfo) (string, error) { parsedURL, err := resolveUpstreamTargetURL(applyUpstreamPathTemplate(strings.TrimSpace(route.UpstreamPath), info), info) if err != nil { return "", err } if shouldUseGeminiStreamURL(converter, info) { useGeminiStreamGenerateContentURL(parsedURL) } if info != nil && info.RelayMode == relayconstant.RelayModeRealtime { switch parsedURL.Scheme { case "https": parsedURL.Scheme = "wss" case "http": parsedURL.Scheme = "ws" } } if route.Auth != nil && strings.TrimSpace(route.Auth.Type) == dto.AdvancedCustomAuthTypeQuery { query := parsedURL.Query() query.Set(strings.TrimSpace(route.Auth.Name), applyAuthTemplate(route.Auth.Value, info.ApiKey)) parsedURL.RawQuery = query.Encode() } return parsedURL.String(), nil } func resolveUpstreamTargetURL(upstreamPath string, info *relaycommon.RelayInfo) (*url.URL, error) { if strings.HasPrefix(upstreamPath, "/") { if strings.HasPrefix(upstreamPath, "//") { return nil, errors.New("advanced custom upstream path must be a full URL or a path starting with /") } if info == nil || strings.TrimSpace(info.ChannelBaseUrl) == "" { return nil, errors.New("channel base URL is required when advanced custom upstream path is relative") } return joinBaseURLAndUpstreamPath(info.ChannelBaseUrl, upstreamPath) } parsedURL, err := url.Parse(upstreamPath) if err != nil { return nil, err } if parsedURL.Scheme == "" || parsedURL.Host == "" { return nil, errors.New("advanced custom upstream path must be a full URL or a path starting with /") } if !strings.EqualFold(parsedURL.Scheme, "http") && !strings.EqualFold(parsedURL.Scheme, "https") { return nil, errors.New("advanced custom upstream path must use http or https") } return parsedURL, nil } func joinBaseURLAndUpstreamPath(baseURL string, upstreamPath string) (*url.URL, error) { parsedBaseURL, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil { return nil, err } if parsedBaseURL.Scheme == "" || parsedBaseURL.Host == "" { return nil, errors.New("channel base URL must be a full URL when advanced custom upstream path is relative") } if !strings.EqualFold(parsedBaseURL.Scheme, "http") && !strings.EqualFold(parsedBaseURL.Scheme, "https") { return nil, errors.New("channel base URL must use http or https when advanced custom upstream path is relative") } parsedPath, err := url.Parse(upstreamPath) if err != nil { return nil, err } parsedBaseURL.Path = strings.TrimRight(parsedBaseURL.Path, "/") + "/" + strings.TrimLeft(parsedPath.Path, "/") parsedBaseURL.RawPath = "" parsedBaseURL.RawQuery = parsedPath.RawQuery parsedBaseURL.Fragment = parsedPath.Fragment return parsedBaseURL, nil } func applyUpstreamPathTemplate(upstreamPath string, info *relaycommon.RelayInfo) string { if info == nil { return upstreamPath } return strings.ReplaceAll(upstreamPath, advancedCustomModelPlaceholder, info.UpstreamModelName) } func shouldUseGeminiStreamURL(converter string, info *relaycommon.RelayInfo) bool { return info != nil && info.IsStream && (converter == relayconvert.ConverterOpenAIChatToGeminiContent || converter == relayconvert.ConverterOpenAIResponsesToGemini) } func useGeminiStreamGenerateContentURL(parsedURL *url.URL) { if strings.Contains(parsedURL.Path, ":generateContent") { parsedURL.Path = strings.Replace(parsedURL.Path, ":generateContent", ":streamGenerateContent", 1) } if strings.Contains(parsedURL.Path, ":streamGenerateContent") { query := parsedURL.Query() query.Set("alt", "sse") parsedURL.RawQuery = query.Encode() } } func shouldApplyClaudeHeaders(converter string, info *relaycommon.RelayInfo) bool { return converter == relayconvert.ConverterOpenAIChatToClaudeMessages || (converter == relayconvert.ConverterNone && info != nil && info.RelayFormat == types.RelayFormatClaude) } func applyClaudeHeaders(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) { anthropicVersion := "" if c != nil && c.Request != nil { anthropicVersion = c.Request.Header.Get("anthropic-version") } if anthropicVersion == "" { anthropicVersion = "2023-06-01" } header.Set("anthropic-version", anthropicVersion) if c != nil { claude.CommonClaudeHeadersOperation(c, header, info) } } func applyAuthTemplate(template string, apiKey string) string { return strings.ReplaceAll(template, "{api_key}", apiKey) } func isJSONRequest(c *gin.Context) bool { if c == nil || c.Request == nil { return false } return strings.Contains(strings.ToLower(c.Request.Header.Get("Content-Type")), "application/json") } func (a *Adaptor) convertOpenAICompatibleRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { old := info.ChannelType info.ChannelType = constant.ChannelTypeOpenAI converted, err := a.openaiAdaptor.ConvertOpenAIRequest(c, info, request) info.ChannelType = old return converted, err } func (a *Adaptor) convertOpenAICompatibleResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { old := info.ChannelType info.ChannelType = constant.ChannelTypeOpenAI converted, err := a.openaiAdaptor.ConvertOpenAIResponsesRequest(c, info, request) info.ChannelType = old return converted, err } func (a *Adaptor) convertOpenAICompatibleEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { old := info.ChannelType info.ChannelType = constant.ChannelTypeOpenAI converted, err := a.openaiAdaptor.ConvertEmbeddingRequest(c, info, request) info.ChannelType = old return converted, err } func (a *Adaptor) convertOpenAICompatibleAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { old := info.ChannelType info.ChannelType = constant.ChannelTypeOpenAI converted, err := a.openaiAdaptor.ConvertAudioRequest(c, info, request) info.ChannelType = old return converted, err } func (a *Adaptor) convertOpenAICompatibleImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { old := info.ChannelType info.ChannelType = constant.ChannelTypeOpenAI converted, err := a.openaiAdaptor.ConvertImageRequest(c, info, request) info.ChannelType = old return converted, err }