Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 3 additions & 2 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -800,8 +800,9 @@ func (info *RelayInfo) ConvOptions() *convmeta.Options {
SupportsImagine: model_setting.IsGeminiModelSupportImagine,
SafetySetting: model_setting.GetGeminiSafetySetting,
},
OpenRouterDialect: info != nil && info.GetChannelType() == constant.ChannelTypeOpenRouter,
PreserveThinkingSuffix: model_setting.ShouldPreserveThinkingSuffix,
OpenRouterDialect: info != nil && info.GetChannelType() == constant.ChannelTypeOpenRouter,
PreserveThinkingSuffix: model_setting.ShouldPreserveThinkingSuffix,
PreserveReasoningContent: info != nil && model_setting.ShouldPreserveReasoningContent(info.GetUpstreamModelName()),
}
if info != nil {
info.convOptions = options
Expand Down
5 changes: 5 additions & 0 deletions relaykit/relayconvert/convmeta/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ type Options struct {
// suffix must be kept on the outgoing model name (host blacklist lookup).
// Nil means "never preserve".
PreserveThinkingSuffix func(modelName string) bool

// PreserveReasoningContent maps assistant reasoning between reasoning_content
// and each target protocol's equivalent request field. It is opt-in because
// reasoning_content is non-standard and is not accepted by every upstream model.
PreserveReasoningContent bool
}

type ClaudeOptions struct {
Expand Down
17 changes: 14 additions & 3 deletions relaykit/relayconvert/internal/claude_messages/to_oai_chat_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info con
openAIRequest.Stream = kitutil.GetPointer(*claudeRequest.Stream)
}

isOpenRouter := convmeta.OptionsOf(info).OpenRouterDialect
opts := convmeta.OptionsOf(info)
isOpenRouter := opts.OpenRouterDialect
if isOpenRouter {
if effort := claudeRequest.GetEfforts(); effort != "" {
effortBytes, _ := kitutil.Marshal(effort)
Expand Down Expand Up @@ -146,7 +147,7 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info con
}
var toolCalls []dto.ToolCallRequest
mediaMessages := make([]dto.MediaContent, 0, len(content))

var reasoning strings.Builder
for _, mediaMsg := range content {
switch mediaMsg.Type {
case "text", "input_text":
Expand All @@ -156,6 +157,12 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info con
CacheControl: mediaMsg.CacheControl,
}
mediaMessages = append(mediaMessages, message)
case "thinking":
if opts.PreserveReasoningContent && claudeMessage.Role == "assistant" && mediaMsg.Thinking != nil {
reasoning.WriteString(*mediaMsg.Thinking)
}
case "redacted_thinking":
// The encrypted payload cannot be represented by reasoning_content.
case "image":
imageData := fmt.Sprintf("data:%s;base64,%s", mediaMsg.Source.MediaType, mediaMsg.Source.Data)
mediaMessage := dto.MediaContent{
Expand Down Expand Up @@ -200,8 +207,12 @@ func ClaudeMessagesRequestToOpenAIChat(claudeRequest dto.ClaudeRequest, info con
if len(mediaMessages) > 0 && len(toolCalls) == 0 {
openAIMessage.SetMediaContent(mediaMessages)
}
if reasoning.Len() > 0 {
reasoningContent := reasoning.String()
openAIMessage.ReasoningContent = &reasoningContent
}
}
if len(openAIMessage.ParseContent()) > 0 || len(openAIMessage.ToolCalls) > 0 {
if len(openAIMessage.ParseContent()) > 0 || len(openAIMessage.ToolCalls) > 0 || openAIMessage.GetReasoningContent() != "" {
openAIMessages = append(openAIMessages, openAIMessage)
}
}
Expand Down
14 changes: 13 additions & 1 deletion relaykit/relayconvert/internal/gemini_chat/to_oai_chat_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
)

func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatRequest, info convmeta.Meta) (*dto.GeneralOpenAIRequest, error) {
preserveReasoning := convmeta.OptionsOf(info).PreserveReasoningContent
modelName := ""
isStream := false
if info != nil {
Expand All @@ -30,7 +31,14 @@ func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatReque

var mediaContents []dto.MediaContent
var toolCalls []dto.ToolCallRequest
var reasoning strings.Builder
for _, part := range content.Parts {
if part.Thought {
if preserveReasoning && message.Role == "assistant" {
reasoning.WriteString(part.Text)
}
continue
}
if part.Text != "" {
mediaContent := dto.MediaContent{
Type: "text",
Expand Down Expand Up @@ -84,8 +92,12 @@ func GeminiGenerateContentRequestToOpenAIChat(geminiRequest *dto.GeminiChatReque
} else if len(mediaContents) > 0 {
message.SetMediaContent(mediaContents)
}
if reasoning.Len() > 0 {
reasoningContent := reasoning.String()
message.ReasoningContent = &reasoningContent
}

if len(message.ParseContent()) > 0 || len(message.ToolCalls) > 0 {
if len(message.ParseContent()) > 0 || len(message.ToolCalls) > 0 || message.GetReasoningContent() != "" {
messages = append(messages, message)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, te
Role: message.Role,
Content: message.Content,
}
if opts.PreserveReasoningContent && message.Role == "assistant" {
if reasoning := message.GetReasoningContent(); reasoning != "" {
fmtMessage.ReasoningContent = &reasoning
}
}
if message.Role == "tool" {
fmtMessage.ToolCallId = message.ToolCallId
}
Expand All @@ -246,6 +251,9 @@ func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, te
if lastMessage.Role == message.Role && lastMessage.Role != "tool" {
if lastMessage.IsStringContent() && message.IsStringContent() {
fmtMessage.SetStringContent(strings.Trim(fmt.Sprintf("%s %s", lastMessage.StringContent(), message.StringContent()), "\""))
if mergedReasoning := lastMessage.GetReasoningContent() + fmtMessage.GetReasoningContent(); mergedReasoning != "" {
fmtMessage.ReasoningContent = &mergedReasoning
}
formatMessages = formatMessages[:len(formatMessages)-1]
}
}
Expand Down Expand Up @@ -329,17 +337,30 @@ func OpenAIChatRequestToClaudeMessages(c context.Context, info convmeta.Meta, te
Content: message.Content,
},
}
} else if message.IsStringContent() && message.ToolCalls == nil {
} else if message.IsStringContent() && message.ToolCalls == nil && message.GetReasoningContent() == "" {
text := message.StringContent()
if text == "" {
text = "..."
}
claudeMessage.Content = text
} else {
claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0)
reasoningEmitted := false
if message.Role == "assistant" {
if reasoning := message.GetReasoningContent(); reasoning != "" {
claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
Type: "thinking",
Thinking: kitutil.GetPointer(reasoning),
})
reasoningEmitted = true
}
}
for _, mediaMessage := range message.ParseContent() {
switch mediaMessage.Type {
case "text":
if reasoningEmitted && mediaMessage.Text == "..." {
continue
}
if mediaMessage.Text != "" {
claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{
Type: "text",
Expand Down
5 changes: 5 additions & 0 deletions relaykit/relayconvert/internal/oai_chat/to_gemini_chat_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ func OpenAIChatRequestToGeminiGenerateContent(c context.Context, textRequest dto
}
shouldAttachThoughtSignature := (message.Role == "assistant" || message.Role == "model") && sharedgemini.ShouldAttachThoughtSignature(opts)
signatureAttached := false
if opts.PreserveReasoningContent && (message.Role == "assistant" || message.Role == "model") {
if reasoning := message.GetReasoningContent(); reasoning != "" {
parts = append(parts, dto.GeminiPart{Text: reasoning, Thought: true})
}
}
if message.ToolCalls != nil {
for _, call := range message.ParseToolCalls() {
args := map[string]interface{}{}
Expand Down
16 changes: 15 additions & 1 deletion relaykit/relayconvert/internal/oai_chat/to_oai_responses_req.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/relayconvert/convmeta"
kitutil "github.com/QuantumNous/new-api/relaykit/relayconvert/kitutil"
"github.com/samber/lo"
)
Expand Down Expand Up @@ -73,7 +74,7 @@ func convertChatResponseFormatToResponsesText(reqFormat *dto.ResponseFormat) jso
return textRaw
}

func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) {
func ChatCompletionsRequestToResponsesRequest(info convmeta.Meta, req *dto.GeneralOpenAIRequest) (*dto.OpenAIResponsesRequest, error) {
if req == nil {
return nil, errors.New("request is nil")
}
Expand All @@ -84,6 +85,7 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
return nil, fmt.Errorf("n>1 is not supported in responses compatibility mode")
}

preserveReasoning := convmeta.OptionsOf(info).PreserveReasoningContent
var instructionsParts []string
inputItems := make([]map[string]any, 0, len(req.Messages))

Expand Down Expand Up @@ -152,6 +154,18 @@ func ChatCompletionsRequestToResponsesRequest(req *dto.GeneralOpenAIRequest) (*d
continue
}

if preserveReasoning && role == "assistant" {
if reasoning := msg.GetReasoningContent(); reasoning != "" {
inputItems = append(inputItems, map[string]any{
"type": "reasoning",
"id": fmt.Sprintf("rs_%s", kitutil.GetUUID()),
"summary": []map[string]any{
{"type": "summary_text", "text": reasoning},
},
})
}
}

item := map[string]any{
"role": role,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestChatCompletionsRequestToResponsesRequestInstructionsAndTools(t *testing
},
}

got, err := ChatCompletionsRequestToResponsesRequest(req)
got, err := ChatCompletionsRequestToResponsesRequest(nil, req)
require.NoError(t, err)

assert.Equal(t, "gpt-test", got.Model)
Expand Down Expand Up @@ -60,7 +60,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesQwenThinkingBudget(t *
},
}

got, err := ChatCompletionsRequestToResponsesRequest(req)
got, err := ChatCompletionsRequestToResponsesRequest(nil, req)
require.NoError(t, err)
assert.Equal(t, tt.budget, got.ThinkingBudget)

Expand All @@ -76,7 +76,7 @@ func TestChatCompletionsRequestToResponsesRequestPreservesQwenThinkingBudget(t *
}

func TestChatCompletionsRequestToResponsesRequestRejectsMultipleChoices(t *testing.T) {
_, err := ChatCompletionsRequestToResponsesRequest(&dto.GeneralOpenAIRequest{
_, err := ChatCompletionsRequestToResponsesRequest(nil, &dto.GeneralOpenAIRequest{
Model: "gpt-test",
N: lo.ToPtr(2),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
return nil, err
}

opts := convmeta.OptionsOf(info)
preserveReasoning := opts.PreserveReasoningContent
claudeRequest := &dto.ClaudeRequest{
Model: req.Model,
Temperature: req.Temperature,
Expand All @@ -41,7 +43,7 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
claudeRequest.MaxTokens = kitutil.GetPointer(*req.MaxOutputTokens)
}
if claudeRequest.MaxTokens == nil || *claudeRequest.MaxTokens == 0 {
if defaultMaxTokens, configured := convmeta.OptionsOf(info).Claude.DefaultMaxTokensFor(req.Model); configured {
if defaultMaxTokens, configured := opts.Claude.DefaultMaxTokensFor(req.Model); configured {
value := uint(defaultMaxTokens)
claudeRequest.MaxTokens = &value
}
Expand Down Expand Up @@ -82,21 +84,50 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
if err != nil {
return nil, err
}
pendingReasoning := ""
for _, item := range inputItems {
itemType := strings.TrimSpace(kitutil.Interface2String(item["type"]))
switch itemType {
case ResponsesInputTypeFunctionCall:
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, "arguments"))
case ResponsesInputTypeCustomToolCall:
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, "input"))
case responsesInputTypeReasoning:
if preserveReasoning {
appendPendingReasoning(&pendingReasoning, responsesItemReasoningText(item))
}
case ResponsesInputTypeFunctionCall, ResponsesInputTypeCustomToolCall:
if preserveReasoning {
appendPendingReasoning(&pendingReasoning, responsesItemReasoningText(item))
claudeRequest.Messages = appendClaudeReasoning(claudeRequest.Messages, pendingReasoning)
pendingReasoning = ""
}
inputKey := "arguments"
if itemType == ResponsesInputTypeCustomToolCall {
inputKey = "input"
}
claudeRequest.Messages = appendClaudeToolUse(claudeRequest.Messages, responsesFunctionCallItemToClaudeToolUse(item, inputKey))
case ResponsesInputTypeFunctionCallOutput, ResponsesInputTypeCustomToolOutput:
if preserveReasoning {
claudeRequest.Messages = appendClaudeReasoning(claudeRequest.Messages, pendingReasoning)
pendingReasoning = ""
}
claudeRequest.Messages = appendClaudeToolResult(claudeRequest.Messages, responsesFunctionOutputItemToClaudeToolResult(item))
default:
role := responsesClaudeRole(item)
parts, err := responsesInputContentToClaudeMediaMessages(c, item["content"])
if err != nil {
return nil, err
}
if preserveReasoning && role == "assistant" {
appendPendingReasoning(&pendingReasoning, responsesItemReasoningText(item))
if pendingReasoning != "" {
parts = append([]dto.ClaudeMediaMessage{{
Type: "thinking",
Thinking: kitutil.GetPointer(pendingReasoning),
}}, parts...)
pendingReasoning = ""
}
} else if preserveReasoning {
claudeRequest.Messages = appendClaudeReasoning(claudeRequest.Messages, pendingReasoning)
pendingReasoning = ""
}
if role == "system" {
systemMessages = append(systemMessages, parts...)
continue
Expand All @@ -115,7 +146,9 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
})
}
}

if preserveReasoning {
claudeRequest.Messages = appendClaudeReasoning(claudeRequest.Messages, pendingReasoning)
}
if len(systemMessages) > 0 {
claudeRequest.System = systemMessages
}
Expand All @@ -128,6 +161,34 @@ func OpenAIResponsesRequestToClaudeMessages(c context.Context, info convmeta.Met
return claudeRequest, nil
}

func appendClaudeReasoning(messages []dto.ClaudeMessage, reasoning string) []dto.ClaudeMessage {
if reasoning == "" {
return messages
}
thinking := dto.ClaudeMediaMessage{
Type: "thinking",
Thinking: kitutil.GetPointer(reasoning),
}
if len(messages) > 0 && messages[len(messages)-1].Role == "assistant" {
last := messages[len(messages)-1]
parts := claudeMessageContentParts(last.Content)
insertAt := 0
for insertAt < len(parts) && parts[insertAt].Type == "thinking" {
insertAt++
}
parts = append(parts, dto.ClaudeMediaMessage{})
copy(parts[insertAt+1:], parts[insertAt:])
parts[insertAt] = thinking
last.Content = parts
messages[len(messages)-1] = last
return messages
}
return append(messages, dto.ClaudeMessage{
Role: "assistant",
Content: []dto.ClaudeMediaMessage{thinking},
})
}

func responsesFunctionDeclarationsToClaudeTools(functions []dto.FunctionRequest) []any {
tools := make([]any, 0, len(functions))
for _, function := range functions {
Expand Down
Loading
Loading