package oaichat import ( "fmt" "strings" "context" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta" relaymedia "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/media" sharedclaude "github.com/QuantumNous/new-api/relaykit/relayconvert/internal/shared/claude" kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil" "github.com/QuantumNous/new-api/relaykit/relayconvert/reasoning" ) func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { opts := convmeta.OptionsOf(info) claudeTools := make([]any, 0, len(textRequest.Tools)) for _, tool := range textRequest.Tools { if _, ok := tool.Function.Parameters.(map[string]any); !ok && tool.Type != "function" { continue } claudeTools = append(claudeTools, &dto.Tool{ Name: tool.Function.Name, Description: tool.Function.Description, InputSchema: sharedclaude.FunctionParametersToInputSchema(tool.Function.Parameters), }) } if textRequest.WebSearchOptions != nil { webSearchTool := dto.ClaudeWebSearchTool{ Type: "web_search_20250305", Name: "web_search", } if textRequest.WebSearchOptions.UserLocation != nil { anthropicUserLocation := &dto.ClaudeWebSearchUserLocation{ Type: "approximate", } var userLocationMap map[string]interface{} if err := kitutil.Unmarshal(textRequest.WebSearchOptions.UserLocation, &userLocationMap); err == nil { if approximateData, ok := userLocationMap["approximate"].(map[string]interface{}); ok { if timezone, ok := approximateData["timezone"].(string); ok && timezone != "" { anthropicUserLocation.Timezone = timezone } if country, ok := approximateData["country"].(string); ok && country != "" { anthropicUserLocation.Country = country } if region, ok := approximateData["region"].(string); ok && region != "" { anthropicUserLocation.Region = region } if city, ok := approximateData["city"].(string); ok && city != "" { anthropicUserLocation.City = city } } } webSearchTool.UserLocation = anthropicUserLocation } claudeTools = append(claudeTools, &webSearchTool) } claudeRequest := dto.ClaudeRequest{ Model: textRequest.Model, StopSequences: nil, Temperature: textRequest.Temperature, } if len(claudeTools) > 0 { claudeRequest.Tools = claudeTools } if textRequest.MaxCompletionTokens != nil && *textRequest.MaxCompletionTokens > 0 { claudeRequest.MaxTokens = kitutil.GetPointer(*textRequest.MaxCompletionTokens) } else if textRequest.MaxTokens != nil && *textRequest.MaxTokens > 0 { claudeRequest.MaxTokens = kitutil.GetPointer(*textRequest.MaxTokens) } if textRequest.TopP != nil { claudeRequest.TopP = kitutil.GetPointer(*textRequest.TopP) } if textRequest.TopK != nil { claudeRequest.TopK = kitutil.GetPointer(*textRequest.TopK) } if textRequest.IsStream(nil) { claudeRequest.Stream = kitutil.GetPointer(true) } if textRequest.ToolChoice != nil || textRequest.ParallelTooCalls != nil { claudeToolChoice := sharedclaude.MapOpenAIToolChoice(textRequest.ToolChoice, textRequest.ParallelTooCalls) if claudeToolChoice != nil { claudeRequest.ToolChoice = claudeToolChoice } } sourceReasoning, err := reasoning.FromOpenAIChat(&textRequest) if err != nil { return nil, reasoning.AsClientError(err) } if err := sharedclaude.ApplyReasoning(&claudeRequest, info, sourceReasoning); err != nil { return nil, reasoning.AsClientError(err) } if claudeRequest.MaxTokens == nil { if defaultMaxTokens, configured := opts.Claude.DefaultMaxTokensFor(claudeRequest.Model); configured { value := uint(defaultMaxTokens) claudeRequest.MaxTokens = &value } } if textRequest.Stop != nil { switch stop := textRequest.Stop.(type) { case string: claudeRequest.StopSequences = []string{stop} case []interface{}: stopSequences := make([]string, 0) for _, item := range stop { stopSequences = append(stopSequences, item.(string)) } claudeRequest.StopSequences = stopSequences } } formatMessages := make([]dto.Message, 0) lastMessage := dto.Message{ Role: "tool", } for _, message := range textRequest.Messages { switch message.Role { case "": message.Role = "user" case "developer": message.Role = "system" case "function": if message.ToolCallId != "" { message.Role = "tool" } else { message.Role = "user" } case "tool": if message.ToolCallId == "" { message.Role = "user" } case "system", "user", "assistant": default: message.Role = "user" } fmtMessage := dto.Message{ Role: message.Role, Content: message.Content, } if message.Role == "tool" { fmtMessage.ToolCallId = message.ToolCallId } if message.Role == "assistant" && message.ToolCalls != nil { fmtMessage.ToolCalls = message.ToolCalls } if lastMessage.Role == message.Role && lastMessage.Role != "tool" { if lastMessage.IsStringContent() && message.IsStringContent() { fmtMessage.SetStringContent(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent())) formatMessages = formatMessages[:len(formatMessages)-1] } } if fmtMessage.Content == nil || (fmtMessage.IsStringContent() && fmtMessage.StringContent() == "") { fmtMessage.SetStringContent("...") } formatMessages = append(formatMessages, fmtMessage) lastMessage = fmtMessage } claudeMessages := make([]dto.ClaudeMessage, 0) isFirstMessage := true var systemMessages []dto.ClaudeMediaMessage placeholderUserMessage := dto.ClaudeMessage{ Role: "user", Content: []dto.ClaudeMediaMessage{ { Type: "text", Text: kitutil.GetPointer[string]("..."), }, }, } for _, message := range formatMessages { if message.Role == "system" { if message.IsStringContent() { if text := message.StringContent(); text != "" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ Type: "text", Text: kitutil.GetPointer[string](text), }) } } else { for _, ctx := range message.ParseContent() { if ctx.Type == "text" && ctx.Text != "" { systemMessages = append(systemMessages, dto.ClaudeMediaMessage{ Type: "text", Text: kitutil.GetPointer[string](ctx.Text), }) } } } continue } if isFirstMessage { isFirstMessage = false if message.Role != "user" { claudeMessages = append(claudeMessages, placeholderUserMessage) } } claudeMessage := dto.ClaudeMessage{ Role: message.Role, } if message.Role == "tool" { if len(claudeMessages) > 0 && claudeMessages[len(claudeMessages)-1].Role == "user" { lastClaudeMessage := claudeMessages[len(claudeMessages)-1] if content, ok := lastClaudeMessage.Content.(string); ok { lastClaudeMessage.Content = []dto.ClaudeMediaMessage{ { Type: "text", Text: kitutil.GetPointer[string](content), }, } } lastClaudeMessage.Content = append(lastClaudeMessage.Content.([]dto.ClaudeMediaMessage), dto.ClaudeMediaMessage{ Type: "tool_result", ToolUseId: message.ToolCallId, Content: message.Content, }) claudeMessages[len(claudeMessages)-1] = lastClaudeMessage continue } claudeMessage.Role = "user" claudeMessage.Content = []dto.ClaudeMediaMessage{ { Type: "tool_result", ToolUseId: message.ToolCallId, Content: message.Content, }, } } else if message.IsStringContent() && message.ToolCalls == nil { text := message.StringContent() if text == "" { text = "..." } claudeMessage.Content = text } else { claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0) for _, mediaMessage := range message.ParseContent() { switch mediaMessage.Type { case "text": if mediaMessage.Text != "" { claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ Type: "text", Text: kitutil.GetPointer[string](mediaMessage.Text), }) } default: source := mediaMessage.ToFileSource() if source == nil { continue } base64Data, mimeType, err := relaymedia.ResolveBase64Data(c, source, "formatting image for Claude") if err != nil { return nil, fmt.Errorf("get file data failed: %s", err.Error()) } claudeMediaMessage := dto.ClaudeMediaMessage{ Source: &dto.ClaudeMessageSource{ Type: "base64", }, } if strings.HasPrefix(mimeType, "application/pdf") { claudeMediaMessage.Type = "document" } else { claudeMediaMessage.Type = "image" } claudeMediaMessage.Source.MediaType = mimeType claudeMediaMessage.Source.Data = base64Data claudeMediaMessages = append(claudeMediaMessages, claudeMediaMessage) continue } } if message.ToolCalls != nil { for _, toolCall := range message.ParseToolCalls() { inputObj := make(map[string]any) if args := toolCall.Function.Arguments; args != "" { if err := kitutil.Unmarshal([]byte(args), &inputObj); err != nil { kitutil.LogInfo("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments)) } } claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ Type: "tool_use", Id: toolCall.ID, Name: toolCall.Function.Name, Input: inputObj, }) } } claudeMessage.Content = claudeMediaMessages } claudeMessages = append(claudeMessages, claudeMessage) } if len(claudeMessages) == 0 && len(systemMessages) > 0 { claudeMessages = append(claudeMessages, placeholderUserMessage) } if len(systemMessages) > 0 { claudeRequest.System = systemMessages } claudeRequest.Prompt = "" claudeRequest.Messages = claudeMessages // Checked last so every injection path (default hook, thinking adapter // floor) has had its chance to satisfy the required field. if claudeRequest.MaxTokens == nil { return nil, sharedclaude.ErrMissingMaxTokens } return &claudeRequest, nil }