package openai import ( "bytes" "encoding/json" "errors" "fmt" "io" "mime/multipart" "net/http" "net/textproto" "one-api/common" constant2 "one-api/constant" "one-api/dto" "one-api/relay/channel" "one-api/relay/channel/ai360" "one-api/relay/channel/lingyiwanwu" "one-api/relay/channel/minimax" "one-api/relay/channel/moonshot" "one-api/relay/channel/openrouter" "one-api/relay/channel/xinference" relaycommon "one-api/relay/common" "one-api/relay/common_handler" "one-api/relay/constant" "one-api/service" "path/filepath" "strings" "github.com/gin-gonic/gin" ) type Adaptor struct { ChannelType int ResponseFormat string } func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { if !strings.Contains(request.Model, "claude") { return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model) } aiRequest, err := service.ClaudeToOpenAIRequest(*request, info) if err != nil { return nil, err } if info.SupportStreamOptions { aiRequest.StreamOptions = &dto.StreamOptions{ IncludeUsage: true, } } return a.ConvertOpenAIRequest(c, info, aiRequest) } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { a.ChannelType = info.ChannelType // initialize ThinkingContentInfo when thinking_to_content is enabled if think2Content, ok := info.ChannelSetting[constant2.ChannelSettingThinkingToContent].(bool); ok && think2Content { info.ThinkingContentInfo = relaycommon.ThinkingContentInfo{ IsFirstThinkingContent: true, SendLastThinkingContent: false, HasSentThinkingContent: false, } } } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if info.RelayFormat == relaycommon.RelayFormatClaude { return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil } if info.RelayMode == constant.RelayModeRealtime { if strings.HasPrefix(info.BaseUrl, "https://") { baseUrl := strings.TrimPrefix(info.BaseUrl, "https://") baseUrl = "wss://" + baseUrl info.BaseUrl = baseUrl } else if strings.HasPrefix(info.BaseUrl, "http://") { baseUrl := strings.TrimPrefix(info.BaseUrl, "http://") baseUrl = "ws://" + baseUrl info.BaseUrl = baseUrl } } switch info.ChannelType { case common.ChannelTypeAzure: apiVersion := info.ApiVersion if apiVersion == "" { apiVersion = constant2.AzureDefaultAPIVersion } // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api requestURL := strings.Split(info.RequestURLPath, "?")[0] requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, apiVersion) task := strings.TrimPrefix(requestURL, "/v1/") // 特殊处理 responses API if info.RelayMode == constant.RelayModeResponses { requestURL = fmt.Sprintf("/openai/v1/responses?api-version=preview") return relaycommon.GetFullRequestURL(info.BaseUrl, requestURL, info.ChannelType), nil } model_ := info.UpstreamModelName // 2025年5月10日后创建的渠道不移除. if info.ChannelCreateTime < constant2.AzureNoRemoveDotTime { model_ = strings.Replace(model_, ".", "", -1) } // https://github.com/songquanpeng/one-api/issues/67 requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task) if info.RelayMode == constant.RelayModeRealtime { requestURL = fmt.Sprintf("/openai/realtime?deployment=%s&api-version=%s", model_, apiVersion) } return relaycommon.GetFullRequestURL(info.BaseUrl, requestURL, info.ChannelType), nil case common.ChannelTypeMiniMax: return minimax.GetRequestURL(info) case common.ChannelTypeCustom: url := info.BaseUrl url = strings.Replace(url, "{model}", info.UpstreamModelName, -1) return url, nil default: return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil } } func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info *relaycommon.RelayInfo) error { channel.SetupApiRequestHeader(info, c, header) if info.ChannelType == common.ChannelTypeAzure { header.Set("api-key", info.ApiKey) return nil } if info.ChannelType == common.ChannelTypeOpenAI && "" != info.Organization { header.Set("OpenAI-Organization", info.Organization) } if info.RelayMode == constant.RelayModeRealtime { swp := c.Request.Header.Get("Sec-WebSocket-Protocol") if swp != "" { items := []string{ "realtime", "openai-insecure-api-key." + info.ApiKey, "openai-beta.realtime-v1", } header.Set("Sec-WebSocket-Protocol", strings.Join(items, ",")) //req.Header.Set("Sec-WebSocket-Key", c.Request.Header.Get("Sec-WebSocket-Key")) //req.Header.Set("Sec-Websocket-Extensions", c.Request.Header.Get("Sec-Websocket-Extensions")) //req.Header.Set("Sec-Websocket-Version", c.Request.Header.Get("Sec-Websocket-Version")) } else { header.Set("openai-beta", "realtime=v1") header.Set("Authorization", "Bearer "+info.ApiKey) } } else { header.Set("Authorization", "Bearer "+info.ApiKey) } if info.ChannelType == common.ChannelTypeOpenRouter { header.Set("HTTP-Referer", "https://github.com/Calcium-Ion/new-api") header.Set("X-Title", "New API") } return nil } func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } if info.ChannelType != common.ChannelTypeOpenAI && info.ChannelType != common.ChannelTypeAzure { request.StreamOptions = nil } if strings.HasPrefix(request.Model, "o") { if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 { request.MaxCompletionTokens = request.MaxTokens request.MaxTokens = 0 } request.Temperature = nil if strings.HasSuffix(request.Model, "-high") { request.ReasoningEffort = "high" request.Model = strings.TrimSuffix(request.Model, "-high") } else if strings.HasSuffix(request.Model, "-low") { request.ReasoningEffort = "low" request.Model = strings.TrimSuffix(request.Model, "-low") } else if strings.HasSuffix(request.Model, "-medium") { request.ReasoningEffort = "medium" request.Model = strings.TrimSuffix(request.Model, "-medium") } info.ReasoningEffort = request.ReasoningEffort info.UpstreamModelName = request.Model // o系列模型developer适配(o1-mini除外) if !strings.HasPrefix(request.Model, "o1-mini") && !strings.HasPrefix(request.Model, "o1-preview") { //修改第一个Message的内容,将system改为developer if len(request.Messages) > 0 && request.Messages[0].Role == "system" { request.Messages[0].Role = "developer" } } } return request, nil } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { return request, nil } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { return request, nil } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { a.ResponseFormat = request.ResponseFormat if info.RelayMode == constant.RelayModeAudioSpeech { jsonData, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("error marshalling object: %w", err) } return bytes.NewReader(jsonData), nil } else { var requestBody bytes.Buffer writer := multipart.NewWriter(&requestBody) writer.WriteField("model", request.Model) // 获取所有表单字段 formData := c.Request.PostForm // 遍历表单字段并打印输出 for key, values := range formData { if key == "model" { continue } for _, value := range values { writer.WriteField(key, value) } } // 添加文件字段 file, header, err := c.Request.FormFile("file") if err != nil { return nil, errors.New("file is required") } defer file.Close() part, err := writer.CreateFormFile("file", header.Filename) if err != nil { return nil, errors.New("create form file failed") } if _, err := io.Copy(part, file); err != nil { return nil, errors.New("copy file failed") } // 关闭 multipart 编写器以设置分界线 writer.Close() c.Request.Header.Set("Content-Type", writer.FormDataContentType()) return &requestBody, nil } } func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { switch info.RelayMode { case constant.RelayModeImagesEdits: var requestBody bytes.Buffer writer := multipart.NewWriter(&requestBody) writer.WriteField("model", request.Model) // 获取所有表单字段 formData := c.Request.PostForm // 遍历表单字段并打印输出 for key, values := range formData { if key == "model" { continue } for _, value := range values { writer.WriteField(key, value) } } // Parse the multipart form to handle both single image and multiple images if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max memory return nil, errors.New("failed to parse multipart form") } if c.Request.MultipartForm != nil && c.Request.MultipartForm.File != nil { // Check if "image" field exists in any form, including array notation var imageFiles []*multipart.FileHeader var exists bool // First check for standard "image" field if imageFiles, exists = c.Request.MultipartForm.File["image"]; !exists || len(imageFiles) == 0 { // If not found, check for "image[]" field if imageFiles, exists = c.Request.MultipartForm.File["image[]"]; !exists || len(imageFiles) == 0 { // If still not found, iterate through all fields to find any that start with "image[" foundArrayImages := false for fieldName, files := range c.Request.MultipartForm.File { if strings.HasPrefix(fieldName, "image[") && len(files) > 0 { foundArrayImages = true for _, file := range files { imageFiles = append(imageFiles, file) } } } // If no image fields found at all if !foundArrayImages && (len(imageFiles) == 0) { return nil, errors.New("image is required") } } } // Process all image files for i, fileHeader := range imageFiles { file, err := fileHeader.Open() if err != nil { return nil, fmt.Errorf("failed to open image file %d: %w", i, err) } defer file.Close() // If multiple images, use image[] as the field name fieldName := "image" if len(imageFiles) > 1 { fieldName = "image[]" } // Determine MIME type based on file extension mimeType := detectImageMimeType(fileHeader.Filename) // Create a form file with the appropriate content type h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, fileHeader.Filename)) h.Set("Content-Type", mimeType) part, err := writer.CreatePart(h) if err != nil { return nil, fmt.Errorf("create form part failed for image %d: %w", i, err) } if _, err := io.Copy(part, file); err != nil { return nil, fmt.Errorf("copy file failed for image %d: %w", i, err) } } // Handle mask file if present if maskFiles, exists := c.Request.MultipartForm.File["mask"]; exists && len(maskFiles) > 0 { maskFile, err := maskFiles[0].Open() if err != nil { return nil, errors.New("failed to open mask file") } defer maskFile.Close() // Determine MIME type for mask file mimeType := detectImageMimeType(maskFiles[0].Filename) // Create a form file with the appropriate content type h := make(textproto.MIMEHeader) h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="mask"; filename="%s"`, maskFiles[0].Filename)) h.Set("Content-Type", mimeType) maskPart, err := writer.CreatePart(h) if err != nil { return nil, errors.New("create form file failed for mask") } if _, err := io.Copy(maskPart, maskFile); err != nil { return nil, errors.New("copy mask file failed") } } } else { return nil, errors.New("no multipart form data found") } // 关闭 multipart 编写器以设置分界线 writer.Close() c.Request.Header.Set("Content-Type", writer.FormDataContentType()) return bytes.NewReader(requestBody.Bytes()), nil default: return request, nil } } // detectImageMimeType determines the MIME type based on the file extension func detectImageMimeType(filename string) string { ext := strings.ToLower(filepath.Ext(filename)) switch ext { case ".jpg", ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".webp": return "image/webp" default: // Try to detect from extension if possible if strings.HasPrefix(ext, ".jp") { return "image/jpeg" } // Default to png as a fallback return "image/png" } } func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { // 模型后缀转换 reasoning effort if strings.HasSuffix(request.Model, "-high") { request.Reasoning.Effort = "high" request.Model = strings.TrimSuffix(request.Model, "-high") } else if strings.HasSuffix(request.Model, "-low") { request.Reasoning.Effort = "low" request.Model = strings.TrimSuffix(request.Model, "-low") } else if strings.HasSuffix(request.Model, "-medium") { request.Reasoning.Effort = "medium" request.Model = strings.TrimSuffix(request.Model, "-medium") } return request, nil } func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { if info.RelayMode == constant.RelayModeAudioTranscription || info.RelayMode == constant.RelayModeAudioTranslation || info.RelayMode == constant.RelayModeImagesEdits { return channel.DoFormRequest(a, c, info, requestBody) } else if info.RelayMode == constant.RelayModeRealtime { return channel.DoWssRequest(a, c, info, requestBody) } else { return channel.DoApiRequest(a, c, info, requestBody) } } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { switch info.RelayMode { case constant.RelayModeRealtime: err, usage = OpenaiRealtimeHandler(c, info) case constant.RelayModeAudioSpeech: err, usage = OpenaiTTSHandler(c, resp, info) case constant.RelayModeAudioTranslation: fallthrough case constant.RelayModeAudioTranscription: err, usage = OpenaiSTTHandler(c, resp, info, a.ResponseFormat) case constant.RelayModeImagesGenerations, constant.RelayModeImagesEdits: err, usage = OpenaiHandlerWithUsage(c, resp, info) case constant.RelayModeRerank: err, usage = common_handler.RerankHandler(c, info, resp) case constant.RelayModeResponses: if info.IsStream { err, usage = OaiResponsesStreamHandler(c, resp, info) } else { err, usage = OaiResponsesHandler(c, resp, info) } default: if info.IsStream { err, usage = OaiStreamHandler(c, resp, info) } else { err, usage = OpenaiHandler(c, resp, info) } } return } func (a *Adaptor) GetModelList() []string { switch a.ChannelType { case common.ChannelType360: return ai360.ModelList case common.ChannelTypeMoonshot: return moonshot.ModelList case common.ChannelTypeLingYiWanWu: return lingyiwanwu.ModelList case common.ChannelTypeMiniMax: return minimax.ModelList case common.ChannelTypeXinference: return xinference.ModelList case common.ChannelTypeOpenRouter: return openrouter.ModelList default: return ModelList } } func (a *Adaptor) GetChannelName() string { switch a.ChannelType { case common.ChannelType360: return ai360.ChannelName case common.ChannelTypeMoonshot: return moonshot.ChannelName case common.ChannelTypeLingYiWanWu: return lingyiwanwu.ChannelName case common.ChannelTypeMiniMax: return minimax.ChannelName case common.ChannelTypeXinference: return xinference.ChannelName case common.ChannelTypeOpenRouter: return openrouter.ChannelName default: return ChannelName } }